I'm a student of functional programming. I am still weaning myself off the old variable-mutation habits. But sometimes I get stuck. Ok, so here is the question--suppose we have the following closure
const bookShelf = () => {
let books = []
const listBooks = () => books
const addBook = (book) => {
books = books.concat(book)
return function removeBook() { books = books.filter( b => b !== book ) }
}
return {addBook,listBooks}
}
const { addBook, listBooks } = bookShelf()
const removeMobyDick = addBook('Moby Dick')
const removeWalden = addBook('Walden')
removeWalden()
console.log(listBooks()) // ["Moby Dick"]
Note that I have one object which is mutated: books.
My question is, how can I refactor this code so that books is immutable yet I achieve the same end-result. Feel free to use a functional library like Ramda if need be. My naive thought here is somehow use recursion to pass in a new value of books and then pass that version back. Seems a bit overreach so I thought to seek out help from someone more knowledgable in this arena.
Thanks for your insight!
Just leave book constant in your bookshelf. That will require creating a new bookshelf every time of course, so the easiest approach is to make books be a parameter of the function:
function bookShelf(books) {
return {
listBooks() { return books },
addBook(book) { return bookShelf(books.concat([book])); }
}
}
const empty = bookShelf([]);
const shelfWithMobyDick = empty.addBook('Moby Dick');
const shelfWithMobyDickAndWalden = shelfWithMobyDick.addBook('Walden');
console.log(shelfWithMobyDick.listBooks());
As you can see, there's no need for a removeBook function - you just use the old value that had not yet included the book.
If you want to be able to remove the book you just added from an arbitrary bookshelf, you can also return both the new bookshelf and a remover function:
…,
addBook(book) {
return {
bookShelf: bookShelf(books.concat([book]));
removeThis(shelf) { return bookShelf(shelf.listBooks().filter(b => b !== book)); }
};
}
to be used as
const empty = bookShelf([]);
const {bookShelf: shelfWithMobyDick, removeThis: removeMobyDick} = empty.addBook('Moby Dick');
const {bookShelf: shelfWithMobyDickAndWalden, removeThis: removeWalden} = shelfWithMobyDick.addBook('Walden');
const shelfWithWalden = removeMobyDick(shelfWithMobyDickAndWalden);
console.log(shelfWithWalden.listBooks());
The bookshelf type doesn’t really seem to be accomplishing anything here, so let’s just make it a list (array).
let bookshelf = [];
Now it looks like you want a way to produce a list with a new item and a way of removing that item from the list. A little weird, but you can do that by returning both in a tuple (array):
const remove = (list, value) =>
list.filter(x => x !== value);
const addRemovable = (list, value) =>
[[...list, value], list => remove(list, value)];
let bookshelf = [];
let removeMobyDick;
let removeWalden;
[bookshelf, removeMobyDick] = addRemovable(bookshelf, 'Moby Dick');
[bookshelf, removeWalden] = addRemovable(bookshelf, 'Walden');
bookshelf = removeWalden(bookshelf);
console.log(bookshelf);
This doesn’t look nice and you probably wouldn’t want to write something like it, but it does achieve the same thing as your original.
Related
I'm trying to improve my JavaScript skills. I'm learning composability and functional patterns and I'm totally lost.
I have two functions: one mapping an array and the other called from within the previous function to generate the markup.
const names = ['peter', 'paul', 'patrice']
const namesMarkup = name => {
return `<p>${name}</p>`
}
const showNames = listOfNames => {
return listOfNames.map(el => {
return namesMarkup(el)
})
}
showNames(names)
I have been reading about HOF, which technically are functions that take a function as an argument and/or return a function.
How could I compose these functions to have a HOF?
I went through the basic examples like
const square = num => num * num
const plus10 = (num, callback) => {
return callback(num) + 10
}
console.log(addTwo(7, square))
but I cannot make my mind around the previous example and working with lists.
I will appreciate help since the more I research the more confused I get.
Your mistake is to assume an array for showNames. Never do this. Always implement the simplest version of a function. In FP array is a computational effect. Don't implement such an effectful function as default:
const nameMarkup = name => {
return `<p>${name}</p>`;
}
const nameMarkup2 = name => {
return `<p>${name.toUpperCase()}!</p>`;
}
const showName = f => name => {
const r = f(name);
/* do something useful with r */
return r;
}
const names = ['peter', 'paul', 'patrice']
console.log(
showName(nameMarkup) ("peter"));
// lift the HOF if you want to process a non-deterministic number of names:
console.log(
names.map(showName(nameMarkup2)));
Now swapping the markup just means to pass another function argument. Your showName is more general, because a HOF lets you pass part of the functionality.
If we drop the array requirement, your showNames doesn't do anything useful anymore. It still illustrates the underlying idea, though.
I am new at ReactJs and have a question about this two method:
1:
handleLike = movie => {
const movies = this.state.movies.map(m => {
if (m._id === movie._id) m.liked = !m.liked;
return m;
});
this.setState({ movies });
};
2:
handleLike = movie => {
const movies = [...this.state.movies];
const index = movies.indexOf(movie);
movies[index] = { ...movies[index] };
movies[index].liked = !movies[index].liked;
this.setState({ movies });
};
Q1: This two methods just toggle liked and work properly but i want to know there is any advantages or not?
Q2: What is purpose of this line in second method:
movies[index] = { ...movies[index] };
Don't use #1, at least not the way it is written. You are mutating the old state, which can easily cause bugs in react which assumes that state is immutable. You do create a new array, which is good, but you're not creating new elements inside the array. If you're changing one of the objects in the array, you need to copy that object before modifying it.
The better way to do #1 would be:
handleLike = movie => {
const movies = this.state.movies.map(m => {
if (m._id === movie._id) {
const copy = { ...m };
copy.liked = !m.liked;
return copy;
}
return m;
});
this.setState({ movies });
};
And that kind of gets to your question about #2 as well:
Q2: What is purpose of this line in second method:
movies[index] = { ...movies[index] };
The purpose of that is to make a copy of the movie. This lets you make a change to the copy, without modifying the old state.
Q1: This two methods just toggle liked and work properly but i want to know there is any advantages or not?
If you fix the mutation issue in #1, then it's pretty much a matter of preference.
I have the following piece of code which works 100% but I can't help but feel their is a better way or more simple way of writing this or maybe not 😄all and any help is always appreciated 👍🏼
const entity: any = response.organisation;
if (Array.isArray(responseKey)) {
// responseKey e.g. ['site', 'accounts']
// I feel this two declarations can be refactored
const list = this.flattenGraphqlList<T>(entity[responseKey[0]][responseKey[1]]);
const {totalCount} = entity[responseKey[0]][responseKey[1]];
return {list, totalCount};
}
const list = this.flattenGraphqlList<T>(entity[responseKey]);
const {totalCount} = entity[responseKey];
return {list, totalCount};
Don't do everything twice:
const entity: any = response.organisation;
const object = Array.isArray(responseKey)
? entity[responseKey[0]][responseKey[1]] // responseKey e.g. ['site', 'accounts']
: entity[responseKey];
const list = this.flattenGraphqlList<T>(object);
const {totalCount} = object;
return {list, totalCount};
I guess you can find a more descriptive name than object :-)
For the last lines, I personally would prefer not to use destructuring, but that's more a stylistic choice:
return {
list: this.flattenGraphqlList<T>(object),
totalCount: object.totalCount
};
Can someone explain the part in the exports section, I seem to lost and stuck for a while. Starting from importPromise. It seems like there's a lot going on, such as arrow functions and map method. I can't see where the data flows from where to where.
const keystone = require('keystone');
const PostCategory = keystone.list('PostCategory');
const Post = keystone.list('Post');
const importData = [
{ name: 'A draft post', category: 'Keystone JS' },
...
];
exports = function (done) {
const importPromise = importData.map(({ name, category }) => createPost({ name, category }));
importPromise.then(() => done()).catch(done);
};
const categories = {};
const createPost = ({ name, category }) => {
let postCategory = new PostCategory.model({ category });
if (categories[category]) {
postCategory = categories[category];
}
categories[category] = postCategory;
const post = new Post.model({ name });
post.category = postCategory._id.toString();
return Promise.all([
post.save(),
postCategory.save()
]);
}
Quite some ES6 magic involved :)
const importPromise = importData.map(({ name, category }) => createPost({ name, category }));
importdata is an array. What the map function on an array does is to take every item of the array and apply a function to it, then return a new array with all of the items in the original array, but modified. map function
Instead of writing .map(function(item) { ... } the preferred way of writing this in ES6 is with a fat arrow function, i.e. .map((item) => ...
The third bit of magic is called destructuring assignment. What it does is it takes an object in this case, and assigns the obj.name and obj.category to two new variables name and category. We can use those variables within our function as if we had called the function with two separate arguments.
Now remember our map function dictates we write a function that takes an array item as a parameter, and returns the modified item. So what we end up with is a map function looping through the arguments of importData, taking name and category of each item and calling another function createPost with them. The result of createPost is the new value of the item, and it all gets appended to an array of the same size as the old one, with the modified items.
importPromise.then(() => done()).catch(done);
createPost creates a promise out of each item. You can read more about Promise here. The .then method on a Promise takes functions as its argument; to be called when the promise returns (either with success or an error). The () => done() is simply a function in fat arrow syntax that takes no arguments and calls the done function. .catch takes a function as well (done is a function), and is executed when the promise returns an error. NB. this way the done function is called both on success and on error!
--and no, this code won't work because what we're creating on the first line with importPromise is not actually a promise, but an array of promises!
Good luck with reading up, and as Beri suggests it might be worthwhile translating the code to es5 to follow along.
I don't know much about KeystoneJS. Anyway, here are my two cents:
const importData = [
{ name: 'A draft post', category: 'Keystone JS' },
// ...
];
importData is an Array which holds a bunch of Object instances, each having a name and category key with String values. For me, it appears this is some "mock data", which is simply put there for testing purposes.
I shifted the next parts, because it makes the code more understandable.
This part:
const categories = {};
Looks to me like the person who wrote it tried to implement some form of "caching". The categories constant is a mere "container" to store posts in so they can be reused later instead of being recreated. The createPost function reveals the purpose of it if you read through it.
const createPost = ({ name, category }) => {
let postCategory = new PostCategory.model({ category });
if (categories[category]) {
postCategory = categories[category];
}
categories[category] = postCategory;
const post = new Post.model({ name });
post.category = postCategory._id.toString();
return Promise.all([
post.save(),
postCategory.save()
]);
}
The first if seems to be there to make use of the "caching" construct (const category), but the way it does it is a bit confusing. Here's how I'd refactor it:
const createPost = ({ name, category }) => {
if (!categories[category]) {
categories[category] = new PostCategory.model({ category });;
}
const post = new Post.model({ name });
post.category = categories[category]._id.toString();
return Promise.all([
post.save(),
categories[category].save()
]);
}
Finally for the exports part:
The module exports a function which awaits a callback as argument (done). It then tries to create a Promise from all the "posts" of the mock data (and - to my understanding - fails), by mapping the createPost function over it. The reason I think it fails is because Array.prototype.map doesn't return a Promise, it returns a new Array instance which doesn't have a then method (see next line). Instead of calling then, it should be a Promise.all again. When that final Promise succeeds (or fails), the callback is called with the result.
exports = function (done) {
const importPromise = importData.map(({ name, category }) => createPost({ name, category }));
importPromise.then(() => done()).catch(done);
};
Again, I'd rewrite it this way:
exports = function (done) {
Promise.all(importData.map(createPost)).then(done).catch(done);
};
Or just return the final Promise and get rid of the done callback altogether.
https://babeljs.io/repl
You can use this tool to translate.
Here's a simple test I've written of what I want to do with an immutable object
it('adds a new map with loaded data where the key is the ticker symbol', () => {
const state = Map();
const tickers = List.of('AAPL', 'TSLA', 'GOOGL');
const nextState = addTickerKeys(state, tickers);
expect(nextState).to.equal(fromJS({
tickers: ['AAPL', 'TSLA', 'GOOGL'],
data: {
AAPL: {},
TSLA: {},
GOOGL: {}
}
}));
})
How do I add the data object and the corresponding keys with empty data into the state object?
Here is what I have tried so far
export function addTickerKeys(state, tickers) {
const newState = setTickers(state, tickers);
const tickerList = newState.get('tickers');
return tickerList.forEach((value, key, iter) => {
return newState.setIn(['data', key]);
})
}
I've tried substituting value, key and iter in place of return newState.setIn(['data', key]) as per the docs (https://facebook.github.io/immutable-js/docs/#/Map/set)
However, I get the same response back each time,
AssertionError: expected 3 to equal { Object (size, _root, ...) }
Can someone explain to me what's going wrong? This seems a simple enough task but I seem to be struggling with Immutable objects and the documentation in TypeScript doesn't help.
Here's a quick answer to this, that I just figured out.
Immutable does not seem to have a great inbuilt function for this task and the way you have to return the state from a pure function is just frustrating.
Here's a quick and dirty solution to adding the object key value pairs.
export function addTickerKeys(state) {
const tickerArray = state.get('tickers');
let newState = Map();
for(let i = 0; i < tickerArray.size; i++){
ctr++;
newState = state.setIn(['data', tickerArray.get(i)], Map());
state = state.concat(newState);
if(i === tickerArray.size - 1){
return state;
}
}
}
If anyone else still has a different answer, a more elegant inbuilt solution perhaps do share.
A quick comment on your first solution:
Much like the native forEach method on Array.prototype, the forEach method on immutable List types is used for executing some side effect upon each iteration of the List. One subtle difference between the two, however, is when the callback function returns false on the immutable forEach, it will immediately end execution of the loop, whereas the native forEach is interminable. Here, you utilize the forEach method on Lists but are returning a value, suggesting that you might have confused it with the map method on immutable types and arrays. Unfortunately, map is not what you're looking for either.
Now, another solution:
function addTickerKeys(state, tickers) {
return tickers.reduce((acc, ticker) => {
return acc.setIn([ 'data', ticker ], Map());
}, Map({
tickers: List(tickers),
}))
}