I come across this use-case at work all the time and I feel like there must be a way to compose fullName in a point-free way without defining cat as a parameter:
const cats = [
{ name: 'Bob', lastName: 'Ross' },
{ name: 'Frank', lastName: 'Langella' },
];
// this bugs me
const fullName = cat => add(
prop('name', cat),
prop('lastName', cat)
);
const isEqual = curry((a, b) => a === b);
const isBobRoss = compose(isEqual('BobRoss'), fullName);
edit: some of the helpers above in case it helps understand the challenge
/**
* compose :: ((a -> b), (b -> c), ..., (y -> z)) -> a -> z
*/
const compose = (...fns) => (...args) =>
fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];
/**
* curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c
*/
function curry(fn) {
const arity = fn.length;
return function $curry(...args) {
if (args.length < arity) {
return $curry.bind(null, ...args);
}
return fn.call(null, ...args);
};
/**
* add :: a -> b -> a + b
*/
const add = curry((x, y) => x + y)
/**
* prop :: String -> Object -> a
*/
const prop = curry((p, obj) => obj[p])
}
This is no exactly function composition, but sure you can write a helper function for it. Ramda does know it as converge, here's a simplified (unary) version of it:
const converge = (fn, wraps) => arg => fn(...wraps.map(wrap => wrap(arg)));
cost fullName = converge(add, [prop('name'), prop('lastName')]);
(Initially I decline to commit to a specific functional programming library for JS/TS and work in general concepts)
Notice that your curred prop function takes a key and returns a reader monad. This will be useful.
Assuming type Obj = {name:string,lastName:string}, then your curried prop fn is (key:'name'|'lastName') => Reader<Obj,string>
You can use a sequence type function to combine two reader monads into a single one, as such:
const getNameParts = sequence(prop('name'), prop('lastName')) // Reader<Obj, [string,string]>
Then you can map the [string,string] to be a single string like in your add function
const add = as => as.reduce((acc, item) => acc + item)
So if you can lift add into your reader monad's computational context (here using a proposed `map: ((a:A)=>B)=>(fa:F<A>)=>F<B>), then compose these operations:
const buildNameFromObject = compose(getNameParts, map(add)) // Reader<Obj, string>
There we have it.
const personsName = buildNameFromObject(someObject) // string
fp-ts is a library that provides everything I just mentioned, and using that library (with a few function name changes to align with fp-ts's vocabulary),
import { reader, map } from 'fp-ts/lib/Reader'
import { sequenceT } from 'fp-ts/lib/Apply'
import { pipe } from 'fp-ts/lib/pipeable'
const getNameParts = sequenceT(reader)(prop('name'), prop('lastName'))
const buildNameFromObject = pipe(getNameParts, map(add)) // Reader<Obj, string>, which is a fancy way of writing (o:Obj)=>string
buildNameFromObject({name:'Foo', lastName: 'Bar'}) // 'FooBar'
Your "fullName" function (buildNameFromObject) is now point free.
I am currently learning functional programming using HyperappJS (V2) and RamdaJS. My first project is a simple blog app where users can comment on posts or other comments. The comments are represented as a tree structure.
My state looks something like this:
// state.js
export default {
posts: [
{
topic: `Topic A`,
comments: []
},
{
topic: `Topic B`,
comments: [
{
text: `Comment`,
comments: [ /* ... */ ]
}
]
},
{
topic: `Topic C`,
comments: []
}
],
otherstuff: ...
}
When the user wants to add a comment I pass the current tree item to my addComment-action. There I add the comment to the referenced item and return a new state object to trigger the view update.
So, currently I'm doing this and it's working fine:
// actions.js
import {concat} from 'ramda'
export default {
addComment: (state, args) => {
args.item.comments = concat(
args.item.comments,
[{text: args.text, comments: []}]
)
return {...state}
}
}
My question: Is this approach correct? Is there any way to clean up this code and make it more functional? What I am looking for would be something like this:
addComment: (state, args) => ({
...state,
posts: addCommentToReferencedPostItemAndReturnUpdatedPosts(args, state.posts)
})
Ramda is intentionally designed not to modify user data. Passing something by reference won't help; Ramda will still refuse to alter it.
One alternative is to see if you can pass the path to the node to which you want to add the comment. Ramda can use a path with lensPath and over to make a version that will return a new state object, something like this:
const addComment = (state, {text, path}) =>
over (
lensPath (['posts', ...intersperse ('comments', path), 'comments']),
append ({text, comments: []}),
state
)
const state = {
posts: [
{topic: `Topic A`, comments: []},
{topic: `Topic B`, comments: [{text: `Comment`, comments: [
{text: 'foo', comments: []}
// path [1, 0] will add here
]}]},
{topic: `Topic C`, comments: []}
],
otherstuff: {}
}
console .log (
addComment (state, {path: [1, 0], text: 'bar'})
)
//=> {
// posts: [
// {topic: `Topic A`, comments: []},
// {topic: `Topic B`, comments: [{text: `Comment`, comments: [
// {text: 'foo', comments: []},
// {text: 'bar', comments: []}
// ]}]},
// {topic: `Topic C`, comments: []}
// ],
// otherstuff: {}
// }
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {over, lensPath, intersperse, append} = R </script>
Here the path we use is [1, 0], representing the second post (index 1) and the first comment (index 0) within it.
We could write more a more sophisticated lens to traverse the object if the path is not enough.
I don't know if this is an overall improvement, but it's definitely a more appropriate use of Ramda. (Disclaimer: I'm one of the authors of Ramda.)
Here's an approach where we 1) locate the target object in your state tree, and then 2) transform the located object. Let's assume that your tree has some way to id the individual objects -
const state =
{ posts:
[ { id: 1 // <-- id
, topic: "Topic A"
, comments: []
}
, { id: 2 // <-- id
, topic: "Topic B"
, comments: []
}
, { id: 3 // <-- id
, topic: "Topic C"
, comments: []
}
]
, otherstuff: [ 1, 2, 3 ]
}
search
You could start by writing a generic search which yields the possible path(s) to a queried object -
const search = function* (o = {}, f = identity, path = [])
{ if (!is (o, Object))
return
if (f (o))
yield path
for (const [ k, v ] of Object.entries(o))
yield* search (v, f, [ ...path, k ])
}
Let's locate all objects where id is greater than 1 -
for (const path of search (state, ({ id = 0 }) => id > 1))
console .log (path)
// [ "posts", "1" ]
// [ "posts", "2" ]
These "paths" point to objects in your state tree where the predicate, ({ id = 0 }) => id > 1), is true. Ie,
// [ "posts", "1" ]
state.posts[1] // { id: 2, topic: "Topic B", comments: [] }
// [ "posts", "2" ]
state.posts[2] // { id: 3, topic: "Topic C", comments: [] }
We will use search to write higher-order functions like searchById, which encodes our intentions more clearly -
const searchById = (o = {}, q = 0) =>
search (o, ({ id = 0 }) => id === q)
for (const path of searchById(state, 2))
console .log (path)
// [ "posts", "1" ]
transform
Next we can write transformAt which takes an input state object, o, a path, and a transformation function, t.
Using inductive reasoning we plainly encode our intentions -
When the query, q, is None, the path has been exhausted and it's time to run the transformation, t, on the input object, o.
(inductive) q is not empty. If the input, o, is an object, using Object.assign create a new object where its new q property is a transform of its old q property, o[q].
(inductive) q is not empty and o is not an object. We cannot expect to lookup q on a non-object, therefore raise an error to signal to that transformAt was given an invalid path.
const None =
Symbol ()
const transformAt =
( o = {}
, [ q = None, ...path ] = []
, t = identity
) =>
q === None // 1
? t (o)
: is (o, Object) // 2
? Object.assign
( is (o, Array) ? [] : {}
, o
, { [q]: transformAt (o[q], path, t) }
)
: raise (Error ("transformAt: invalid path")) // 3
Now we can easily write appendComment which takes an input, state, a comment's id, parentId, and a new comment, c -
const append = x => a =>
[ ...a, x ]
const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
return transformAt // <-- only transform first; return
( state
, [ ...path, "comments" ]
, append (c)
)
return state // <-- if no search result, return unmodified state
}
Recall search generates all possible paths to where the predicate query returns true. You have to make a choice how you will handle the scenario where a query returns more than one result. Consider data like -
const otherState =
{ posts: [ { type: "post", id: 1, ... }, ... ]
, images: [ { type: "image", id: 1, ... }, ... ]
}
Using searchById(otherState, 1) would get two objects where id = 1. In appendComment we choose only to modify the first match. It's possible to modify all the search results, if we wanted -
// but don't actually do this
const appendComment = (state = {}, parentId = 0, c = {}) =>
Array
.from (searchById (state, parentId)) // <-- all results
.reduce
( (r, path) =>
transformAt // <-- transform each
( r
, [ ...path, "comments" ]
, append (c)
)
, state // <-- init state
)
But in this scenario, we probably don't want duplicate comments in our app. Any querying function like search may return zero, one, or more results and you have to decide how your program responds in each scenario.
put it together
Here are the remaining dependencies -
const is = (t, T) =>
t?.constructor === T
const raise = e =>
{ throw e }
const identity = x =>
x
Let's append our first new comment to id = 2, "Topic B" -
const state1 =
appendComment
( state
, 2
, { id: 4, text: "nice article!", comments: [] }
)
Our first state revision, state1, will be -
{ posts:
[ { id: 1
, topic: "Topic A"
, comments: []
}
, { id: 2
, topic: "Topic B"
, comments:
[ { id: 4 //
, text: "nice article!" // <-- newly-added
, comments: [] // comment
} //
]
}
, { id: 3
, topic: "Topic C"
, comments: []
}
]
, otherstuff: [ 1, 2, 3 ]
}
And we'll append another comment, nested on that one -
const state2 =
appendComment
( state
, 4 // <-- id of our last comment
, { id: 5, text: "i agree!", comments: [] }
)
This second revision, state2, will be -
{ posts:
[ { id: 1, ...}
, { id: 2
, topic: "Topic B"
, comments:
[ { id: 4
, text: "nice article!"
, comments:
[ { id: 5 // nested
, text: "i agree!" // <-- comment
, comments: [] // added
} //
]
}
]
}
, { id: 3, ... }
]
, ...
}
code demonstration
In this demo we will,
create state1 by modifying state to add the first comment
create state2 by modifying state1 to add the second (nested) comment
print state2 to show the expected state
print state to show that the original state is not modified
Expand the snippet below to verify the results in your own browser -
const None =
Symbol ()
const is = (t, T) =>
t?.constructor === T
const raise = e =>
{ throw e }
const identity = x =>
x
const append = x => a =>
[ ...a, x ]
const search = function* (o = {}, f = identity, path = [])
{ if (!is (o, Object))
return
if (f (o))
yield path
for (const [ k, v ] of Object.entries(o))
yield* search (v, f, [ ...path, k ])
}
const searchById = (o = {}, q = 0) =>
search (o, ({ id = 0 }) => id === q)
const transformAt =
( o = {}
, [ q = None, ...path ] = []
, t = identity
) =>
q === None
? t (o)
: is (o, Object)
? Object.assign
( is (o, Array) ? [] : {}
, o
, { [q]: transformAt (o[q], path, t) }
)
: raise (Error ("transformAt: invalid path"))
const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
return transformAt
( state
, [ ...path, "comments" ]
, append (c)
)
return state
}
const state =
{ posts:
[ { id: 1
, topic: "Topic A"
, comments: []
}
, { id: 2
, topic: "Topic B"
, comments: []
}
, { id: 3
, topic: "Topic C"
, comments: []
}
]
, otherstuff: [ 1, 2, 3 ]
}
const state1 =
appendComment
( state
, 2
, { id: 4, text: "nice article!", comments: [] }
)
const state2 =
appendComment
( state1
, 4
, { id: 5, text: "i agree!", comments: [] }
)
console.log("state2", JSON.stringify(state2, null, 2))
console.log("original", JSON.stringify(state, null, 2))
immutablejs
The techniques described above are parallel to the other (excellent) answer using lenses provided by Scott. The notable difference here is we start with an unknown path to the target object, find the path, then transform the state at the discovered path.
The techniques in these two answers could even be combined. search yields paths that could be used to create R.lensPath and then we could update the state using R.over.
And a higher-level technique is lurking right around the corner. This one comes from the understanding that writing functions like transformAt is reasonably complex and it's difficult to get them right. At the heart of the problem, our state object is a plain JS object, { ... }, which offers no such feature as immutable updates. Nested within those object we use arrays, [ ... ], that have the same issue.
Data structures like Object and Array were designed with countless considerations that may not match your own. It is for this reason why you have the ability to design your own data structures that behave the way you want. This is an often overlooked area of programming, but before we jump in and try to write our own, let's see how the Wise Ones before us did it.
One example, ImmutableJS, solves this exact problem. The library gives you a collection of data structures as well as functions that operate on those data structures, all of which guarantee immutable behaviour. Using the library is convenient -
const append = x => a => // ⌫ unused
[ ...a, x ]
const { fromJS } =
require ("immutable")
const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
return transformAt
( fromJS (state) // <-- 1. from JS to immutable
, [ ...path, "comments" ]
, list => list .push (c) // <-- 2. immutable push
)
.toJS () // <-- 3. from immutable to JS
return state
}
Now we write transformAt with the expectation that it will be given an immutable structure -
const is = (t, T) => // ⌫ unused
t?.constructor === T
const { Map, isCollection, get, set } =
require ("immutable")
const transformAt =
( o = Map () // <-- empty immutable object
, [ q = None, ...path ] = []
, t = identity
) =>
q === None
? t (o)
: isCollection (o) // <-- immutable object?
? set // <-- immutable set
( o
, q
, transformAt
( get (o, q) // <-- immutable get
, path
, t
)
)
: raise (Error ("transformAt: invalid path"))
Hopefully we can begin to see transformAt as a generic function. It is not coincidence that ImmutableJS includes functions to do exactly this, getIn and setIn -
const None = // ⌫ unused
Symbol ()
const raise = e => // ⌫ unused
{ throw e }
import { Map, setIn, getIn } from "immutable"
const transformAt =
( o = Map () // <-- immutable Map
, path = []
, t = identity
) =>
setIn // <-- immutable set by path
( o
, path
, t (getIn (o, path)) // <-- immutable get by path
)
To my surprise, even transformAt is implemented exactly as updateIn -
const identity = x => // ⌫ unused
x
const transformAt = //
( o = Map () // ⌫ unused
, path = [] //
, t = identity //
) => ... //
import { fromJS, updateIn } from "immutable"
const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
return updateIn // <-- immutable update by path
( fromJS (state)
, [ ...path, "comments" ]
, list => list .push (c)
)
.toJS ()
return state
}
This the lesson of higher-level data structures. By using structures designed for immutable operations, we reduce the overall complexity of our entire program. As a result, the program can now be written in less than 30 lines of straightforward code -
//
// complete implementation using ImmutableJS
//
import { fromJS, updateIn } from "immutable"
const search = function* (o = {}, f = identity, path = [])
{ if (Object (o) !== o)
return
if (f (o))
yield path
for (const [ k, v ] of Object.entries(o))
yield* search (v, f, [ ...path, k ])
}
const searchById = (o = {}, q = 0) =>
search (o, ({ id = 0 }) => id === q)
const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
return updateIn
( fromJS (state)
, [ ...path, "comments" ]
, list => list .push (c)
)
.toJS ()
return state
}
export { search, searchById, appendComment }
ImmutableJS is just one possible implementation of these structures. Many others exist, each with their unique APIs and trade-offs. You can pick from a pre-made library or you can custom tailor your own data structures to meet your exact needs. Either way, hopefully you can see the benefits provided by well-designed data structures and perhaps gain insight on why popular structures of today were invented in the first place.
Expand the snippet below to run the ImmutableJS version of the program in your browser -
const { fromJS, updateIn } =
Immutable
const search = function* (o = {}, f = identity, path = [])
{ if (Object (o) !== o)
return
if (f (o))
yield path
for (const [ k, v ] of Object.entries(o))
yield* search (v, f, [ ...path, k ])
}
const searchById = (o = {}, q = 0) =>
search (o, ({ id = 0 }) => id === q)
const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
return updateIn
( fromJS (state)
, [ ...path, 'comments' ]
, list => list .push (c)
)
.toJS ()
return state
}
const state =
{ posts:
[ { id: 1
, topic: 'Topic A'
, comments: []
}
, { id: 2
, topic: 'Topic B'
, comments: []
}
, { id: 3
, topic: 'Topic C'
, comments: []
}
]
, otherstuff: [ 1, 2, 3 ]
}
const state1 =
appendComment
( state
, 2
, { id: 4, text: "nice article!", comments: [] }
)
const state2 =
appendComment
( state1
, 4
, { id: 5, text: "i agree!", comments: [] }
)
console.log("state2", JSON.stringify(state2, null, 2))
console.log("original", JSON.stringify(state, null, 2))
<script src="https://unpkg.com/immutable#4.0.0-rc.12/dist/immutable.js"></script>
So, I have the following code, but flow errors keep popping up. I've tried to cast the Object.entries, but just won't work - others things to. Any insight?
type Fields = {
name: string,
func: (*) => boolean
};
type S = {
key1: Fields,
bill: Fields
}
var a: S = {
key1: {name: 'mary', func: (str) => str === 'mary'},
bill: {name: 'bill', func: (str) => str === 'bill'}
}
var c = Object
.entries(a)
.map(([key, obj]) => obj.func(key) ? obj : false)
.filter(f => f)
.reduce((acc, c) => {
return 'something here'
}, {});
I've left some things off, but the slow is the same. Flow is reading that entries as a return Tuple Type. I've tried all sorts of things, but instead of mudding things up, I left it untouched.
I can't seem to annotate the destructured items here ([key, obj]), get tuple errors...
Any assistance on getting that code assigned to var c, to work with annotations etc..?
The errors I get:
Cannot call method on mixed type (from obj.func)
Cannot assign value in Tuple etc..
The error is accurate. Object.entries has the type
entries(object: any): Array<[string, mixed]>;
It has no way to know what the type of the second item in the tuple will be. That means your code
.map(([key, obj]) => obj.func(key) ? obj : false)
would need to do
.map(([key, obj]) => {
if (typeof obj.func !== 'function') throw new Error();
return obj.func(key) ? obj : false;
})
so that flow knows that it is guaranteed to be a function.
Alternatively, you could change your data structure to use a type where the second item in the tuple has a guaranteed type, like Map, e.g.
type Fields = {
name: string,
func: (string) => boolean
};
type S = Map<string, Fields>;
var a: S = new Map([
['key1', {name: 'mary', func: (str) => str === 'mary'}],
['bill', {name: 'bill', func: (str) => str === 'bill'}],
]);
var c = Array.from(a, ([key, obj]) => obj.func(key) ? obj : false)
.filter(f => f)
.reduce((acc, c) => {
return 'something here'
}, {});
In my case, I had:
let objectsByName : { [string] : MyObjectType } = {}; //simple map
...
objectsByName[object.name] = object; //call repeatedly to populate map.
...
let results : any[] = []; //next we will populate this
Trying to operate on it like this failed for Flow (though this is executable JavaScript):
for (let [name : string, object : MyObjectType] of Object.entries(objectsByName))
{
let result = doSomethingWith(object); //<- error on arg
results.push(result);
}
This succeeded for Flow:
for (let name : string in objectsByName)
{
let object = objectsByName[name];
let result = doSomethingWith(object); //<- error on arg
results.push(result);
}
It is annoying having to change code structure to suit a supposedly non-intrusive system like Flow comment types, which I chose in the hopes of making my code completely oblivious to Flow's presence. In this case I have to make an exception and structure my code as Flow wants it.
Replacing Object.entries with Object.keys + lookup fixes flow errors for me assuming the input object is properly typed.
i.e. replace Object.entries(a) with Object.keys(a).map(key => [key, a[key]])
This works with flow:
type Fields = {
name: string,
func: (*) => boolean
};
type S = {
key1: Fields,
bill: Fields
}
var a: S = {
key1: {name: 'mary', func: (str) => str === 'mary'},
bill: {name: 'bill', func: (str) => str === 'bill'}
}
var c = Object
.keys(a)
.map(key => a[key].func(key) ? obj : false)
.filter(f => f)
.reduce((acc, c) => {
return 'something here'
}, {});