Maybe monad: A practical Example
Vagelis Papadogiannakis
Source Development

papas_source


Basic Idea from James Sinclair
https://jrsinclair.com/articles/2016/marvellously-mysterious-javascript-maybe-monad/

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?
The Maybe monad - Practical Guide
By papas_source
The Maybe monad - Practical Guide
A Talk for the May 2019 meeting of our local DevStaff community https://www.meetup.com/devstaff/
- 221