DEV Community

Heiker
Heiker

Posted on • Updated on

Functional programming for your everyday javascript: Using a Maybe

Puedes leer la versión en español aquí.

Have you ever heard about monads and how great they are? Maybe you have but you still don't get it. Well... I'm not here to tell you what they are, I won't try to sell them to you either, what I will do is show you an example of how would it be if you use them in your javascripts.

We'll do something fun, lets solve a fairly trivial problem in an unnecessary complicated way.

Suppose that we have a dictionary stored in a json file or a plain js object.

{
    "accident": ["An unexpected, unfortunate mishap, failure or loss with the potential for harming human life, property or the environment.", "An event that happens suddenly or by chance without an apparent cause."], 
    "accumulator": ["A rechargeable device for storing electrical energy in the form of chemical energy, consisting of one or more separate secondary cells.\\n(Source: CED)"],
    "acid": ["A compound capable of transferring a hydrogen ion in solution.", "Being harsh or corrosive in tone.", "Having an acid, sharp or tangy taste.", "A powerful hallucinogenic drug manufactured from lysergic acid.", "Having a pH less than 7, or being sour, or having the strength to neutralize  alkalis, or turning a litmus paper red."],

     // ... moar words and meanings

    "Paris": ["The capital and largest city of France."]
  }
Enter fullscreen mode Exit fullscreen mode

We want a form that lets a user search one of this words and then shows the meaning(s). This is simple, right? What could possibly go wrong?

Because everyone loves HTML we'll start with that.

<form id="search_form">
  <label for="search_input">Search a word</label>
  <input id="search_input" type="text">
  <button type="submit">Submit</button>
</form>

<div id="result"></div>
Enter fullscreen mode Exit fullscreen mode

In the first version we will just try get one those values based on the user input.

// main.js

// magically retrieve the data from a file or whatever
const entries = data();

function format(results) {
  return results.join('<br>'); // I regret nothing
}

window.search_form.addEventListener('submit', function(ev) {
  ev.preventDefault();
  let input = ev.target[0];
  window.result.innerHTML = format(entries[input.value]);
});
Enter fullscreen mode Exit fullscreen mode

Naturally the first thing we try to search is "acid." And behold here are the results.

A compound capable of transferring a hydrogen ion in solution.
Being harsh or corrosive in tone.
Having an acid, sharp or tangy taste.
A powerful hallucinogenic drug manufactured from lysergic acid.
Having a pH less than 7, or being sour, or having the strength to neutralize alkalis, or turning a litmus paper red.

Now we search for "paris", I'm sure it's there. What did we get? Nothing. Not exactly nothing, we got.

TypeError: results is undefined

We also got an unpredictable submit button that sometime works and sometimes doesn't. So what do we want? What do we really, really want? Safety, objects that don't crash our application, we want reliable objects.

What we will do is implement containers that let us describe the flow of execution without worrying about the value they hold. Sounds good, right? Let me show you what I mean with a little javascript. Try this.

const is_even = num => num % 2 === 0;

const odd_arr = [1,3,4,5].filter(is_even).map(val => val.toString());
const empty_arr = [].filter(is_even).map(val => val.toString());

console.log({odd_arr, empty_arr});
Enter fullscreen mode Exit fullscreen mode

Did it throw an exception on the empty array? (if it did let me know). Isn't that nice? Doesn't it feel all warm and fuzzy knowing that the array methods would do the right thing even if there isn't anything to work with? That is what we want.

You might be wondering couldn't we just write a few if statements and be done with it? Well... yeah, but where is the fun in that? We all know that chaining functions is cool, and we are fans of functional programming, we do what every functional programming savvy does: hide things under a function.

So we are going to hide an if statement (or maybe a couple), if the value we evaluate is undefined-ish we return a wrapper that will know how to behave no matter what happens.

// maybe.js
// (I would like to apologize for the many `thing`s you'll see)

function Maybe(the_thing) {
  if(the_thing === null 
     || the_thing === undefined 
     || the_thing.is_nothing
  ) {
    return Nothing();
  }

  // I don't want nested Maybes
  if(the_thing.is_just) {
    return the_thing;
  }

  return Just(the_thing);
}
Enter fullscreen mode Exit fullscreen mode

This wrappers are not going to be your standard by the book Maybe you see in a proper functional programming language. We will cheat a little in the name of convenience and side effects. Also their methods will be named after the methods in the Option type you find in Rust (I like those names better). Here is where the magic happens.

// maybe.js

// I lied, there will be a lot of cheating and `fun`s.

function Just(thing) {
  return {
    map: fun => Maybe(fun(thing)),
    and_then: fun => fun(thing),
    or_else: () => Maybe(thing),
    tap: fun => (fun(thing), Maybe(thing)),
    unwrap_or: () => thing,

    filter: predicate_fun => 
      predicate_fun(thing) 
        ? Maybe(thing) 
        : Nothing(),

    is_just: true,
    is_nothing: false,
    inspect: () => `Just(${thing})`,
  };
}

function Nothing() {
  return {
    map: Nothing,
    and_then: Nothing,
    or_else: fun => fun(),
    tap: Nothing,
    unwrap_or: arg => arg,

    filter: Nothing,

    is_just: false,
    is_nothing: true,
    inspect: () => `Nothing`,
  };
}
Enter fullscreen mode Exit fullscreen mode

What is the purpose of these methods?

  • map: Applies the function fun to the_thing and wraps it again on a Maybe to keep the party going... I mean to keep the shape of the object, so you can keep chaining functions.
  • and_then: This is mostly an escape hatch. Apply the function fun and let fate decide.
  • or_else: It is the else to your map and and_then. The other path. The "what if is not there?"
  • tap: These one is there just for the side effects. If you see it then it's probably affecting something outside of it's scope (or maybe is just the perfect place to put a console.log).
  • filter: It "lets you go through" if the predicate function returns something truthy.
  • unwrap_or: This is how you get the_thing out. You'll want this when you're done chaining methods and you're ready to get back to the imperative world.

Lets go back to our form and see it in action. We'll make a function search that may o may not retrieve a match to the user's query. If it does we'll chain other functions that will be executed in a "safe context."

// main.js

const search = (data, input) => Maybe(data[input]);

const search_word = word => search(entries, word)
  .map(format)
  .unwrap_or('word not found');
Enter fullscreen mode Exit fullscreen mode

And now we replace our unholy old way with the new safe(r) function.

 window.search_form.addEventListener('submit', function(ev) {
   ev.preventDefault();
   let input = ev.target[0];
-  window.result.innerHTML = format(entries[input.value]);
+  window.result.innerHTML = search_word(input.value);
 });
Enter fullscreen mode Exit fullscreen mode

Now we test. Search for "accident."

An unexpected, unfortunate mishap, failure or loss with the potential for harming human life, property or the environment.
An event that happens suddenly or by chance without an apparent cause.

Now Paris. Search for "paris."

word not found

It didn't freeze the button, that's good. But I know Paris is there. If you check you'll see that is "Paris." We'll just capitalize the user input so they don't have to. First we'll try to search the exact input, if that fails we'll try the capitalize way.

// main.js

function create_search(data, exact) {
  return input => {
    const word = exact ? input : capitalize(input);
    return Maybe(data[word]);
  }
}

function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}
Enter fullscreen mode Exit fullscreen mode

Change the search function.

- const search = (data, input) => Maybe(data[input]);
+ const search = create_search(entries, true);
+ const search_name = create_search(entries, false);
-
- const search_word = word => search(entries, word)
+ const search_word = word => search(word)
+   .or_else(() => search_name(word))
    .map(format)
    .unwrap_or('word not found');
Enter fullscreen mode Exit fullscreen mode

Very nice. This what we got so far in main.js if you wanna see the whole picture.

// main.js

const entries = data();

function create_search(data, exact) {
  return input => {
    const word = exact ? input : capitalize(input);
    return Maybe(data[word]);
  }
}

function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

function format(results) {
  return results.join('<br>');
}

const search = create_search(entries, true);
const search_name = create_search(entries, false);

const search_word = word => search(word)
  .or_else(() => search_name(word))
  .map(format)
  .unwrap_or('word not found');

window.search_form.addEventListener('submit', function(ev) {
  ev.preventDefault();
  let input = ev.target[0];
  window.result.innerHTML = search_word(input.value);
});
Enter fullscreen mode Exit fullscreen mode

But is that all we want in life? No, of course not, we want love but since javascript can't give us that we'll settle for a little "suggest word" feature. I want to search "accu" and have a confirm dialog telling me "Did you mean accumulator?"

We'll need help with this one, we'll bring a dependency, one that can perform a fuzzy search on the entries: fuzzy-search. So we add the following.

// main.js

import FuzzySearch from 'https://unpkg.com/fuzzy-search@3.0.1/src/FuzzySearch.js';

const fzf = new FuzzySearch(
  Object.keys(entries),
  [],
  {caseSensitive: false, sort: true}
);
Enter fullscreen mode Exit fullscreen mode

But again we can't perform a safe operation 'cause the moment we try to get a match from an empty array the whole thing will fall apart. So what do we do? We hide things under a function.

// main.js

function suggest(word) {
  const matches = fzf.search(word);
  return Maybe(matches[0]);
}
Enter fullscreen mode Exit fullscreen mode

Fuzzy search is ready, now lets throw in a super awesome confirm dialog. You'll love it.

// main.js

function confirm_word(value) {
  if(value && confirm(`Did you mean ${value}`)) {
    return value;
  }
}
Enter fullscreen mode Exit fullscreen mode

We combine the new functions with our search.

// main.js

const suggest_word = value => () => suggest(value)
  .map(confirm_word)
  .map(search);
Enter fullscreen mode Exit fullscreen mode

Add the feature to search_word.

 const search_word = word => search(word)
   .or_else(() => search_name(word))
+  .or_else(suggest_word(word))
   .map(format)
   .unwrap_or('word not found');
Enter fullscreen mode Exit fullscreen mode

That works! But lets say we are allergic to if statements and not to mention that it's just rude to return undefined from a function. We can do better.

 function confirm_word(value) {
-  if(value && confirm(`Did you mean ${value}`)) {
-    return value;
-  }
+  return confirm(`Did you mean ${value}`);
 }
Enter fullscreen mode Exit fullscreen mode
 const suggest_word = value => () => suggest(value)
-  .map(confirm_word)
+  .filter(confirm_word)
   .map(search);
Enter fullscreen mode Exit fullscreen mode

Something bugs me. I search "accu", the dialog pops in, I confirm the suggestion and the results appears. But "accu" it's still there in the input, it's awkward. Lets update the input with the right word.

const update_input = val => window.search_form[0].value = val;
Enter fullscreen mode Exit fullscreen mode
 const suggest_word = value => () => suggest(value)
   .filter(confirm_word)
+  .tap(update_input)
   .map(search);
Enter fullscreen mode Exit fullscreen mode

Want to see it in action? There you go.

Bonus track

Warning: The main point of the post (which is me showing that codepen example) was already accomplished. What follows is a strange experiment to see if I could make that Maybe function support asynchronous operations. If you are tired just skip everything and check out the last example code.

Now you might be saying: this is cute and all but in the "real world" we make http requests, query a database, make all sorts of asynchronous stuff, can this still be useful in that context?

I hear you. Our current implementation just supports normal blocking tasks. You would have to break the chain of Maybes the moment a Promise shows up.

But what if... listen... we make a promise aware Just. We can do that, an AsyncJust? JustAsync? Oh, that's awful.

If you don't know, a Promise is a data type that javascript uses to coordinate future events. To do so it uses a method called then that takes a callback (it also has catch for when things go wrong) So if we hijack what goes into that then then we can keep our nice Maybe interface.

How good are you following a bunch of callbacks?

Here I go. Let me show you the Future.

// Don't judge me. 

function Future(promise_thing) { 
  return {
    map: fun => Future(promise_thing.then(map_future(fun))),
    and_then: fun => Future(promise_thing.then(map_future(fun))),
    or_else: fun => Future(promise_thing.catch(fun)),
    tap: fun => Future(promise_thing.then(val => (fun(val), val))),
    unwrap_or: arg => promise_thing.catch(val => arg),

    filter: fun => Future(promise_thing.then(filter_future(fun))), 

    is_just: false,
    is_nothing: false,
    is_future: true,
    inspect: () => `<Promise>`
  };
}
Enter fullscreen mode Exit fullscreen mode

If we remove the noise maybe we could understand better.

// In it's very core is callbacks all the way.

{
  map: fun => promise.then(fun),
  and_then: fun => promise.then(fun),
  or_else: fun => promise.catch(fun),
  tap: fun => promise.then(val => (fun(val), val))),
  unwrap_or: arg => promise.catch(val => arg),

  filter: fun => promise.then(fun), 
}
Enter fullscreen mode Exit fullscreen mode
  • map/and_then: these do the same thing because you can't get out of a Promise.
  • or_else: puts your callback in the catch method to mimic an else behavior.
  • tap: uses then to peek at the value. Since this is for side effects we return the value again.
  • unwrap_or: It will return the promise so you can use await. If everything goes well the original value of the Promise will be returned when you await, else the provided argument will be returned. Either way the promise doesn't throw an error because the Future attached the catch method to it.
  • filter: these one is a special kind of map that's why filter_future exists.
  • Almost all these methods return a new Future 'cause promise.then returns a new Promise.

What makes the Future weird is what happens inside map. Remember map_future?

function map_future(fun) { // `fun` is the user's callback
  return val => {
    /* Evaluate the original value */
    let promise_content = val;

    // It needs to decide if the value of the Promise
    // can be trusted
    if(Maybe(promise_content).is_nothing) {
      Promise.reject();
      return;
    }

    // If it is a Just then unwrap it.
    if(promise_content.is_just) {
      promise_content = val.unwrap_or();
    }

    /* Evaluate the return value of the user's callback */

    // Use Maybe because I have trust issues.
    // For the javascript world is undefined and full of errors.
    const result = Maybe(fun(promise_content));

    if(result.is_just) {
      // If it gets here it's all good.
      return result.unwrap_or();
    }

    // at this point i should check if result is a Future
    // if that happens you are using them in a wrong way
    // so for now I don't do it 

    // There is something seriously wrong.
    return Promise.reject();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now filter_future.

function filter_future(predicate_fun) { // the user's function
  return val => {
    const result = predicate_fun(val);

    // Did you just returned a `Promise`?
    if(result.then) {
      // You did! That's why you can't have nice things.

      // peek inside the user's promise.
      const return_result = the_real_result => the_real_result 
        ? val
        : Promise.reject();

      // keep the promise chain alive.
      return result.then(return_result);
    }

    return result ? val : Promise.reject();
  }
}
Enter fullscreen mode Exit fullscreen mode

There is one last thing I would like to do and that is create a helper function to convert a regular value into a Future.

Future.from_val = function(val) {
  return Future(Promise.resolve(val));
}
Enter fullscreen mode Exit fullscreen mode

All we have to do now to support a Future in a Maybe is this.

 function Maybe(the_thing) {
   if(the_thing === null 
     || the_thing === undefined 
     || the_thing.is_nothing
   ) {
     return Nothing();
   }
-
-  if(the_thing.is_just) {
+  if(the_thing.is_future || the_thing.is_just) {
     return the_thing;
    }

    return Just(the_thing);
 }
Enter fullscreen mode Exit fullscreen mode

But the million dollar question remains. Does it actually work?

I have CLI version of this. And here is the same codepen example with some tweaks: I added the Future related functions, the confirm dialog is actually a dialog (this one) and the event listener is now an async function that can await the result.

Bonus bonus edit

That is how it looks like when we cheat. If we didn't cheat it would be like this.

Other resources


Thank you for reading. If you find this article useful and want to support my efforts, buy me a coffee ☕.

buy me a coffee

Top comments (4)

Collapse
 
aminnairi profile image
Amin

I just got into Haskell and recently into Rust. This just reminds me some common patterns used in both of these languages and seeing them here is just awesome! I can't wait to play with those in JavaScript.

Collapse
 
artydev profile image
artydev

Awesome, thank you very much

Collapse
 
juanrodriguezarc profile image
Juan Rodríguez

Excelent article!

Collapse
 
rolandcsibrei profile image
Roland Csibrei

How cool is this!