Use your Cerebral — match your patterns!

Damian Płaza
5 min readDec 26, 2018

--

A pattern: Photo by Vanessa Ives on Unsplash

In previous post we had outstanding journey from imperative to declarative using Cerebral library. If you don’t know what Cerebral is or you just want to have a brief review what you can easily achieve using it, check this post. Now I would like to invite you to a short story of pattern matching in Cerebral.

What is pattern matching? Well, of course I can give you this definition, but we don’t have a time for that. At this point we can’t afford complete pattern matching mechanism in JS/TS (for your curiosity check this). There are some libraries giving us some amount of power to handle (like unionize), but still it won’t be it’s final form.

Fortunately, we’re in the middle of Cerebral post series! Is Cerebral equipped with proper pattern matching? Does it empower us so we can write bug-free, expressive and safe code? Well, the answer is — it depends. Let’s define our goal. Setting up a goal is really important I would say.

The goal

Suppose we were asked to create a small (tiny, micro, etc.) feature for customer’s application — users can buy some stuff, really doesn’t matter what exactly. New functionality is about to introduce fancy points — they’re giving possibility to buy new kind of stuff. Contact person informed that only Buyers should be allowed to buy — this means only this type of person should see respective UI. Other roles — Admins and Officers — shouldn’t have access (strange, huh?).

Tiny state

Here’s the state of the application that we’re going to implement.

Easy, straightforward, etc. Nothing can go wrong, right?

First thoughts

So how we can tackle that? What’s our strategy? Let’s see what tools we have under Cerebral’s toolbelt.

As you probably can recall from the first post, Cerebral has interesting building block called operators. In reality, they are function factories accepting some arguments and returning action or sequence of actions. They are giving flexibility, better readability and reusability across your Cerebral’s codebase.

Cerebral has equals operator built-in. Basically this operator enables expressing different states/values that given tag equals to. Here we can see how it works in some sequence trying to match selected payment method:

Looks very clear, isn’t it? I think we would agree on that. Equals introduces additional path/case which is named otherwise — in case of other paths weren’t matched. This resembles complete form of pattern matching where you can aggregate not handled cases. We can cover all possible cases and leave opportunity to handle unexpected “other” ones. Another pattern matching resemblance point!

What’s more, it expresses domain quite verbosely, gives intentions to the new team mate and also brings feeling that we’re safe enough to add new cases. Wait, what?

It’s a trap

It seems like we’re not safe. Even if we accepted not ideal world of TypeScript, we’re leaving some space for unintented refactoring bugs that would creep into the app when you aren’t looking. Branching plain object after equals operator is completely untyped. It means that only “otherwise” branch you can really expect to preserve your refactoring. What if you would change “Buyer” role to “RegularUser” and introduce another type of user that would have other alternatives? Well, you’ll get runtime exception if you forgot to change it in at least one place.

What if I told you…

What if there’s a perfect world when all people are happy and…Wait, wrong story. What if I told you that you would be warned by a compiler if you forgot to change one occurrence after a refactoring process in your Cerebral app? Even if it’s not that safe like in F#, Haskell or Elm, still I would say it’s worth it. Each small step towards greater stability/safety is on demand. Sorry, I need to emphasize it. I SAID SMALL STEP. It doesn’t mean rewriting your entire application with 130 different views, hundreds sequences to Elm. It means adding more type safety to your front-end application, if it’s really low hanging fruit. I think you know what we’ll do right now? Exercise!

Start small

We would like to have a match operator that would accept a tag and paths as arguments. Caller should be responsible for placing logic for each path. You know, reusability. We’re going to start with types (it’s awesome to start with types, belive me. I’m waving to you, dear F#).

In this snippet of code we declared PathsOfType type — it’s responsible for choosing between string base type (including union type, enum, regular string) and number base type or object base type.

Generic Paths type adds type with otherwise property. Strickly speaking, we’re composing cases given from input generic type and aggregated, unhandled cases.

This was the heavy lifting in terms of entire match operator. Here we have code for operator itself.

As you can see, no magic. We’re ensuring safety by using TypeScript powers. Additionally, we’re restricting caller to always provide paths argument. Raw Cerebral doesn’t enforce this. By enclosing usage of built-in Cerebral’s operator, along with paths object, we can feel pretty sure that our new team mate or Cerebral beginner would recognize our intentions.

How our refactored code looks like using our new, shiny match operator?

Almost like no change. Except the fact we gained slightly more confidence when refactoring case-based code. We can go one step further and introduce more strict alternative to match operator — let’s call it strictMatch. It looks almost the same, but it doesn’t leave any space of “other” cases. We are obliged to cover all possible ones. This also means we’re limited to case-based types like union types, enums and objects working as enums.

We removed “otherwise” type and made all properties as required. So whenever we want to increase our confidence while trading-off our patience, here we are.

Summary

Do not lie to ourselves — combined TypeScript and Cerebral aren’t that safe as Haskell, F#, Elm or PureScript. Our lovely Cerebral has some capabilities of pattern matching, but it isn’t complete. We can add a bit more and empower it, but still we’re living in unsafe world. Deal with it.

But! Looking positively into goal we achieved with small effort, I would say it’s fair enough to feel satisfied. We levereged Cerebral “native” operator to be more helpful during refactoring process and not only then. Also we gained extra verbose code points, because, in some sense, we’re forced to cover all cases or set “otherwise” bouncer.

Cerebral gives a great way to operate on underlaying possible values in terms of case types like union type or enum. I would say it’s so awesome feature of a library when it transforms its users into a superheroes that can easily create better tools out of built-in tools and not fight against it.

Thanks for your attention and see you next time!

--

--