I've been working with anamorphisms or unfold in JavaScript lately and I thought I might share them with you using your program as a context to learn them in
const getAllStuff = async (initUrl = '/0') =>
asyncUnfold
( async (next, done, stuff) =>
stuff.next
? next (stuff, await get (stuff.next))
: done (stuff)
, await get (initUrl)
)
const get = async (url = '') =>
fetch (url) .then (res => res.json ())
To demonstrate that this works, we introduce a fake fetch and database DB with a fake delay of 250ms per request
const fetch = (url = '') =>
Promise.resolve ({ json: () => DB[url] }) .then (delay)
const delay = (x, ms = 250) =>
new Promise (r => setTimeout (r, ms, x))
const DB =
{ '/0': { a: 1, next: '/1' }
, '/1': { b: 2, next: '/2' }
, '/2': { c: 3, d: 4, next: '/3' }
, '/3': { e: 5 }
}
Now we just run our program like this
getAllStuff () .then (console.log, console.error)
// [ { a: 1, next: '/1' }
// , { b: 2, next: '/2' }
// , { c: 3, d: 4, next: '/3' }
// , { e: 5 }
// ]
And finally, here's asyncUnfold
const asyncUnfold = async (f, init) =>
f ( async (x, acc) => [ x, ...await asyncUnfold (f, acc) ]
, async (x) => [ x ]
, init
)
Program demonstration 1
const asyncUnfold = async (f, init) =>
f ( async (x, acc) => [ x, ...await asyncUnfold (f, acc) ]
, async (x) => [ x ]
, init
)
const getAllStuff = async (initUrl = '/0') =>
asyncUnfold
( async (next, done, stuff) =>
stuff.next
? next (stuff, await get (stuff.next))
: done (stuff)
, await get (initUrl)
)
const get = async (url = '') =>
fetch (url).then (res => res.json ())
const fetch = (url = '') =>
Promise.resolve ({ json: () => DB[url] }) .then (delay)
const delay = (x, ms = 250) =>
new Promise (r => setTimeout (r, ms, x))
const DB =
{ '/0': { a: 1, next: '/1' }
, '/1': { b: 2, next: '/2' }
, '/2': { c: 3, d: 4, next: '/3' }
, '/3': { e: 5 }
}
getAllStuff () .then (console.log, console.error)
// [ { a: 1, next: '/1' }
// , { b: 2, next: '/2' }
// , { c: 3, d: 4, next: '/3' }
// , { e: 5 }
// ]
Now say you wanted to collapse the result into a single object, we could do so with a reduce – this is closer to what your original program does. Note how the next property honors the last value when a key collision happens
getAllStuff ()
.then (res => res.reduce ((x, y) => Object.assign (x, y), {}))
.then (console.log, console.error)
// { a: 1, next: '/3', b: 2, c: 3, d: 4, e: 5 }
If you're sharp, you'll see that asyncUnfold could be changed to output our object directly. I chose to output an array because the sequence of the unfold result is generally important. If you're thinking about this from a type perspective, each foldable type's fold has an isomorphic unfold.
Below we rename asyncUnfold to asyncUnfoldArray and introduce asyncUnfoldObject. Now we see that the direct result is achievable without the intermediate reduce step
const asyncUnfold = async (f, init) =>
const asyncUnfoldArray = async (f, init) =>
f ( async (x, acc) => [ x, ...await asyncUnfoldArray (f, acc) ]
, async (x) => [ x ]
, init
)
const asyncUnfoldObject = async (f, init) =>
f ( async (x, acc) => ({ ...x, ...await asyncUnfoldObject (f, acc) })
, async (x) => x
, init
)
const getAllStuff = async (initUrl = '/0') =>
asyncUnfold
asyncUnfoldObject
( async (next, done, stuff) =>
, ...
)
getAllStuff ()
.then (res => res.reduce ((x, y) => Object.assign (x, y), {}))
.then (console.log, console.error)
// { a: 1, next: '/3', b: 2, c: 3, d: 4, e: 5 }
But having functions with names like asyncUnfoldArray and asyncUnfoldObject is completely unacceptable, you'll say - and I'll agree. The entire process can be made generic by supplying a type t as an argument
const asyncUnfold = async (t, f, init) =>
f ( async (x, acc) => t.concat (t.of (x), await asyncUnfold (t, f, acc))
, async (x) => t.of (x)
, init
)
const getAllStuff = async (initUrl = '/0') =>
asyncUnfoldObject
asyncUnfold
( Object
, ...
, ...
)
getAllStuff () .then (console.log, console.error)
// { a: 1, next: '/3', b: 2, c: 3, d: 4, e: 5 }
Now if we want to build an array instead, just pass Array instead of Object
const getAllStuff = async (initUrl = '/0') =>
asyncUnfold
( Array
, ...
, ...
)
getAllStuff () .then (console.log, console.error)
// [ { a: 1, next: '/1' }
// , { b: 2, next: '/2' }
// , { c: 3, d: 4, next: '/3' }
// , { e: 5 }
// ]
Of course we have to concede JavaScript's deficiency of a functional language at this point, as it does not provide consistent interfaces for even its own native types. That's OK, they're pretty easy to add!
Array.of = x =>
[ x ]
Array.concat = (x, y) =>
[ ...x, ...y ]
Object.of = x =>
Object (x)
Object.concat = (x, y) =>
({ ...x, ...y })
Program demonstration 2
Array.of = x =>
[ x ]
Array.concat = (x, y) =>
[ ...x, ...y ]
Object.of = x =>
Object (x)
Object.concat = (x, y) =>
({ ...x, ...y })
const asyncUnfold = async (t, f, init) =>
f ( async (x, acc) => t.concat (t.of (x), await asyncUnfold (t, f, acc))
, async (x) => t.of (x)
, init
)
const getAllStuff = async (initUrl = '/0') =>
asyncUnfold
( Object // <-- change this to Array for for array result
, async (next, done, stuff) =>
stuff.next
? next (stuff, await get (stuff.next))
: done (stuff)
, await get (initUrl)
)
const get = async (url = '') =>
fetch (url).then (res => res.json ())
const fetch = (url = '') =>
Promise.resolve ({ json: () => DB[url] }) .then (delay)
const delay = (x, ms = 250) =>
new Promise (r => setTimeout (r, ms, x))
const DB =
{ '/0': { a: 1, next: '/1' }
, '/1': { b: 2, next: '/2' }
, '/2': { c: 3, d: 4, next: '/3' }
, '/3': { e: 5 }
}
getAllStuff () .then (console.log, console.error)
// { a: 1, next: '/3', b: 2, c: 3, d: 4, e: 5 }
Finally, if you're fussing about touching properties on the native Array or Object, you can skip that and instead pass a generic descriptor in directly
const getAllStuff = async (initUrl = '/0') =>
asyncUnfold
( { of: x => [ x ], concat: (x, y) => [ ...x, ...y ] }
, ...
)
getAllStuff () .then (console.log, console.error)
// [ { a: 1, next: '/1' }
// , { b: 2, next: '/2' }
// , { c: 3, d: 4, next: '/3' }
// , { e: 5 }
// ]