DEV Community

Heiker
Heiker

Posted on • Edited on • Originally published at vonheikemen.github.io

Tagged unions and Fantasy Land

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

Let's do something fun, let's explore one branch of the Fantasy Land specification using tagged unions. In order to keep this as short as possible I'll mostly focus on how things work and leave out a lot of details. So, what we'll do is create a data structure and see if we can follow the rules on the specification.

Tagged Unions

Also known as variants, is a data structure that can represent different states of a single type. At any given time it can only be in one of those states. Other important features include the ability to carry information about themselves as well as an extra "payload" that can hold anything.

It sounds cool until we realize we don't have those things in javascript. If we want to use them we'll have to recreate them. Fortunately for us we don't need a bulletproof implementation. We just need to deal with a couple of things, the type of the variant and the payload they should carry. We can handle that.

function Union(types) {
  const target = {};

  for(const type of types) {
    target[type] = (data) => ({ type, data });
  }

  return target;
}
Enter fullscreen mode Exit fullscreen mode

What do we have here? You can think of Union as a factory of constructor functions. It takes a list of variants and for each it will create a constructor. It looks better in an example. Let's say we want to model the states of a task, using Union we could create this.

const Status = Union(['Success', 'Failed', 'Pending']);
Enter fullscreen mode Exit fullscreen mode

Now we can create our Status variants.

Status.Success({ some: 'stuff' });
// { "type": "Success", "data": { "some": "stuff" } }
Enter fullscreen mode Exit fullscreen mode

As you can see here we have a function that returns a plain object. In this object we have a type key where we store the name of our variant. The data key will hold anything we can think of. You might think that storing just the name of the variant isn't enough, because it may cause collisions with other variants of different types and you would be right. Since we are only going to create one data type this is not an issue for us.

If you find this pattern useful and want to use it, you'll need something reliable, consider using a library like tagmeme or daggy or something else.

Fantasy Land

The github description says the following.

Specification for interoperability of common algebraic structures in JavaScript

Algebraic structures? What? I know. The wikipedia definition for that doesn't help much either. Best I can offer is a vague sentence that leaves you with the least amount of questions, here I go: A set of values that have some operations associated with them that follow certain rules.

In our case, you can think the variants as our "set of values" and the functions that we create will be the "operations," as far as the rules goes we follow the Fantasy Land specification.

The Link

So, we know about tagged unions and we have a vague idea about this Fantasy Land thing but know the question remains, how do we connect those two? The answer is pattern matching. Those familiar with the term also know that we don't have that in javascript. Sadly, in this case we can only mimic certain features.

How do we start? Let's just describe what we need. We need to evaluate a variant, be able to determine which type we have and execute a block of code. We already have the type key which is a String, with that we could just use a switch/case.

switch(status.type) {
  case 'Success':
    // Everything went well
    break;

  case 'Failed':
    // Something went wrong
    break;

  case 'Pending':
    // Waiting...
    break;

  default:
    // Should never happen
    break;
}
Enter fullscreen mode Exit fullscreen mode

This actually gets pretty close to what we want but there is a problem, it doesn't return anything. We want to do the same this switch/case does but inside an expression, something that yields a result. To recreate this behavior in the way we want we'll use objects and functions.

function match(value, patterns) {
  const { type = null } = value || {};
  const _match = patterns[type];

  if (typeof _match == 'function') {
    return _match(value.data);
  } else if (typeof patterns._ == 'function') {
    return patterns._();
  }

  return null;
}
Enter fullscreen mode Exit fullscreen mode

Once again we take advantage of the fact that type is a String and use it to "choose" the pattern that we want. This time around our patterns are inside an object. Now, each "pattern" will be associated with a method on the patterns object and our function match will return what the chosen pattern returns. If it can't find the pattern it will try to call a method with the name _, this will mimic the default keyword on the switch/case and if that fails it just returns null. With this we can have the behavior we want.

match(status, {
  Success: ({ some }) => `Some: ${some}`,
  Failed:  () => 'Oops something went wrong',
  Pending: () => 'Wait for it',
  _:       () => 'AAAAHHHH'
});
// "Some: stuff"
Enter fullscreen mode Exit fullscreen mode

With this function at our disposal we can now move on.

The Data Structure

This is the part where we create the thing we're going to work with. We are going to model a fairly popular concept, an action that might fail. To do this we'll create a union with two variants Ok and Err, we will call it Result. The idea is simple, Ok will represent a success and we'll use it to carry the "expected" value, all our operations will be based on this variant. On the other hand if we get a variant of type Err all we want to do is propagate the error, this means that we'll ignore any kind of transformation on this variant.

const Result = Union(['Ok', 'Err']);
Enter fullscreen mode Exit fullscreen mode

The Operations

Before we move on let's just do one more thing, let's create a match function specific to our data type.

Result.match = function(err, ok, data) {
  return match(data, {Ok: ok, Err: err});
};
Enter fullscreen mode Exit fullscreen mode

Okay, now everything is in place. So like I said before, we will focus on just one branch of the Fantasy Land specification and that will be the one that goes from Functor to Monad. For each operation we will implement a static method in our Result object and I'll try to explain how does it work and why is useful.

Logic dictates that we start with Functor but we are going to take another road.

Chain

The chain operation lets us interact with the value that's inside our structure and apply a transformation. Sounds easy, right? We do that all the time, but this time we have rules. I present to you the first law of the day.

  • Associativity
Val.chain(Fx).chain(Gx);
// is equivalent to
Val.chain(v => Fx(v).chain(Gx));
Enter fullscreen mode Exit fullscreen mode

Notice that the comment says "equivalent to" although in most cases it should have identical results, this doesn't necessarily means that it should be read like equality, it is more like "it has the same effect as."

This law is about the order of the operations. In the first statement notice that it reads like a sequence, it goes one after the other. In the second statement it's like one operation wraps around the other. And this part is interesting, Fx(value).chain(Gx). That second chain comes directly from Fx. We can tell that Fx and Gx are functions that return a data type that also follows this law.

Let's see this in practice with another data type everyone is familiar with, arrays. It turns out that arrays follow this law (sorta). I know there is no chain in the Array prototype but there is a flatMap which behaves just like it.

const to_uppercase = (str) => str.toUpperCase();
const exclaim      = (str) => str + '!!';

const Val = ['hello'];

const Uppercase = (str) => [to_uppercase(str)];
const Exclaim   = (str) => [exclaim(str)];

const one = Val.flatMap(Uppercase).flatMap(Exclaim);
const two = Val.flatMap(v => Uppercase(v).flatMap(Exclaim));

one.length === two.length;
// true

one[0] === two[0];
// true
Enter fullscreen mode Exit fullscreen mode

So flatMap let us interact with the String inside the array and transform it using a function and it didn't mattered that the second flatMap was inside or outside of the first, we got the same result.

Now let's do the same with our data type. Our implementation will be a static method (just for fun), so our examples will look a tiny bit different. This is how we do it.

Result.chain = Result.match.bind(null, Result.Err);
Enter fullscreen mode Exit fullscreen mode

Thanks to the power of convenience Result.match has all the logic we need, the only thing we need to do is provide a value for the err parameter and just like that we achieve the effect we want. So Result.chain is a function that expects the ok and the data parameters. If the variant is of type err the error will just be wrapped again in a variant of the same type, like nothing happened. If the variant is of type Ok it will call the function we pass in the first argument.

const Val = Result.Ok('hello');

const Uppercase = (str) => Result.Ok(to_uppercase(str));
const Exclaim   = (str) => Result.Ok(exclaim(str));

const one = Result.chain(Exclaim, Result.chain(Uppercase, Val));
const two = Result.chain(v => Result.chain(Exclaim, Uppercase(v)), Val);

one.type === two.type
// true

one.data === two.data;
// true
Enter fullscreen mode Exit fullscreen mode

Since our function follows law we now have a way to compose functions that return other values of the same type. This is specially useful when creating a function composition where arguments of a function are the result of a previous function call.

Result.chain can also be use to create other utility functions. Let's start by creating one that allows us to "extract" a value from the wrapper structure.

const identity = (arg) => arg;

Result.join = Result.chain.bind(null, identity);
Enter fullscreen mode Exit fullscreen mode

So with this we get Result.join a function that only waits for the data parameter (this is the power of partial application). Let's see it in action.

const good_data = Result.Ok('Hello');
Result.join(good_data);
// "Hello"

const bad_data = Result.Err({ message: 'Ooh noes' });
Result.join(bad_data);
// { "type": "Err", "data": { "message": "Ooh noes" } }
Enter fullscreen mode Exit fullscreen mode

We called join because we should only use it to "flatten" a nested structure. Like in this case.

const nested_data = Result.Ok(Result.Ok('Hello'));

Result.join(nested_data);
// { "type": "Ok", "data": "Hello" }
Enter fullscreen mode Exit fullscreen mode

I'm going to abuse the nature of this function in future tests, to compare the content inside our structures. To make my intentions clear I'm going to create an "alias."

Result.unwrap = Result.join;
Enter fullscreen mode Exit fullscreen mode

Functor

If you've been reading other articles about functional programming in javascript that name may sound familiar. Even if you don't recognize it you've probably use it before. This part of the specification is the one that introduces our old friend .map. Let's see what makes it so special.

  • Identity
Val.map(v => v);
// is equivalent to
Val;
Enter fullscreen mode Exit fullscreen mode

It might not look interesting but it is. Pay attention to that function on the first statement, v => v, you know this one, right? We've used it before, it is known as the identity function. So, in math an identity element is a neutral value that has no effect on the result of the operation and this is exactly what this function does (nothing). But the interesting part is not on the surface, is what we can't see. If the first statement has the same effect as the second, then it means that .map(v => v) returns another value of the same type, it does that even if we give it the most useless function we could possibly imagine. Let's show this again using arrays as an example.

const identity = (arg) => arg;

const Val = ['hello'];
const Id  = Val.map(identity);

Array.isArray(Val) === Array.isArray(Id);
// true

Val.length === Id.length;
// true

Val[0] === Id[0];
// true
Enter fullscreen mode Exit fullscreen mode

That's nice but how does that help us? The important part to understand here is that .map should "preserve the shape" of our structure. In this case with arrays, if we call it with an array with one item we get back another array with one item, if we call it with an array with one hundred items we get back another array with one hundred items. Knowing that the result will always have the same type allows us to do stuff like this.

Val.map(fx).map(gx).map(hx);
Enter fullscreen mode Exit fullscreen mode

I know what you're thinking, using .map like that way with arrays can have a big impact on performance. Shall not be worried, the second law has that covered.

  • Composition
Val.map(v => fx(gx(v)));
// is equivalent to
Val.map(gx).map(fx);
Enter fullscreen mode Exit fullscreen mode

This law tells us that we can replace several calls to .map if we compose directly the functions we use as arguments. Let's try it.

const Val = ['hello'];

const one = Val.map(v => exclaim(to_uppercase(v)));
const two = Val.map(to_uppercase).map(exclaim);

one[0] === two[0];
// true
Enter fullscreen mode Exit fullscreen mode

So .map gave us the ability to combine those functions in different ways, this gives us the opportunity to optimize for speed or readability. Function composition is a very complex subject and I would like to say more but we don't have time for that right now. If you're curious about it you can read this: composition techniques.

Now is the time to implement the famous .map in our structure. You might have notice that this method is very similar to .chain, it has almost the same behavior except for one thing, with .map we should the guarantee that the result should be a value of the same type.

Result.map = function(fn, data) { 
  return Result.chain(v => Result.Ok(fn(v)), data)
};
Enter fullscreen mode Exit fullscreen mode

If you remember the behavior of .chain it only executes the callback function if data is a variant of type Ok, so the only thing we need to do to keep our structure is wrap the result from fn with Result.Ok.

const Val = Result.Ok('hello');

// Identity
const Id = Result.map(identity, Val);

Result.unwrap(Val) === Result.unwrap(Id);
// true

// Composition
const one = Result.map(v => exclaim(to_uppercase(v)), Val);
const two = Result.map(exclaim, Result.map(to_uppercase, Val));

Result.unwrap(one) === Result.unwrap(two);
// true
Enter fullscreen mode Exit fullscreen mode

Apply

This is a tough one, I better try to explain after I show you the law.

  • Composition
Val.ap(Gx.ap(Fx.map(fx => gx => v => fx(gx(v)))));
// is equivalent to
Val.ap(Gx).ap(Fx);
Enter fullscreen mode Exit fullscreen mode

"Whaaat?"

Yes, my thoughts exactly. That first statement is the most confusing thing we've seen so far. This time it looks like Fx and Gx are not functions, they are data structures. Gx has an .ap method so it must be the same type as Val. And if go further we can tell that Fx has a map method, that means is a Functor. So for this to work Val, Fx and Gx must implement the Functor and Apply specification. The last piece of the puzzle is this Fx.map(fx => ... fx(...)), there are functions involve but they are inside a data structure.

The name of this law and that second statement suggest this is about function composition. I'm thinking that this should behave just like .map but with a plot twist, the callback we get it's trapped inside a Functor. With this we have enough information to make our method.

Result.ap = function(res, data) {
  return Result.chain(v => Result.map(fn => fn(v), res), data);
};
Enter fullscreen mode Exit fullscreen mode

What's going on in here? Well, let me explain. First we get the value inside data if everything goes well.

Result.chain(v => ..., data);
Enter fullscreen mode Exit fullscreen mode

At this point we have problem, .chain doesn't give us any guarantee about the result, it can return anything. But we know that res is a Functor, so we can use .map to save the day.

Result.map(fn => ..., res);
Enter fullscreen mode Exit fullscreen mode

In here .map has two jobs, it give us access to the function inside res and helps us preserve the shape of our structure. So, .chain will return anything that .map gives it, with this in place we can now have the confidence to call .ap multiple times.

The last stop in our trip is this.

fn(v);
Enter fullscreen mode Exit fullscreen mode

That is what we actually want from .ap. Thanks to .map the result of that expression gets wrapped inside another variant which in turn goes back to the outside world thanks to .chain. We can test it now.

const Val = Result.Ok('hello');

const composition = fx => gx => arg => fx(gx(arg));
const Uppercase   = Result.Ok(to_uppercase);
const Exclaim     = Result.Ok(exclaim);

const one = Result.ap(Result.ap(Result.map(composition, Exclaim), Uppercase), Val);
const two = Result.ap(Exclaim, Result.ap(Uppercase, Val));

Result.unwrap(one) === Result.unwrap(two);
// true
Enter fullscreen mode Exit fullscreen mode

Fine, but what is it good for? Putting a function inside a Result.Ok doesn't seem like common thing, why would somebody do that? All fair questions. I believe this is all confusing because .ap is only half of the story.

.ap can be used to create a helper function called liftA2, the goal of this function is to make another function work with values that are wrapped in a structure. Something like this.

const Title = Result.Ok('Dr. ');
const Name  = Result.Ok('Acula');

const concat = (one, two) => one.concat(two);

Result.liftA2(concat, Title, Name);
// { "type": "Ok", "data": "Dr. Acula" }
Enter fullscreen mode Exit fullscreen mode

You can think of it as the extended version of .map. While .map is meant to work with callbacks that take one argument, liftA2 is designed to work with a function that takes two arguments. Now the question is how does it work? The answer is in this piece of code.

const composition = fx => gx => arg => fx(gx(arg));
Result.ap(Result.ap(Result.map(composition, Exclaim), Uppercase), Val);
Enter fullscreen mode Exit fullscreen mode

Let's see what happens here. It all begins with .map.

Result.map(composition, Exclaim)
Enter fullscreen mode Exit fullscreen mode

In this expression we extract the function inside Exclaim and we apply it to composition.

fx => gx => arg => fx(gx(arg))
// becomes
gx => arg => exclaim(gx(arg))
Enter fullscreen mode Exit fullscreen mode

That second statement gets wrapped inside an Ok variant which is exactly what .ap expects as the first argument. So, after .map gets evaluated we get this.

Result.ap(Result.Ok(gx => arg => exclaim(gx(arg))), Uppercase);
Enter fullscreen mode Exit fullscreen mode

And now that we have a function inside a variant .ap has all it needs to continue. In here we basically have more of the same, the function inside the second argument is applied to the function in the first. So we get this.

Result.Ok(arg => exclaim(to_uppercase(arg)));
Enter fullscreen mode Exit fullscreen mode

Notice the pattern now? We have yet another function inside a variant, and this is exactly what our last .ap gets.

Result.ap(Result.Ok(arg => exclaim(to_uppercase(arg))), Val);
Enter fullscreen mode Exit fullscreen mode

The cycle repeats again and finally we get.

Result.Ok('HELLO!!');
Enter fullscreen mode Exit fullscreen mode

This is basically the pattern that liftA2 follows, the only difference is that instead of taking functions to a value, we take values to a function. You'll see.

Result.liftA2 = function(fn, R1, R2) {
  const curried = a => b => fn(a, b);
  return Result.ap(Result.map(curried, R1), R2);
};
Enter fullscreen mode Exit fullscreen mode

We test again.

const concat = (one, two) => one.concat(two);

Result.liftA2(concat, Result.Ok('Dr. '), Result.Ok('Acula'));
// { "type": "Ok", "data": "Dr. Acula" }
Enter fullscreen mode Exit fullscreen mode

And what if you want to make a liftA3? You know what to do.

Result.liftA3 = function(fn, R1, R2, R3) {
  const curried = a => b => c => fn(a, b, c);
  return Result.ap(Result.ap(Result.map(curried, R1), R2), R3);
};
Enter fullscreen mode Exit fullscreen mode

And now that is the law of composition acting in our favor. As long a s Result.ap follows the law we can keep adding arguments with little effort. Now just for fun let's create a liftN function that can take any number of arguments. This time we will need a little help.

function curry(arity, fn, ...args) {
  if(arity <= args.length) {
    return fn(...args);
  }

  return curry.bind(null, arity, fn, ...args);
}

const apply = (arg, fn) => fn(arg);
const pipe  = (fns) => (arg) => fns.reduce(apply, arg);

Result.liftN = function(fn, R1, ...RN) {
  const arity   = RN.length + 1;
  const curried = curry(arity, fn);

  const flipped = data => R => Result.ap(R, data);
  const ap      = pipe(RN.map(flipped));

  return ap(Result.map(curried, R1));
};
Enter fullscreen mode Exit fullscreen mode

That is the "automated" version of liftA3. Now we can use all kinds of functions.

const concat = (one, ...rest) => one.concat(...rest);

Result.liftN(
  concat,
  Result.Ok('Hello, '),
  Result.Ok('Dr'),
  Result.Ok('. '),
  Result.Ok('Acula'),
  Result.Ok('!!')
);
// { "type": "Ok", "data": "Hello, Dr. Acula!!" }
Enter fullscreen mode Exit fullscreen mode

Applicative

You might have notice that everything we have built is some sort of extension of the previous methods, this will not be the exception. In order for our data structure to be an applicative it must first implement the Apply specification and then it must add one tiny detail.

The new contribution will be a method that can help us take a value and convert it into the simplest unit of our data structure. It's kinda like a constructor method in a class, the idea is to take any regular value and take to the "context" of our structure so we can start making any kind of operation.

You've probably used something like this before. With the Promise class we can do this.

Promise.resolve('hello').then(to_uppercase).then(console.log);
// Promise { <state>: "pending" }
// HELLO
Enter fullscreen mode Exit fullscreen mode

After we call Promise.resolve our 'hello' is "inside" a promise and we can immediately call methods like then or catch. If we wanted to do the same using the constructor we would have to do this.

(new Promise((resolve, reject) => { resolve('hello'); }))
  .then(to_uppercase)
  .then(console.log);
// Promise { <state>: "pending" }
// HELLO
Enter fullscreen mode Exit fullscreen mode

All that extra effort doesn't look very clean, right? This is why a "shortcut" is useful, we can make a "simple" unit of our data structure without extra steps. It's time to make this for Result.

Result.of = Result.Ok;
Enter fullscreen mode Exit fullscreen mode

I can assure you that is a coincidence, it's not always this easy. But really this is everything we need and we can prove that if we check the laws.

  • Identity
Val.ap(M.of(v => v));
// is equivalent to
Val;
Enter fullscreen mode Exit fullscreen mode

Our old friend "identity" comes back to remind us that .ap really does behave like .map.

const Val = Result.Ok('hello');

const Id = Result.ap(Result.of(identity), Val);

Result.unwrap(Val) === Result.unwrap(Id);
// true
Enter fullscreen mode Exit fullscreen mode
  • Homomorphism
M.of(val).ap(M.of(fx));
// is equivalent to
M.of(fx(val));
Enter fullscreen mode Exit fullscreen mode

Okay, so here we have a new concept we should learn. As far as I can tell an Homomorphism is some kind of transformation where we keep some of the "abilities" of the original value. I think this law tells us that .of doesn't have any effect when you "apply" a function to a value.

const value = 'hello';

const one = Result.ap(Result.of(exclaim), Result.of(value));
const two = Result.of(exclaim(value));

Result.unwrap(one) === Result.unwrap(two);
// true
Enter fullscreen mode Exit fullscreen mode

To recap, in the first statement we apply exclaim to value while both of them are wrapped inside a variant. In the second statement we apply exclaim to value directly. In both cases we get the same result. With this we prove that there is nothing special about .of, it's just there to create a unit of our data structure.

  • Interchange
M.of(y).ap(U);
// is equivalent to
U.ap(M.of(fx => fx(y)));
Enter fullscreen mode Exit fullscreen mode

This is a tough one. Honestly, I'm not sure I understand what's trying to prove here. If I had to guess I would say that it doesn't matter which side of .ap we have the .of method, if can treat its content as a constant the result will be the same.

const value   = 'hello';
const Exclaim = Result.Ok(exclaim);

const one = Result.ap(Exclaim, Result.of(value));
const two = Result.ap(Result.of(fn => fn(value)), Exclaim);

Result.unwrap(one) === Result.unwrap(two);
// true
Enter fullscreen mode Exit fullscreen mode

Monad

To create a Monad we must implement the Applicative and Chain specifications. So, what we have to do now is... nothing. Really, there is nothing left to do. You have created a Monad, congrats! Want to read some laws?

  • Identity - left side
M.of(a).chain(f);
// is equivalent to
f(a);
Enter fullscreen mode Exit fullscreen mode

We check.

const one = Result.chain(exclaim, Result.of('hello'));
const two = exclaim('hello');

one === two;
// true
Enter fullscreen mode Exit fullscreen mode

At this point you may be wondering, couldn't we have done this after implementing .chain (since.of is an alias for Ok)? The answer is yes, but that wouldn't be fun.

So, what problems does this solve? What do we gain? This solves a very specific problem, one that could happen very often if we use Functors and that is nested structures.

Say we want to retrieve a config object that we have in localStorage. We know this action can fail that's why we created a function that uses our Result variant.

function get_config() {
  const config = localStorage.getItem('config');

  return config 
    ? Result.Ok(config)
    : Result.Err({ message: 'Config not found' });
}
Enter fullscreen mode Exit fullscreen mode

This works wonders. Now the problem is localStorage.getItem doesn't return an object, the data we want is in a String.

'{"dark-mode":true}'
Enter fullscreen mode Exit fullscreen mode

We anticipated this so we created a function that can transform this into an object.

function safe_parse(data) {
  try {
    return Result.Ok(JSON.parse(data));
  } catch(e) {
    return Result.Err(e);
  }
}
Enter fullscreen mode Exit fullscreen mode

We know that JSON.parse can also fail, that's why we figure we could wrap it in a "safe function" that also uses our variant. Now try to use those two together using .map.

Result.map(safe_parse, get_config());
// { "type": "Ok", "data": { "type": "Ok", "data": { "dark-mode": true } } }
Enter fullscreen mode Exit fullscreen mode

Is that what you expected? If we close our eyes and pretend that get_config is always successful we could replace it with this.

Result.of('{"dark-mode":true}');
// { "type": "Ok", "data": "{\"dark-mode\":true}" }
Enter fullscreen mode Exit fullscreen mode

This law tells me that if I use .chain to apply the function to a structure it's the same as applying that function to the data inside the structure. Let's use that, we have the perfect function for this situation.

const one = Result.chain(identity, Result.of('{"dark-mode":true}'));
const two = identity('{"dark-mode":true}');

one === two;
// true
Enter fullscreen mode Exit fullscreen mode

I hope by now you know what I'm going to do. You've seen it before.

Result.join = Result.chain.bind(null, identity);
Enter fullscreen mode Exit fullscreen mode

Yes, it's .join. This is starting to look like a prequel. Let's open our eyes now and go back to our problem with .map.

Result.join(Result.map(safe_parse, get_config()));
// { "type": "Ok", "data": { "dark-mode": true } }
Enter fullscreen mode Exit fullscreen mode

We solved our problem. Now here comes the funny thing, in theory we could implement .chain using .join and .map. Using .join and .map together is so common that .chain was created (also, that's why some people call it .flatMap). Let's use it.

Result.chain(safe_parse, get_config());
// { "type": "Ok", "data": { "dark-mode": true } }
Enter fullscreen mode Exit fullscreen mode

Isn't it great when everything is wrap in a nice cycle? But don't get up your seats just yet, we still have a post-credit scene.

  • Identity - right side

So predictable. Alright, what does it say?

Val.chain(M.of);
// is equivalent to
Val;
Enter fullscreen mode Exit fullscreen mode

We know we can do this but let's check anyway.

const Val = Result.Ok('hello');

const Id = Result.chain(Result.of, Val);

Result.unwrap(Val) === Result.unwrap(Id);
// true
Enter fullscreen mode Exit fullscreen mode

Nice, what can we do with this? Well, the only thing I can think of right now is making a more generic version of .map.

Result.map = function(fn, data) {
  return Result.chain(v => Result.of(fn(v)), data);
};
Enter fullscreen mode Exit fullscreen mode

It may not look like much because .of and Ok are the same thing, but if our constructor was a bit more complex (like Promise) this could be a nice way to simplify the implementation of .map.

And with this we close the cycle and end our journey through Fantasy Land.

Conclusion

If you read all of this but couldn't understand all of it, don't worry you can blame me, maybe I didn't explain as well as I thought. It took me like two year to gather the knowledge to write this. Even if it takes you like a month to get it, you are already doing better than me.

A nice way to try understand how this methods work is to follow the specification using regular class instances, that should be easier.

I hope you enjoyed the reading and I hope I didn't cause you a headache. Until next time.

Sources


Thank you for your time. If you find this article useful and want to support my efforts, consider leaving a tip in ko-fi.com/vonheikemen.

buy me a coffee

Top comments (0)