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+B is the number of possible values for type A added to the number of possible values for type B.
    • Works like a logical OR
  • Product types
    • Counts like multiplication: the total number of possible values for a product type A*B is the number of possible values for type A multiplied by the number of possible values for type B.
    • Works like a logical AND

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':

SumProductSum and Product
ClosedEither a b

Variant (a :: A, b :: B)
Tuple a b

Record (a :: A, b :: B)
(e.g. { a :: A, b :: B }
These a b
OpenVariant (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 'nested Tuples'
  • Variants are 'nested Eithers'
-- 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.