Sum and Product Types
There are generally two data types in FP languages. These are otherwise known as Algebraic Data Types (ADTs):
- Sum types
- Counts like addition: the total number of possible values for a sum type
A+Bis the number of possible values for typeAadded to the number of possible values for typeB. - Works like a logical OR
- Counts like addition: the total number of possible values for a sum type
- Product types
- Counts like multiplication: the total number of possible values for a product type
A*Bis the number of possible values for typeAmultiplied by the number of possible values for typeB. - Works like a logical AND
- Counts like multiplication: the total number of possible values for a product type
These are better explained in this video as to how they get their names.
The simplest form of them are Either and Tuple
-- sum
data Either a b -- a value of this type is an `a` value OR a `b` value
= Left a
| Right b
-- product -- a value of this type is an `a` value AND a `b` value
data Tuple a b
= Tuple a b
-- both -- a value of this type is one of the following:
data These a b
= This a -- - an `a` value
| That b -- - a `b` value
| Both a b -- - an `a` value AND a `b` value
-- For example, These could be rewritten to
-- use a combination of Either and Tuple:
type These_ a b = Either a (Either b (Tuple a b))
However, these types can also be 'open' or 'closed':
| Sum | Product | Sum and Product | |
|---|---|---|---|
| Closed | Either a bVariant (a :: A, b :: B) | Tuple a bRecord (a :: A, b :: B)(e.g. { a :: A, b :: B } | These a b |
| Open | Variant (a :: A | allOtherRows) | Record (a :: A | allOtherRows)(e.g. { a :: b | allOtherRows }) | - |
What does 'Open' mean?
Using this example from the Syntax folder...
-- the 'r' means, 'all other fields in the record'
function :: forall r. { fst :: String, snd :: String | r } -> String
function record = record.fst <> record.snd
-- so calling the function with both record arguments below works
function { fst: "hello", snd: "world" }
function { fst: "hello", snd: "world", unrelatedField: 0 } -- works!
-- If this function used Tuple instead of Record,
-- the first argument would work, but not the second one.
Here's another way to think about this:
Records are 'nestedTuples'Variants are 'nestedEithers'
-- We could write
Tuple a (Tuple b (Tuple c (Tuple d e)))
-- or we could write
{ a :: A, b :: B, c :: C, d :: D, e :: E }
-- which desugars to
Record ( a :: A, b :: B, c :: C, d :: D, e :: E )
-- We could write
Either a (Either b (Either c (Either d e)))
-- or we could write
Variant ( a :: A, b :: B, c :: C, d :: D, e :: E)
Keep in mind that records/variants can be but do not necessarily have to be open. If we changed the above function's type signature to remove the r, it would restrict its arguments to a closed Record:
closed :: { fst :: String, snd :: String } -> String
closed record = record.fst <> record.snd
closed { fst: "hello", snd: "world" } -- compiles
closed { fst: "hello", snd: "world", unrelatedField: 0 } -- compiler error
Concluding Thoughts
The next few pages will cover the above types in a bit more depth. However, performance-wise, it's generally better to use Record instead of Tuple, and it's definitely better to use Record instead of a nested Tuple.
Similarly, it's better to use Variant instead of a nested Either. However, sometimes Either is all one needs and Variant is overkill.
For people new to the language and algebraic data types (ADTs) in general, stick with Tuple, Either, and closed Records.