Skip to content

Refactor expectMap/flatMap/map into one function#13

Open
ghost wants to merge 8 commits into
masterfrom
refactor/applicators
Open

Refactor expectMap/flatMap/map into one function#13
ghost wants to merge 8 commits into
masterfrom
refactor/applicators

Conversation

@ghost

@ghost ghost commented Dec 3, 2019

Copy link
Copy Markdown

This diff proposes to converge expectMap / flatMap / map into one function and creating aliases for them.

Kevin Ley added 8 commits November 3, 2019 03:56
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
@ghost
ghost requested a review from floriansimon1 December 3, 2019 10:43
Comment thread source/result.js
return this.value;
},

map(λ) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, I like to have something that is clear and predictable instead of something magical (which more often than not is unpredictable!)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I thought map is redundant in monads. flatMap already does that

@floriansimon1 floriansimon1 Dec 3, 2019

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread source/result.js
return transformResult(() => λ(this.value), Ok);
},

flatMap(λ) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's why I chose to have aliases

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flatMap is not supposed to do any trick, and the management of Promises is already more than it should do

Comment thread source/result.js
flatMap(λ) {
return transformResult(() => λ(this.value), Result.expect);
chain(λ) {
try {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

@floriansimon1 floriansimon1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

https://xkcd.com/927/

Comment thread source/result.js Outdated

flatMap(λ) {
return transformResult(() => λ(this.value), Result.expect);
chain(λ) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm I thought I had called this transform, and created the aliases chain, flatMap and bind

Comment thread source/result.js
flatMap: returnThis.unary,
replace: returnThis.unary,
property: returnThis.unary,
expectMap: returnThis.unary,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good riddance!

Comment thread source/result.js Outdated

[['transform', 'map']].forEach(([alias, method]) => {
const decideWrapper = cond([
[value => [null, undefined].includes(value), constant(Error())],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Null & undefined are OK values. Why would they be an error?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I didn't invent this behavior, not sure, need to investigate

Comment thread source/result.js Outdated
[['transform', 'map']].forEach(([alias, method]) => {
const decideWrapper = cond([
[value => [null, undefined].includes(value), constant(Error())],
[value => typeof value !== 'object', value => Ok(value)],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dangerous and unnecessary

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I think I also took this one from somewhere, need to investigate

Comment thread source/result.js Outdated
[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 })]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't have nullables at that point so you can just use OK

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right ..

Comment thread source/result.js
return this.value;
},

map(λ) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, I like to have something that is clear and predictable instead of something magical (which more often than not is unpredictable!)

@ghost

ghost commented Dec 3, 2019

Copy link
Copy Markdown
Author

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.

https://xkcd.com/927/

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.

@floriansimon1

floriansimon1 commented Dec 3, 2019

Copy link
Copy Markdown
Collaborator

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, ..

As a matter of fact, I do! Nobody complains on arrays that they have to use flatMap instead of map automatically flattening arrays (worse, in the case of arrays it would actually be a detriment!). It also helps when writing typings. It also gives more info at a glance about your code(*). Sure, it's error-prone in Javascript because you can mix them up, but none of that would occur if Result had types.... Which I unfortunately haven't written yet.

No need for mapResult. That's flatMap. mapRaw is just plain map. I'm still currently for abstracting away promises, because it's mostly predictable, but I might change my mind if I ever face inconsistencies.

On the topic of mapOptional: an absent value is not necessarily an Error. I don't think it's unreasonable to have Result<Optional<a>>, especially when writing more generic code. You can have explicit unwrapping but you need an escape hatch if a Result<Optional<a>> is what you need.

Yes it feels like magic, but one function which is interoperable with all those types is not necessarily unpredictable.

expectMap (which is more or less what you're trying to replace) certainly is, and your auto-erroring of null or undefined, or handling of Optionals certainly verge on the unpredictable side. Every time I use expect or expectMap, I have to look at the implementation to know what I will get out, which is to me a sign of bad design on my part.

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 map or flatMap.

(*: Ok(userId).optionalMap(getUser) conveys more info about getUser than Ok(userId).chain(getUser): you know getUser can fail to get a user)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant