Episode 3 - Products

This is the third episode of the series about types. If you have not read previous episodes yet, i strongly recommend doing so before diving into this one.

Products, often called tuples, records or case classes, are a convenient way to bundle values of different types into a single value. The product of n types (with n being 0, 1, 2, etc) A_1, A_2, …, A_n is precisely the type whose values are formed with exactly one value of each type A_i for 1 ≤ i ≤ n. It is written (A_1, A_2, ..., A_n) in many languages, Product_n[A_1, A_2, ..., A_n] in Scala and A_1 × ... × A_n in Type Theory. As an example, the product of two types A and B would be defined as

final case class Product2[A, B](a: A, b: B) {
  def fold[R](f: (A, B) => R): R = f(a,b)
data Product2 a b = Constructor a b

Although in practice this is the preferred way to define the product of two types A and B, we will in this presentation prefer the equivalent definition below:

sealed abstract class Product2[A,B] {
  def fold[C](f: (A, B) => C): C =
    this match {
      case Constructor(a,b) => f(a,b)
final case class Constructor[A,B](a: A, b: B) extends Product2[A,B]
data Product2 a b where
  Constructor :: a -> b -> Product2 a b

This equivalent definition makes clear the distinction between the type Product2[A,B] and the constructor Constructor[A,B]. the constructor is a function taking as argument one value of type A, another value of type B and returning a value of type Product2[A,B]. Its type in Haskell is A -> B -> Product2 A B and in Scala (A, B) => Product2[A,B]. Note than in Scala functions of type (A, B) => C are functions of two arguments (precisely Function2[A,B,C]) and not of one argument (A,B) (which would be Function1[(A,B), C]):

scala> Constructor[Int, String] _ : ( (Int, String) => Product2[Int, String] )
res0: (Int, String) => Product2[Int,String] = $$Lambda$1620/2032136633@55be2608
Prelude> :t Constructor
Constructor :: a -> b -> Product2 a b

Besides, by definition constructors are injective functions, which means that for any values a1:A, a2:A, b1:B and b2:B, Constructor[A,B](a1,b1) == Constructor[A,B](a2,b2) if and only if a1 == a2 and b1 == b2 $$\forall a1,\ a2:\texttt{A},\ b1,\ b2:\texttt{B},\quad \texttt{Constructor[A,B]}(a1,b1) = \texttt{Constructor[A,B]}(a2,b2) \Longleftrightarrow a1 = a2 \textbf{ and } b1 = b2$$

Besides, Constructor[A,B] being the only constructor of Product2[A,B] then for any value v of type Product2[A,B], there is a unique value a of type A and a unique value b of type B such that v = Constructor[A,B](a,b) $$\forall v:\texttt{Product2[A,B]}\ \exists!\ (a:\texttt{A} \textrm{ and } b:\texttt{B}),\quad v = \texttt{Constructor[A,B]}(a,b)$$

Equivalence of inductive and functional forms

Once again any product can be equivalently expressed as a data type Product_n_Ind[A_1, ..., A_n] and as a function type Product_n_Fun[A_1, ..., A_n] with inverse functions ind2fun and fun2ind converting back and forth types Product_n_Ind[A_1, ..., A_n] and Product_n_Fun[A_1, ..., A_n]. For example with n = 2:

sealed abstract class Product2Ind[A,B]
final case class ConstructorInd[A,B](a: A, b: B) extends Product2Ind[A,B]

trait Product2Fun[A,B] {
  def fold[C](f: (A, B) => C): C

def constructorFun[A,B](a: A, b: B): Product2Fun[A,B] =
  new Product2Fun[A,B] {
    def fold[C](f: (A, B) => C): C = f(a, b)

def ind2fun[A,B]: Product2Ind[A,B] => Product2Fun[A,B] =
  (i: Product2Ind[A,B]) =>
    i match {
      case ConstructorInd(a,b) => constructorFun(a,b)

def fun2ind[A,B]: Product2Fun[A,B] => Product2Ind[A,B] =
  (f: Product2Fun[A,B]) => f.fold[Product2Ind[A,B]](ConstructorInd[A,B] _)
data Product2Ind a b where
  ConstructorInd :: a -> b -> Product2Ind a b

type Product2Fun a b = forall c. (a -> b -> c) -> c

constructorFun :: a -> b -> Product2Fun a b
constructorFun a b f = f a b

ind2fun :: Product2Ind a b -> Product2Fun a b
ind2fun (ConstructorInd a b) = constructorFun a b

fun2ind :: Product2Fun a b -> Product2Ind a b
fun2ind f = f ConstructorInd

Generalization to a product of any number of types

The definition of Product2 above can be adapted to Product_n[A_1, ..., A_n] for any value of n (i.e. 0, 1, 2, 3, 4, …). With n = 0, which means a product of zero types, the constructor takes zero argument, which means it is a constant. Thus Product_0 is equivalent to Unit (i.e. Product_0 ≅ Unit). This is the reason why Unit is sometimes written (). With n = 1, Product_n[A] ≅ A.

Further more for any types A, B and C we have the following properties

  • (A , B) is equivalent to (B , A)
  • ((A, B), C) and (A, (B, C)) and (A, B, C) are all equivalent
  • (Unit, A) and (A, Unit) and A are all equivalent
  • (A) is equivalent to A
  • (False, A) and (A, False) and False are all equivalent

Exercise: write conversion functions from and tofor each of these equivalence, such that from and to are inverse functions.

Next Episode: CoProducts

In the next episode, we will a convenient way to express alternatives: CoProducts.