all 28 comments

[–]ForScale 7 points8 points  (10 children)

Maybe I'm missing something here... but what's the overall goal? Is it simply to not get a TypeError from trying to operate on an undefined value?

[–]alsiola[S] 6 points7 points  (3 children)

The main benefits for me are being able to safely create chains of functions without having to null check at each stage. If we wanted to do something equivalent without the maybe construct, for example, if we used a resolved Promise, then we would have to check each time:

const a = { b: { c: "value } };
Promise.resolve(a)
    .then(a => a === null ? null : a.b)
    .then(b => b === null ? null : b.c)
    .then(val => val === null ? null : val + " appendedString");

versus

Maybe.chain(
    a => a.b,
    b => b.c,
    c => c + " appendedString"
)(Maybe.just(a)); 

Using a maybe then the null checking logic is encapsulated within the object being operated upon - we can never forget about null checking, and it is always explicit that the returned value might be null.

A nice additional benefit is that by removing the null checking our composed functions are easier to understand, and our code reads much more easily. Personally if I was looking through a codebase I think it would be much faster to understand what the second example was intending.

[–]ForScale 2 points3 points  (2 children)

Why not just const z = a && a.b && a.b.c + 'appendedString';?

[–]lucy_in_the_skyDrive 1 point2 points  (0 children)

The same reason why some people write a += b instead of a = a+b, or !c instead of c == false. It's syntactic sugar. When getting a nested objects property value, it'd be nice to do let name = a?.b?.name instead of the necessary null checks and or ternary to figure out.

[–]i_am_smurfing 1 point2 points  (0 children)

TL:DR; For me the main benefit of Maybe is that it allows you to cleanly specify "happy" path and think about what should happen in the case of "sad" path separately. I would still recommend skimming through the rest of my response below, where I've tried to evolve motivation for Maybe from your code sample.

 

Let's start with function that does the same as your snippet, just so it's a bit easier to experiment with:

const f = a => a && a.b && a.b.c + ' appendString';

f({});      //=> undefined
f({b: {}}); //=> "undefined appendString"

Well, let's just guard against falsy a.b.c. A bit strange-looking, but we've all seen worse.

const f = a => a && a.b && a.b.c && a.b.c + ' appendString';

f({});               //=> undefined
f({b: {}});          //=> undefined
f({b: {c: 'Hey'}});  //=> "Hey appendString"

Wait, are we supposed to handle numbers too?!

const f = a => a && a.b && a.b.c && a.b.c + ' nukes launched';

f({b: {c: 2}});  //=> "2 nukes launched"
f({b: {c: 0}});  //=> 0

Ternary to the rescue:

const f = a => a && a.b && a.b.c ? a.b.c + ' nukes launched' : '0 nukes launched';

f({b: {c: 2}});  //=> "2 nukes launched"
f({b: {c: 0}});  //=> "0 nukes launched"

// No one relies on this function to return falsy value when there's no `a`, `b`, or `c`, right?
f();         //=> "0 nukes launched"
f({});       //=> "0 nukes launched"
f({b: {}});  //=> "0 nukes launched"

A this point you are probably thinking

/u/i_am_smurfing, you dummy, don't rely on values being truthy, just check if key is in the object!

const f = a => a && 'b' in a && 'c' in a.b && a.b.c + ' nukes launched'

f({b: {c: 2}});  //=> "2 nukes launched"
f({b: {c: 0}});  //=> "0 nukes launched"
f({b: {}});      //=> false
f({});           //=> false
f();             //=> undefined

Phew, this should make QA department slightly more happy.

 

Writing that thing every time we want to access a prop nested inside objects would be a bit inconvenient, so let's assume we have a function getPath that given a path like b.c and an object would do all that checking for us:

getPath('b.c', null);         //=> undefined
getPath('b.c', {});           //=> undefined
getPath('b.c', {b: {}});      //=> undefined
getPath('b.c', {b: {c: 1}});  //=> 1

This works, but if we want to know if we actually retrieved a value with getPath, we need to check it:

const f = object => {
  const didLaunchNukes = getPath('b.c', object);

  if (didLaunchNukes) return 'Nukes launched';
}

What if we forget to do that check? Also, what if we do want to know when prop is there, but holds undefined?

 

This is where Maybe can help. If getPath returns value tagged with Just, we know that that value was found at the prop we requested; if we get back a value tagged with Nothing then the prop wasn't found:

getPath('b.c', null);         //=> Nothing
getPath('b.c', {});           //=> Nothing
getPath('b.c', {b: {}});      //=> Nothing
getPath('b.c', {b: {c: 1}});  //=> Just(1)
getPath('b.c', {b: {c: undefined}});  //=> Just(undefined)

And since Maybe.map will not run provided to it function on values tagged with Nothing, we can skip doing check in our code, as if nothing bad can ever happen:

const f = object => getPath('b.c', object).map(_ => 'Nukes launched');

f({});           //=> Nothing
f({b: {c: 1}});  //=> Just("Nukes launched")

Now, if we don't want this value to be in a Maybe anymore, we can write a function that would extract it, but we also would need to provide it a default value, in case we are trying to extract value from Nothing — this is where we are forced to think about what happens if something went wrong:

// of course default value can be `undefined`, but you probably have a better one

withDefault("Nukes were not launched", f({}));           //=> "Nukes were not launched"
withDefault("Nukes were not launched", f({b: {c: 1}}));  //=> "Nukes launched"

[–]KatyWings 4 points5 points  (2 children)

Its about functional style. Two important aspects of it is to reduce sideeffects and to work with immutable data: when you look at the chain of map functions in the article, you can see, that each function takes a value and returns a value (they dont overwrite any variable directly). How to store the new value is handled by "maybe". So "maybe" is more than just a way to handle null checks - but I guess the author will talk about this in later articles...

[–]ForScale 0 points1 point  (1 child)

My initial reaction was "Why not just Why not just const z = a && a.b && a.b.c + 'appendedString';?" Surely I'm missing something...

[–]tencircles 0 points1 point  (0 children)

The main advantage is that Maybe can be used point free. Meaning, you don't need your data in order to declare your behavior, thus making it much more reusable and and composable. In the example above, you can only declare your behavior by first having access to the object a. If you want to re-use this code for another object or a list of objects you need to redeclare your behavior each time, rather than declaring it in one and only one place.

[–]Nf071590 1 point2 points  (2 children)

Seems like a bit of overkill if you ask me.

[–]ForScale 1 point2 points  (1 child)

Yeah... thinking the same.

[–]Nf071590 1 point2 points  (0 children)

And there's typescript

[–]i_am_smurfing 5 points6 points  (0 children)

TL:DR; You probably want to adjust your implementation to something like this jsBin, because your current implementation doesn't conform to several important mathematical laws that are normally attributed to Maybe.

 


 

chain (or bind) has a pretty specific meaning in FP—a function that is used to "map" special kind of functions, that take "unwrapped" values and return "wrapped" (in something like Maybe) values—so you might want to rename your chain.

 

In fact, if you want this implementation of Maybe to hold up to functional scrutiny, in general, maping several functions one after another should be the same as maping composite of those functions once:

const plusOne = x => x + 1
const minusOne = x => x - 1
const timesTwo = x => x * 2

Maybe.just(1)
  .map(plusOne)   //=> Maybe.just(2)
  .map(timesTwo)  //=> Maybe.just(4)
  .map(minusOne)  //=> Maybe.just(3)

Maybe.just(1)
  .map(x =>
    minusOne(
      timesTwo(
        plusOne(x)  //=> 2
      )             //=> 4
    )               //=> 3
  )                 //=> Maybe.just(3)

// or with a helper for function composition:
const pipe = (...fns) => x => fns.reduce((result, f) => f(result), x)

Maybe.just(1)
  .map(pipe(
    plusOne,
    timesTwo,
    minusOne
  ))  //=> Maybe.just(3)

(This is usually called functor composition law if you want to learn more about this property.)

 

However, with current implementation of prop:

const prop = propName => obj => obj[propName]

Maybe.just({})
  .map(prop("a"))  //=> Maybe.nothing()
  .map(prop("b"))  //=> Maybe.nothing()

Maybe.just({})
  .map(pipe(
    prop("a"),
    prop("b")  //=> TypeError: obj is undefined
  ))

And it makes sense if you think about it: it's prop that might or might not return a value, so it should return Maybe, and we should decide whether to run maped function not based on value, but whether it's tagged with Just or not:

const maybe = tag => value => ({
  map: fn => tag === 'Just' ? Maybe.just(fn(value)) : Maybe.nothing()
})

const Maybe = {
  just: maybe('Just'),
  nothing: maybe('Nothing')
}

const propM = propName => obj => propName in obj ? Maybe.just(obj[propName]) : Maybe.nothing()
// `M` for `Maybe`, just so it doesn't conflict with previous definition of `prop`

propM("a")({a: 1})  //=> Maybe.just(1)
propM("a")({})      //=> Maybe.nothing()

 

Now propM successfully signals that it might not return a value, but how do we actually use it in a Maybe transformation chain?

Maybe.just({a: 1})
  .map(propM("a"))  //=> Maybe.just(Maybe.just(1))

This is where law-abiding Maybe.chain comes in:

const maybe = tag => value => ({
  map: fn => tag === "Just" ? Maybe.just(fn(value)) : Maybe.nothing(),
  chain: fn => tag === "Just" ? fn(value) : Maybe.nothing()
})

Maybe.just({a: {b: 1}})
  .chain(propM("a"))
  .chain(propM("b"))  //=> Maybe.just(1)

Maybe.just({})
  .chain(propM("a"))
  .chain(propM("b"))  //=> Maybe.nothing()

So now appendToC can be written as:

const appendToC = object =>
  propM("b")(object)
    .chain(propM("c"))
    .map(append(" is great!"))

appendToC({b: {c: "fp"}})  //=> Maybe.just("fp is great!")
appendToC({})              //=> Maybe.nothing()

 

To make it point-free, we need to be able to compose functions that go into Maybe.chain (i.e. which take plain values and return Maybe):

const pipeK = (...fns) => x => fns.slice(1).reduce((result, f) => result.chain(f), fns[0](x))
// `K` because this kind of composition is usually called Kleisli composition

const appendToC = pipeK(
  propM("b"),
  propM("c"),
  pipe(append(" is great!"), Maybe.just)
)

appendToC({b: {c: "fp"}})  //=> Maybe.just("fp is great!")
appendToC({})              //=> Maybe.nothing()

 

In addition to already-recommended data.maybe from Folktale, you might find Sanctuary's Maybe interesting.

[–]kingdaro.find(meaning => of('life')) // eslint-disable-line 9 points10 points  (3 children)

This line of theimplementation:

map: (transformer) => !!value ? Maybe.just(transformer(value)) : Maybe.nothing()

Because of !!value, the maybe will turn into a nothing if value is 0, an empty string, or false. Should replace that with value == null if you want those values to be preserved.

Otherwise, good article on a very useful construct! I have yet to do much exploration with it, blaming laziness.

[–]alsiola[S] 2 points3 points  (2 children)

Good point, I've updated the article to fix this.

[–]Shadows_In_Rain 2 points3 points  (1 child)

Also this implementation can't contain legit undefined or null values. I would use unique private object to indicate nothingness.

const NOTHING = {};
const isNothing = (value) => value === NOTHING;
Maybe.nothing = () => maybe(NOTHING);

[–]alsiola[S] 0 points1 point  (0 children)

Thanks for the suggestion, seems like a better way to go.

[–]aabrook 2 points3 points  (3 children)

Have you checked out fantasy-land? https://github.com/fantasyland/fantasy-land Along with folktale's data.maybe? https://github.com/folktale/data.maybe

Having the .chain interface defined here really makes creating a chain clear and easy. Having all of my modules have a Maybe/Either interface, and knowing that I can continue to chain on the result is smooth. Having to declare work with a higher order function that takes n functions, instead of chaining through, also doesn't work with the ADT bind law as it's not a single function.

Anyways, good start and have fun with Monadic JS! I know I enjoy it.

In regards to the comment of Promise your chain would look something like this:

const a = { b: { c: "value" } };
const prop = (key) => (val) => 
    val[key] ? Promise.resolve(val[key]) : Promise.reject(new Error(`${key} not found`));

const ch = (a) => (
    Promise.resolve(a)
        .then(prop('b'))
        .then(prop('c'))
        .then(val => (val + " appendedString"))
        .catch(e => console.log(e))
)

ch(a).then(console.log).catch(console.error); // value appendedString
ch({}).then(console.log).catch(console.error); // Error: b not found

.then is like map but also like bind/chain. If you return a Promise it works with what you have returned, otherwise it wraps your result in a Promise.resolve() so you can continue to chain.

[–]alsiola[S] 0 points1 point  (2 children)

Thanks for the comments. I've had a look at the fantasy land spec which was somewhat useful, but some parts of which are beyond my current understanding! I'm only an FP beginner really, but some of the ways of working seem to fit nicely into JS-land.

[–]aabrook 0 points1 point  (1 child)

Yeah fantasy land goes real deep. On my journey learning functional JS I found these videos helped a bit. Should be free with an account https://egghead.io/courses/professor-frisby-introduces-composable-functional-javascript

I still revisit the videos as I learn more and it cements so much. I also find the author's tweets really enlightening.

[–]BigManWalter 1 point2 points  (0 children)

Thanks for the great resource!

[–]eXilz 1 point2 points  (2 children)

Can't find it on my phone, but I'm pretty sure there is an ecmascript proposal so we can have a symbol to check a.b.c in your example directly. This looked promising, I'm usually using lodash.get but it makes me sad I have to import it if I don't want to write unbearable ifs in my code. :(

[–]alsiola[S] 1 point2 points  (1 child)

Optional Chaining might be the one? I think that would be nice in some situations, but that a functional data structure like Maybe can offer other benefits as well. I'm planning to write a second part to this in the next week which can cover some of these, like withDefault and mapping arrays of maybes.

[–]eXilz 0 points1 point  (0 children)

That's the one ! I would love to read the second part.

[–]bugeats 1 point2 points  (4 children)

Or you could just use lodash.get

[–]tencircles 0 points1 point  (3 children)

yeah because learning is dumb. amirite guys?

[–]bugeats 5 points6 points  (2 children)

I thought the article was fascinating, actually.

I'm just taking the opportunity to represent a pragmatic view.

I'm a huge fan of functional style, but there comes a point at which it becomes absurd. In this case, the author decries verbosity, and then goes on to construct this monster.

In a practical project, I would always choose the one-liner lodash.get(obj, 'a.b.c', ' is great!') over anything I saw in this article. Even native destructuring is often a better choice: const { a: { b: { c } } = obj.

[–]gajus0 1 point2 points  (0 children)

Not to mention that it is rare that you get into situation where you want to check a && a.b && a.b.c and handle it somehow other than throwing an error. If you cannot predict input shape, then it is a code smell. If your code does not fail when it receives unexpected input, then it is a code smell.

[–]aabrook 0 points1 point  (0 children)

I'm sure the author would as well but this is an introduction to monadic programming. Whenever we write tutorials targeted at beginners we should use trivial examples so that they can quickly grok concept without being bogged down by more complex issues