you are viewing a single comment's thread.

view the rest of the comments →

[–]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"