Maybe monad: A practical Example

Vagelis Papadogiannakis

Source Development

papas_source

Modifications by me

Testimonials and Idea

Vagelis Papadogiannakis

Source Development

papas_source

Disclaimer: This presentation has nothing to do with functional programming (FP). I ruin and shamelesly step on allmost all the FP rules, I use the monad word to my liking, and I rename core functions to what looks more familiar to me. The core concepts are there, but be warned, this is in no way a FP walkthrough, let alone a complete monad implementation.

What we will be building

We have a website. We want to display a banner on it, based on the location of the visitor

Vagelis Papadogiannakis

Source Development

papas_source

Banner Here

The data

The user object, that comes from an ajax call, is structured like this

Vagelis Papadogiannakis

Source Development

papas_source


  {
     "name": "John Doe",
     "details": {
        "gender": "male",
        "address": {
           "country": "GR",
           "city": "Athens",
           "street": "Papandreou",
           "nr": "3"
        }
     }
  }

We will be using the country to determine the banner

The other data

And then we have an object that holds the banners

Vagelis Papadogiannakis

Source Development

papas_source

const banners = {
   EN: '/images/frt2tk_cndm_off.jpg',
   EL: '/images/greek_eikona.jpg',
   ...
   ...
   DE: '/images/img_69_666.jpg',
}

So, it shouldn'd be that hard!

const country = user.details.address.country;
$('#banner').attr('src', banners[country]);
//
// FINITO!

The problem

Sometimes the user is not logged in

Vagelis Papadogiannakis

Source Development

papas_source

user = {};

Sometimes the server takes too long to reply...

user = null;

Sometimes the user has not provided address details...

user = {
     "name": "John Doe",
     "details": {}
}

Sometimes the user is from a country we don't have a banner for...

user.details.address.country = "vi";

 

And because we have a junior HTML guru

who messes a lot with the DOM

(yes, it happens a lot)

 

even the

 

<img id="banner">

 

may not be available

by the time we want to update the banner!

 

POJ (plain Old Javascript)

Vagelis Papadogiannakis

Source Development

papas_source

let country = null;
let banner = null;

if ( user != null && 
   user.details && 
   Object.keys(user.details).length > 0 && 
   user.details.address &&
   Object.keys(user.details.address).length > 0 && 
   user.details.address.country ){
      country = user.details.address.country;
}

if ( country != null ) {
   if (banners[country]) {
      banner = banners[country];
   }

   if ( banner === null ) {
      banner = '/default/banner.jpg';
   }

   if (document.getElementById('banner') ) {
      document.getElementById('banner').src = banner;
   }
}

// we are done!!!!

GET ME OUTTA HERE!

Vagelis Papadogiannakis

Source Development

papas_source

We need a way to deal with

  • empty objects
  • null values
  • inexistent DOM elements
  • default values

 

Help!

How about we wrap our values in something that will

take care of all this sh*t automatically for us?

Enter the monad

Vagelis Papadogiannakis

Source Development

papas_source

Lets start with a simple tapper... A container you say?

Well... I'll still call it a tapper

const Maybe = function(val) {
    this.__value = val;
};

// I don't want to type the `new` keyword!
Maybe.of = function(val) {
    return new Maybe(val);
};


const one = Maybe.of(1)
// Maybe {__value: 1}

Into the maybe monad

Vagelis Papadogiannakis

Source Development

papas_source

We need a way to protect us from nulls and such. Let's incorporate a simple check

Maybe.prototype.isNothing = function() {
   return (this.__value === null || this.__value === undefined);
};

And we also need a way to deal with this value; pass it to a function for example... But take care of the nasty non-values

Maybe.prototype.map = function(callable) {
   if (this.isNothing()) {
      return Maybe.of(null);
   }
   return Maybe.of(callable(this.__value));
};
// we created a function that takes a function
// argument (callable) and if our value is OK, it calls
// this function with the argument and returns the result

And that's all!

Vagelis Papadogiannakis

Source Development

papas_source

Let's see what we've accomplished...

const prop = function(prop) {
   return function(obj) {
      return obj[prop];
   };
};

const maybe_country = Maybe.of(user)
   .map(prop('details'))
   .map(prop('address'))
   .map(prop('country'));

const maybe_el = Maybe.of(document.getElementById('banner'));
const country = maybe_country.__value, el = maybe_el.__value;

if (country && el) {
   const banner = banners[country] || '/fallback/banner.jpg';
   el.src = banner;
}

Our code is still a mess...

Vagelis Papadogiannakis

Source Development

papas_source

We can do better than that!

const country_banner = function(country) {
   return Maybe.of(banners[country]);
};

const get_banner = function(user) {
   return Maybe.of(user)
      .map(prop('details'))
      .map(prop('address'))
      .map(prop('country'))
      .map(country_banner);
}

A `Maybe`

inside

a `Maybe`

Our `map` revisited

Vagelis Papadogiannakis

Source Development

papas_source

Who needs inception?

That's the real

thing, baby!

Maybe.prototype.map = function(f) {
   if (this.isNothing()) {
      return Maybe.of(null);
   }
   var res = f(this.__value);

   return (res instanceof Maybe) ?
      res : 
      Maybe.of(res);
};

And the fallback, Sir?

Vagelis Papadogiannakis

Source Development

papas_source

What happens if no banner matches our country or when there is no data for the user to determine his/hers country?

Lets fallback and enjoy!

Maybe.prototype.orElse = function(fallback) {
    if (this.isNothing()) {
        return Maybe.of(fallback);
    }
    return this;
};
const get_banner = function(user) {
   return Maybe.of(user)
      .map(prop('details'))
      .map(prop('address'))
      .map(prop('country'))
      .map(country_banner)
      .orElse('/default/banner.jpg');
};

Grab the DOM element and...

Vagelis Papadogiannakis

Source Development

papas_source

We should now get the DOM element. If it is there.

const get_element = function(selector) {
   return Maybe.of(
      document.querySelector(selector)
   );
};

const maybe_el = get_element('#banner');
const maybe_banner = get_banner(user);

A Maybe and a Maybe

Vagelis Papadogiannakis

Source Development

papas_source

Let's combine them! We could write a function like this...

function apply_banner(banner, element) {
    element.__value.src = banner.__value;
}

But what happens when either one is null? We need to be smarter than that... We will write the function as if no Maybes were involved, and we'll figure it out later... But since we'll probably add another function to the Maybe prototype, lets make a function that will return a function 

const apply_banner_to_element = function(banner) {
   return function(element){
      element.src = banner;
      return element;
   };
};

Pretty simple now

Vagelis Papadogiannakis

Source Development

papas_source

It should be pretty easy now to understand this

Maybe.prototype.with = function(maybe) {
   return maybe.map(this.__value);
};
get_banner(user)
   .map(apply_banner_to_element)
   .with(get_element('#banner'));

And our final code will be

Lets sum it up...

Vagelis Papadogiannakis

Source Development

papas_source

const country_banner = function(country) {
   return Maybe.of(banners[country]);
};

const get_banner = function(user) {
   return Maybe.of(user)
      .map(prop('details')).map(prop('address'))
      .map(prop('country')).map(country_banner);
};

const get_element = function(selector) {
   return Maybe.of(document.querySelector(selector));
};

const apply_banner_to_element = function(banner) {
   return function(element){
      element.src = banner;
      return element;
   };
};

get_banner(user)
   .orElse('default/banner.png')
   .map(apply_banner_to_element)
   .with(get_element('#banner'));

And your Maybe, maybe?

Vagelis Papadogiannakis

Source Development

papas_source

var Maybe = function(val) {
   this.__value = val;
};

Maybe.of = function(val) {
   return new Maybe(val);
};

Maybe.prototype.isNothing = function() {
   return (this.__value === null || this.__value === undefined);
};

Maybe.prototype.map = function(f) {
   if (this.isNothing()) {
      return Maybe.of(null);
   }
   var res = f(this.__value);
   return (res instanceof Maybe) ? res : Maybe.of(res);
};

Maybe.prototype.orElse = function(fallback) {
   return this.isNothing() ? Maybe.of(fallback) : this;
};

Maybe.prototype.with = function(maybe) {
   return maybe.map(this.__value);
};

ANY QUESTIONS?

Made with Slides.com