When you first learn to program, you quickly learn to wield the power of if/then statements. Being one of the base building blocks of writing software, you discover the simplest form of program flow.
In programming language terminology, an “expression” is a combination of values and functions combined and interpreted by the compiler to create a new value instead of a “statement”, which is just a standalone execution unit that doesn’t return anything. [1]
So, an expression makes you focus on the small context of your sub-problem. You return a value and continue with that. Use the value to solve the next sub-problem, and so on. A simple example:
You only work with the data known inside your context (which is a function here).
A statement created with if/then and switch allows you to think and work with a global context. And for small applications like scripts, this does not have to be a problem, but once the project grows or multiple people work on it over time, the global context easily grows into spaghetti. There are no restraints for you or other developers to add another totally unrelated clause that writes to any random part of the global context or even outside calling remote services directly.
A switch statement looks a bit more like pattern matching (I’ll explain what that is in a minute), however, because it also is implemented as a statement, you can have completely unrelated test expressions. Let’s look at an example where a switch statement is used to alter the global context based on a string value to match:
Where “priveWithVAT” and “notifyUser” are declared elsewhere in the global context. The variable ‘fruit’ is the context to match on, but the switch statement does not enforce cohesion between the test cases and besides that allows you to make further statements, like ‘notifyUser = true’ and ‘callRemoteService()’ in the global context.
The result of running this code is that several global variables are updated. What is the problem with global context? It could be unintentionally updated by other pieces of code causing unintended effects from your statements. Besides that it makes your code harder to understand, there are moving parts everywhere.
Another problem of if/then and switch is that it does not force you to write a case for all possible inputs. Sure you could add an ‘else’ to your block or a ‘default’ for a switch, but those are optional, so you may forget, or you add one and your colleague deletes it when refactoring.
So the bottom line is that statements like if/else and switch have some disadvantages:
Luckily, in the 1950s smart people solved this problem for us. They invented pattern matching. Pattern matching gives you a context to guide the matching to a clear outcome (expression). In typed languages, the compiler forces you to cover all possible cases.
Many modern languages like F#, Haskell, Python, Rust, Scala and Swift have pattern matching built into the language. But when working on a web application, you may need to use JavaScript or TypeScript. Can you use pattern matching in this application to improve the quality of your software?
There is currently no built-in support for pattern matching, but a proposal [2] championed by developers from Google, Microsoft, Netflix, Sony and others is being worked on. This, however, could take many years. In the meanwhile, we can use several options depending on your project and needs.
Is a large collection of small functions that you can combine to create a program in a declarative way. If you happen to use Ramda in your project, know that you can use ‘cond’ to create an expression that can pattern match. “cond” needs a set of predicate function + outcome function pairs. The first pair where the predicate is true will have its outcome called.
Gives us “sum types” and a way to actually match input and output on the type. Since JavaScript does not have strong types you can use Daggy to create the type structure you need to pattern match on.
A “sum type” is literally a summation of all the options a type can have, for example a boolean sum type consists of the sum of all options for a boolean being true and false.
Daggy makes sense when you are working with a view technology like React. You can separate the definition plus the creation of the sum type (outside the view layer) from the usage of the sum type (inside the view layer). This way you can keep your view layer as simple as possible.
Armed with Daggy to define the possible options in the sum type and Ramda.cond for the matching of the given input to the right type in the sum, we can do this:
One of the principles here is to keep the view as dumb as possible. When you do not care so much or just want to have one place to go from input to output, you could also skip Daggy and only use Ramda.cond and some function abstractions like isUnderweight, isNormal etc. directly in the View.
(for reference I’ve included a version without pattern matching [8]).
If you are working with TypeScript instead of JavaScript, you would like to take advantage of having the compiler. You could see if you break the context you are using at compile time, and you could check if you’ve covered all branches when your use case has a finite set of logical branches (options/outcomes).
This lib was especially made to take advantage of TypeScripts compile time type checking. This will give you confidence you are not violating the context you declared.
Instead of using Daggy to structure the context and Ramda.cond to pattern match, we’ll use a TypeScript type for the structure and ts-pattern “match” to do the matching in both model and view layers.
Again, if you want to combine the model and view layers into one, you can do the calculation of the categories and the displaying of the right category in one call to “match”.
In the references are links to two GitHub repos where you can check out complete runnable projects.
Pattern matching is a powerful and flexible technique, reducing the chance of subtle bugs. And while we wait for native support in JavaScript and TypeScript, we can use (and combine) the options mentioned above.
For a web-application It has added value to ‘dumb down’ your view layer so that it only has to pattern match on a structure that has all information needed to render. When using ts-pattern or Daggy you can rest assured that all possible view states are handled and that your view is very readable.
Tjerk Valentijn is a seasoned Software Developer with a remarkable 12-year tenure at Visma Connect. In his role, Tjerk collaborates with a talented team to craft cutting-edge software solutions, leveraging his expertise in programs like Typescript and JavaScript. His contributions are pivotal in advancing technology for the Dutch government, showcasing Tjerk's commitment to excellence and innovation in software development.
[1] https://fsharpforfunandprofit.com/posts/expressions-vs-statements/
[2] https://github.com/tc39/proposal-pattern-matching
[3] https://github.com/fantasyland/daggy
[4] https://ramdajs.com/docs/#cond
[5] https://github.com/gvergnaud/ts-pattern
Source code for this article is available
https://github.com/tjercus/bmi-daggy-ramda
https://github.com/tjercus/bmi-ts-pattern
[8] https://github.com/tjercus/bmi-daggy-ramda/tree/an_imperative_version