-
Reducing complexity is good, and so it's helpful to have a way of measuring it.
-
Look at this simple type:
type ManyOptions = {
type: 'electricity' | 'gas'
eco7?: boolean
}
-
How many options could there be above?
-
What about now?
type SlightlyLessOptions =
| {
type: 'electricity'
eco7: boolean
}
| {
type: 'gas'
}
-
A small change reduces the number of options, and thus the number of code paths needed to deal with it.
-
Although it's trivial now, as a data type grows it becomes more significant.
type ContrivedTariff = {
type: 'electricity' | 'gas'
eco7?: boolean
elecReading?: number
gasReading?: number
}
This measure of how many possible values exist in a type is called cardinality.
- It is defined by the first result in a Google search I just did as:
the number of elements in a set or other grouping, as a property of that grouping.
-
Boolean
can betrue
orfalse
-
so...?
-
Yes, indeed,
2
. -
type TrafficLights = 'Red' | 'Green' | 'Blue'
..? -
of course,
4
. -
I mean
3
. Lolle. -
The cardinality of
string
ornumber
is very large indeed -
How does this relate to our new friends
Either
andOption
?
-
So
Option
andEither
are both examples ofAlgebraic Data Types
-
More accurately,
sum types
-
(The other kind are
product types
, we'll come to those...)
-
Option A
is asum type
because it's cardinality is -
the sum of
whatever the cardinality of A is
and1
-
So
A
+1
, kinda. -
(The
1
is to representNothing
) -
The type
Option<boolean>
could have values of either -
Some(true)
-
Some(false)
-
Nothing
-
So,
3
.
-
Either E A
is also a sum type, but it'scardinality
is -
the sum of
the cardinality of E
andthe cardinality of A
-
so
E + A
, sort of (if you squint) -
So
Either<boolean, boolean>
would be2
+2
=4
-
or
Either<TrafficLights, boolean>
would be3
+2
=5
.
But how does it relate to complexity?
- Bare with me - I swear we're getting to a breakthrough
-
Product types
are the other kind ofAlgebraic Data Type
(ADT
) -
They are way more boring tbh.
-
Most Typescript interfaces are
Product types
interface Person {
name: string
age: number
}
-
When it comes to data modelling, they are the probably the tool we reach for first.
-
(And why wouldn't we? They are broadly supported and it's considered idiomatic to do so.)
-
(And I'm not writing a thinkpiece named Javascript Objects Considered Harmful or anything)
-
(yet)
-
Whilst
sum types
describethis
ORthat
. -
Product types
describethis
ANDthat
. -
So, a
Person
interface is just a nice way of carrying around astring
AND anumber
-
A good example of how these things are actually very similar is
Tuple
. -
A very simple simple product type is
Tuple A B
-
We could represent it like this:
type Tuple<A, B> = { type: 'Tuple'; a: A; b: B }
-
It is the dual of
Either
... -
Either<A,B>
isA
+B
. -
Tuple<A,B>
isA
xB
. -
A
Tuple<string,number>
could representPerson
interface from before as it's theirname
ANDage
. -
Whilst
Either<string,number>
could be used to describe a person'sname
ORage
. -
(I'm not sure why you would do this, admittedly)
-
So, knowing that the cardinality of
Either<TrafficLights, boolean>
is5
-
What is cardinality of
Tuple<TrafficLights, Boolean>
...? -
.
-
..
-
...
-
3
x2
=6
-
We calculate the cardinality of a
sum type
with+
-
But for
product types
we usex
-
You can imagine which ones end up bigger
-
Adding another
boolean
multipies the options by2
. -
And adding an
optional boolean
multiplies the options by3
. -
So wherever you find yourself adding more rows to a type, think
-
Will this REALLY always be there?
-
Or can I make it a
sum
instead?
- Given a constructor for making
Tuple
types:
const tuple = <A, B>(a: A, b: B): Tuple<A, B> => ({
type: 'Tuple',
a,
b,
})
tuple('Horse', 100)
// { type: "Tuple", a: "Horse", b: 100 })
-
...can we implement some of the functions we made for
Either
forTuple
? -
map :: (B -> D) -> Tuple A B -> Tuple A D
-
leftMap :: (A -> C) -> Tuple A B -> Tuple C B
-
bimap :: (A -> C) -> (B -> D) -> Tuple A B -> Tuple B D
-
match :: (A -> B -> C) -> Tuple A B -> C
-
Why is implementing
join
orbind
difficult?