Refactor expectMap/flatMap/map into one function#13
Conversation
Summary:
- usage of `map`, `flatMap`, `expectMap` and their internal use of `transformResult` and `expect` were hard to grasp
- merged `map`, `flatMap` and `expectMap` into a new applicator `chain` able to handle nil values, raw values, results, optionals, promises
- it is like `map` except it also prevents otherwise resulting nested objects and supports more types
- IMHO `chain` is a good naming choice because:
- it enables the developer to distinguish between js arrays and lonad results (searchable)
- it is also known as flatmap or bind in other languages (relatable)
Test Plan: npm test
Reviewers: kwisatz, lattam, paul
Subscribers: max, elia, federico
Differential Revision: https://cator.1024.lu/D1922
| return this.value; | ||
| }, | ||
|
|
||
| map(λ) { |
There was a problem hiding this comment.
We need to preserve compat for this which users of a monad-like will expect there to exist, so we can add but not remove
There was a problem hiding this comment.
Additionally, I like to have something that is clear and predictable instead of something magical (which more often than not is unpredictable!)
There was a problem hiding this comment.
Well, I thought map is redundant in monads. flatMap already does that
There was a problem hiding this comment.
map and flatMap are supposed to be different in monads.
For any monad Monad:
map (or fmap) takes a function which has the signature: a => b
whereas
flatMap (or bind, chain, or (>>=)) takes a function with the signature a => Monad b
| return transformResult(() => λ(this.value), Ok); | ||
| }, | ||
|
|
||
| flatMap(λ) { |
There was a problem hiding this comment.
that's why I chose to have aliases
There was a problem hiding this comment.
flatMap is not supposed to do any trick, and the management of Promises is already more than it should do
| flatMap(λ) { | ||
| return transformResult(() => λ(this.value), Result.expect); | ||
| chain(λ) { | ||
| try { |
There was a problem hiding this comment.
DecideWrapper should encapsulate all of the logic to transform values. I see why you need that, but it's a bit weird to split the logic in two places (the try catch and decideWrapper)
floriansimon1
left a comment
There was a problem hiding this comment.
Not gonna lie, this feels like another do-all method to rule them all, with its own unpredictable choices. That was the goal of expectMap and it's been a miserable failure. IMO, we should go towards less magic and more explicitness to make methods rememberable and the code easier to debug. Feel free to improve on expectMap, and rename it, but don't make it even less predictable.
|
|
||
| flatMap(λ) { | ||
| return transformResult(() => λ(this.value), Result.expect); | ||
| chain(λ) { |
There was a problem hiding this comment.
Not a big fan of the name tbh. Chain is traditionally an alias for bind/flatMap, and imthis might surprise our users. Why not just repurpose transform?
There was a problem hiding this comment.
hm I thought I had called this transform, and created the aliases chain, flatMap and bind
| flatMap: returnThis.unary, | ||
| replace: returnThis.unary, | ||
| property: returnThis.unary, | ||
| expectMap: returnThis.unary, |
|
|
||
| [['transform', 'map']].forEach(([alias, method]) => { | ||
| const decideWrapper = cond([ | ||
| [value => [null, undefined].includes(value), constant(Error())], |
There was a problem hiding this comment.
Null & undefined are OK values. Why would they be an error?
There was a problem hiding this comment.
I think I didn't invent this behavior, not sure, need to investigate
| [['transform', 'map']].forEach(([alias, method]) => { | ||
| const decideWrapper = cond([ | ||
| [value => [null, undefined].includes(value), constant(Error())], | ||
| [value => typeof value !== 'object', value => Ok(value)], |
There was a problem hiding this comment.
Dangerous and unnecessary
There was a problem hiding this comment.
ok, I think I also took this one from somewhere, need to investigate
| [value => isPromise(value), value => Result.Pending(value.then(Result.expect, Aborted))], | ||
| [value => value.isResultInstance, value => value], | ||
| [value => value.isOptionalInstance, value => value.match({ Some: Ok, None: Error })], | ||
| [constant(true), value => Optional.fromNullable(value).match({ Some: Ok, None: Error })] |
There was a problem hiding this comment.
You can't have nullables at that point so you can just use OK
| return this.value; | ||
| }, | ||
|
|
||
| map(λ) { |
There was a problem hiding this comment.
Additionally, I like to have something that is clear and predictable instead of something magical (which more often than not is unpredictable!)
So you think it's better to have one function which unwraps Optionals, one that unwraps Results, one that unwraps Promises instead of the decideWrapper ? i.e. mapOptional, mapPromise, mapResult, mapRaw, .. Yes it feels like magic, but one function which is interoperable with all those types is not necessarily unpredictable. |
As a matter of fact, I do! Nobody complains on arrays that they have to use No need for On the topic of
As I said, if you know how to make it more predictable and easier to use, feel free to change it in any way you see fit (including the name!), but don't change (*: |
This diff proposes to converge expectMap / flatMap / map into one function and creating aliases for them.