TL;DR; functional programming contains a lot of jargon that can sometimes get in the way of the purpose. In this post, we take a different approach to think about functional ADTs.
Who is this post for?
- Developers who are familiar with Javascript and understand functions, closures, and higher-order functions.
- Want to learn alternative building blocks than loops, and other primitive control flows.
- Likes creating highly maintainable and extendible code with clean abstractions and intuitive patterns
What will I learn?
- Basics of an Algebraic Data Type
- How to change imperative code into declarative code using ADTs
Example: Change this: (imperative code)
var greeting = 'hello'
greeting = greeting + ' world' // add world
greeting = greeting.toUpperCase() // make loud
greeting = greeting + '!' //exclaim
console.log(greeting)
Example: To This: (declarative code)
const append = y => x => x + y
const toUpper = x => x.toUpperCase()
const exclaim = x => append('!')(x)
const greeting = ['hello']
.map(append(' world'))
.map(toUpper)
.map(exclaim)
.pop()
console.log(greeting)
Example: Or This with Identity ADT (declarative)
const greeting = Identity('hello')
.map(append(' world'))
.map(toUpper)
.map(exclaim)
.extract()
Rather watch than read, checkout this video:
What are algebraic data types? ADTs? Why should I care to learn these patterns?
ADTs is a steep learning curve for sure, but the return on investment is so worth the climb. You get all the "ilities":
- Maintainability
- Testability
- Reliability
- Extensibility
Learning ADTs resets your mindset when approaching software problems, you start to view programming as flowing data versus a set of stateful conversions.
Separation of concerns
Have you heard of concepts like separating your business logic from your side effects? And use more pure functions, create small utility functions, or reuse utility (aka RamdaJS) libraries that contain these little functions.
How? Use ADTs
ADTs are a set of types that can compose business logic into a pipeline that manages and contains the process from A to B.
I would be lying if I said this post will contain all the information you will ever need to fully understand ADTs, but hopefully, it will help you in your journey. ย
More than likely writing modern Javascript, developers have used already ADTs without even knowing it. Without going into a lot of jargon a couple of ADT-like types are built in the language. (Arrays, Sets, Maps and Promises)
I must confess I hardly ever use Sets and Maps, if I need a more complex data structure, I usually reach for a List type from a functional library like immutability js.
An array is an ADT ๐
Let's look at arrays, arrays are containers, they can hold values, developers can treat the array as an ADT. The identity ADT holds a value and allows you to apply map
and chain
to that value while keeping the value within the ADT container.
Why contain values and then operate on them?
You may have heard of things like nulls and exceptions, these can cause problems in your codebase, they are the source of many bugs, by wrapping values in a container, you prevent the outside world from modifying those values and only allow your application to use methods like map
and chain
to modify the wrapped value.
The map
method on an ADT takes a function, this function receives the value inside the ADT as an argument, then replaces the value with the returned result of the function.
[1].map(v => v + 1) // -> [2]
You can think of the ADT as a container and the value is inside the container, the only way you can modify the value is to call a method on the container or ADT. This interface creates a chain-able pattern because every method returns the ADT back to the developer.
[1].map(v => v + 1).map(v => v + 2).map(v => v + 4) // -> [8]
This technique starts to flow data through a series of pure functions, the functions can't have side effects.
In the example, you see the value modify from 1 to 2 to 4 to 8 after each map
is called. At end of the pipeline, the value is removed from the container and passed to our client.
In the identity ADT, you would call this method extract
, but an array does not have an extract
method, but it has a pop
a method that will do the trick.
[1].pop() // -> 1
Another common method on an ADT is called, this method allows you to replace the ADT with another ADT of the same type. With map
you replace the value with chain
you replace the container. Array does not have a method named, but it has a method called flatmap
that performs the chain
function.
[1].flatmap(v => [3]) // -> [3]
The chain
replaces the entire type instance with a new type instance of the same type. Said another way, chain
replaces a container and value with a different container and different value. While it may not seem handy on the array, the chain
method will become very handy on other ADTs. ย
Build our own ADT
We can build or own ADT using the map, chain, and extract
methods:
const Id = v =>
({
map: fn => Id(fn(v)),
chain: fn => fn(v),
extract: () => v
})
Now we can do the same logic we did with Array with our Id ADT:
Id(1).map(v => v + 1).map(v => v + 2).map(v => v + 4) // -> Id(8)
Id(5).chain(v => Id(10)).extract() // -> 10
How does this relate to some of the above benefits?
By keeping your data in a container, developers are encouraged to apply small pure functions to modify the value in a control flow.
Extensibility
Id(1)
.map(add(1))
.extract()
Id(1)
.map(add(1))
.map(mult(2)) // extend by adding a new map
.map(add(10)) // extend again
.extract()
Give it a try
This is a simple example, but start with the value as a string and uppercase the string, then append a !
to the string finally extract the results using both the array
and Id
.
Now swap the uppercase and !
functions include a function that replaces all spaces in the string with a -
.
In the next post, we will discuss the Async ADTs, how to work on side effects in pure functions. Side effects are necessary in building applications, but the more you can keep them on the fringe of your application, the more maintainable your application becomes. In the next post we will learn about the value of lazy triggered functions and working with side effects in a purely functional way.