Do you ever look at your React components and think to yourself, there has got to be a better way to handle these conditions inside my components.
When I was first introduced to functional programming. One of the "rules" imposed was to never use if / else / else if
.
This presented a huge issue for me. How can I manage this. Also, how can I manage this in my React Components?
Lets first look at an example of what I am talking about.
Below is an example of checking a variable and then returning the correct component.
The same result can also be achieved using a switch statement.
import React from 'react'
import Doberman from './Doberman'
import Chihuahua from './Chihuahua'
import BullDog from './BullDog'
const Dog = breed => {
if(breed === 'Doberman') {
return <Doberman />
} else if (breed === 'Chihuahua')
return <Chihuahua />
} else {
return <BullDog />
}
}
So, whats wrong with this?
In my opinion, its ugly.
It is not safe. breed
can come back as undefined or better yet some other breed of dog.
Also, in this example we are adding logic to our UI components, which is challenging to test.
So lets talk about how Catamorphisms can helps us manage these conditions in a different way.
Catamorphism
Taken from Wikipedia
In functional programming, catamorphisms provide generalizations of folds of lists to arbitrary algebraic data types
For those new to functional programming, fold can also be referred to as reduce or aggregate.
Say our application needed to determine the breed of dog, and then render the corresponding component to its user. In order to implement a catamorphism we would need to identify all the breeds of dog we would expect.
Here is an example of our list, that will support a catamorphism.
import daggy from 'daggy'
const DogBreed = daggy.taggedSum('DogBreed', {
Doberman : [],
Chihuahua : [],
BullDog : [],
Unknown : []
})
Our application would need to have an initial state defined. Inside our initial state we would assign our dog breed. Lets have a look...
const INITIAL_STATE = {
dog : {
breed : DogBreed.Unknown
}
}
Since our application has not yet loaded, and we do not know what our dog breed is, we create an Unknown
breed.
At some point in our applications lifecycle we would set our dog's breed.
Lets have a look at this example of setting our dog's breed using this super cool FP library Pratica.
import { Ok, Err } from 'pratica'
import daggy from 'daggy'
const DogBreed = daggy.taggedSum('DogBreed', {
Doberman : [],
Chihuahua : [],
BullDog : [],
Unknown : []
})
// DogBreed.is
// A useful built in type check when using daggy.
const isValidBreed = breed => DogBreed.is(breed) ? Ok(breed) : Err()
// Safe function with no side effects.
export const getBreed = dog => Ok(dog)
.chain(dog => Ok(dog.breed))
.map(breed => breed === 'Doberman' ? DogBreed.Doberman : breed)
.map(breed => breed === 'Chihuahua' ? DogBreed.Chihuahua : breed)
.map(breed => breed === 'BullDog' ? DogBreed.BullDog : breed)
.chain(isValidBreed)
.cata({
Ok: breed => breed,
Err: () => DogBreed.Unknown
})
Let me take a second to talk about what is going on here.
Im using the Ok
monad to check our dog object.
We pass our dog into our
Ok monad
Next step, we
chain
chain allows us to unwrap our Ok Monad.
a. Then set another Ok monad to check fordog.breed
.
b. Ifdog.breed
is undefined our Ok monad will return Err and will pass straight to ourcata.Err
where we setDogBreed.Unknown
.We then pass the into a
map
. Map accepts the output of ourchain
, ourOK Monad
.
a. Map takes anOK monad
unwraps it and checks it and then and wraps it back into ourOk monad
b. We map over every possible breed type.
c. If we find a match we set our breed.
d. If not we return type to our next map.Our last check,
.chain(isValidBreed)
.
a. Why do we need this? If the breed is not one we are expecting, we need to handle that case and defer toErr()
which will default toDogBreed.Unknown
b. We chain the result of our above maps into a functionisValidBreed
c. isValidBreed does a check on the breed. If it is of type DogBreed, we return an Ok monad with the breed. If not, we return Err()
Great.
Here, we show calling our safe getBreed
functions that implements pratica
.
// a contrived example of updating our state...
const dog = { breed: 'Chihuahua' }
const state = {
dog : {
breed : getBreed(dog) // DogBreed.Chihuahua
}
}
We are now ready to see this in action.
Lets remember the goal is using an alternative to if / else in our react components.
We will be passing breed
to our Dog component. breed
is now an instance of our daggy
DogBreed
. Meaning we can apply a catamorphism (cata) to it. The cata
will resolve to its current type.
import React from 'react'
import Doberman from './Doberman'
import Chihuahua from './Chihuahua'
import BullDog from './BullDog'
// Lets remember here that breed is the result of `getBreed(dog)` which is a List we can apply a catamorphism on.
const Dog = breed => breed.cata({
Doberman : () => <Doberman />,
Chihuahua : () => <Chihuahua />,
BullDog : () => <BullDog />,
Unknown : () => <div>{'Unknown breed'}</div>,
})
Daggy also supports the passing of parameters, which can also be quite interesting. Check out daggy here.
Top comments (0)