Types as Specifications:

An Intro to Type-level Programming in Haskell

Thomas Dietert

Lambda Conf 2019

Boulder, CO

Setup

.../types-as-specifications $ stack setup
.../types-as-specicications $ stack build

whoami

  • Software Engineer @ Freckle

  • Hardware Hobbyist

  • Boulderer

  • Reader

  • NE IPA drinker

“Everyone thinks they think, but if you don’t write down your thoughts you are fooling yourself"

 

- Leslie Lamport

Workshop Structure

  • Introduction
    • Lecture & Exercises
  • Type-level Programming
    • Lecture & Exercises
  • Type-Indexed Lists
    • Lecture & Exercises
  • Discussion
    • First-class TypeFamilies
    • Type-level State Machines

Assumed Prerequisite Knowledge

  • Static vs Dynamic Type Systems

  • Type Signatures

  • Algebraic Datatypes

  • Type classes

  • Constraints

  • Functors, Applicatives, & Monads

Haskell

  • strong, static type system

  • Algebraic Datatypes (ADTs)

  • pure vs impure

  • GHC: an extensible compiler

Introduction:

Types as program Specifications

What is a Specification?

 

A specification is a formal description of program semantics

 

What does your program mean?

Types of Specifications

  • Decision Tables

  • Business Requirements

  • RFCs

  • Documentation

  • Types

What is a type?

 

Metadata associated with a particular region of memory locations that describe the structure of the data content of those memory locations.

A set designating the shape, form, or structure of language level values.

The Goal:  to make invalid states unrepresentable

Values, Types, & Kinds

values describe data

types describe values

kinds describe types

$$ Int = \{..., -1, 0, 1, ...  9223372036854775807 \}$$

\begin{aligned} * =&\ \{..., Int, Maybe\ Bool, a \rightarrow b, ...\} \\ * \rightarrow * =&\ \{..., Maybe, Either \ Double, Reader \ Int, ...\} \end{aligned}

$$17 = \text{<Language Specific Memory Layout>}$$

Types of Values

Prelude> :type True
True :: Bool
Prelude> :type Just "Haskell"
Just "Haskell" :: Maybe [Char]
Prelude> :type 5
5 :: Num p => p
Prelude> :type fmap
fmap :: Functor f => (a -> b) -> f a -> f b
Prelude> :type (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b

Value Constructors

data Maybe a 
  = Just a
  | Nothing

data Either a b 
  = Left a
  | Right b

newtype T m a 
  = T { unT :: m a }
Prelude> :t Nothing
Nothing :: Maybe a
Prelude> :t Just 
Just :: a -> Maybe a
Prelude> :t Left
Left :: a -> Either a b
Prelude> :t Right
Right :: b -> Either a b
Prelude> :t T
T :: m a -> T m a

Kinds of Types

Prelude> :kind Int
Int :: *
Prelude> :kind Maybe
Maybe :: * -> *
Prelude> :kind Either
Either :: * -> * -> *
Prelude> :kind Either Bool
Either Bool :: * -> *

Type Constructors

data Maybe a 
  = Just a
  | Nothing

data Either a b 
  = Left a
  | Right b

newtype T m a 
  = T { unT :: m a }

data App m f a b  
  = App 
  { x :: f a
  , y :: m a b
   } 
Prelude> :k Maybe
Maybe :: * -> *
Prelude> :k Either
Either :: * -> * -> *
Prelude> :k T
T :: (* -> *) -> * -> *
Prelude> :k App
App  
  :: (* -> * -> *) 
  -> (* -> *) 
  -> * 
  -> * 
  -> *

Type Constructors

data Maybe (a :: *) 
  = Just a
  | Nothing

data Either (a :: *) (b :: *) 
  = Left a
  | Right b

newtype T (m :: * -> *) (a :: *) 
  = T { unT :: m a }
{-# LANGUAGE KindSignatures #-}
data App 
  (m :: * -> * -> *) 
  (f :: * -> *) 
  (a :: *) 
  (b :: *) 
  = App 
  { x :: f a
  , y :: m a b
  } 

Intro to

Type-level Programming in Haskell

  • Phantom Types
  • GADTs ​

Language Extensions

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GADTs                 #-}

Phantom Types

  • Allow us to attach extra information to all values of a datatype without modifying their structure.
  • Notably, "phantom" type arguments attach this information to all value constructors.
-- Example:
data Proxy a = Proxy

Phantom Types: Motivation

data DistanceUnit 
  = Miles 
  | Kilometers
  deriving Eq

data Distance 
  = Distance
  { distanceUnit :: DistanceUnit
  , distanceValue :: Double
  } 
addDistances
  :: Distance
  -> Distance
  -> Either [Char] Distance
addDistances d1 d2 
  | distanceUnit d1 == distanceUnit d2 
    = Right
    $ Distance
    { distanceUnit = d1
    , distanceValue = d1 + d2
    }
  | otherwise 
    = Left "Distance units unequal!" 

Phantom Types: Motivation

-- This doesn't work very well...
convert :: Distance -> Distance
-- | Wrap the underlying conversion function
convertDistance :: Distance -> DistanceUnit -> Distance
convertDistance (Distance fromUnits v) toUnits = 
  convert v fromUnits toUnits
-- | This is better, but more granular than we want
convert :: Double -> DistanceUnit -> DistanceUnit -> Double
convert v Miles      Kilometers = v * 1.60934
convert v Kilometers Miles      = v * 0.621371
data PDistance a
  = PDistance
  { pdistanceValue :: Double
  }

addPDistances
  :: PDistance a 
  -> PDistance a 
  -> PDistance a
addPDistances (PDistance pv1) (PDistance pv2) 
  = PDistance (pv1 + pv2)

Phantom Types: Solution

Phantom Types: Solution

data Miles
data Kilometers

milesToKilometers 
  :: PDistance Miles 
  -> PDistance Kilometers
milesToKilometers (PDistance v) 
  = PDistance (v * 1.60934)

kilometersToMiles 
  :: PDistance Kilometers 
  -> PDistance Miles 
kilometersToMiles (PDistance v) 
  = PDistance (v * 0.621371)

Phantom Types: Solution

{-# LANGUAGE MultiParamTypeClasses #-}

class PConvert a b where
  pconvert :: PDistance a -> PDistance b

instance PConvert Miles Kilometers where
  pconvert = milesToKilometers

instance PConvert Kilometers Miles where
  pconvert = kilometersToMiles

{-# LANGUAGE GADTs #-}

(Generalized Algebraic Datatypes)

  • We can embed type information for datatype constructor return types
  • Constructor return types can differ
  • Pattern matching on GADT constructors can determine the return type of the function case expression
-- | GADT syntax
data Either a b where
  Left  :: a -> Either a b
  Right :: b -> Either a b
-- | ADT syntax
data Either a b 
  = Left a
  | Right b
-- Distance Units as "Phantom" type
data PDistance a
  = PDistance
  { pdistanceValue :: Double
  }
-- Distance Units as Generalized Type
data GDistance a where
  GDistance :: Double -> GDistance a

GADT Translation

(PDistance => GDistance)

{-# LANGUAGE GADTs #-}

(Generalized Algebraic Datatypes)

data Miles
data Kilometers

data GDistance a where
  GDistance :: Double -> GDistance a

class GConvert a b where
  gconvert :: GDistance a -> GDistance b

instance GConvert Miles Kilometers where
  gconvert (GDistance v) 
    = GDistance (v * 1.60934)

instance GConvert Kilometers Miles where
  gconvert (GDistance v) 
    = GDistance (v * 0.621371)

GADTs: Motivation

No Phantom types 

data ELit 
  = LBool Bool
  | LInt Int

data Expr 
  = ELit ELit
  | EAdd Expr Expr
  | EAnd Expr Expr
  | EEq Expr Expr 
  | EIf Expr Expr Expr
\begin{aligned} x \ :=& \ True \\ \mid& \ False \\ \mid& \ n \\ e \ :=& \ x \\ \mid& \ e + e' \\ \mid& \ e \land e' \\ \mid& \ e == e' \\ \mid& \ \text{if} \ e \ \text{then} \ e' \ \text{else} \ e'' \end{aligned}

GADTs: Motivation

No Phantom types 

eval :: Expr -> Either [Char] ELit
eval expr = 
  case expr of
    ELit lit -> Right lit
    EAdd e1 e2 -> do
      ELit res1 <- eval e1 
      case res1 of
        LInt n1 -> do
          ELit res2 <- eval e2
          case res2 of
            LInt n2 ->
              Right (LInt (n1 + n2))
            LBool _ -> 
              Left "Cannot add EBool"
        LBool _ -> 
          Left "Cannot add EBool"
    ...

Exercise 1

src/Typelevel/Exercises/Intro/GADTs.hs

GADTs: Motivation

Phantom types 

data PLit
  = PBool Bool
  | PInt Int

data PExpr a
  = PLit PLit
  | PAdd (PExpr Int) (PExpr Int)
  | PAnd (PExpr Bool) (PExpr Bool)
  | PEq (PExpr a) (PExpr a)
  | PIf (PExpr Bool) (PExpr a) (PExpr a)

The type parameter 'a' allows us to add extra information about the value at compile time.

GADTs: Motivation

Phantom types 

peval :: PExpr a -> Either [Char] PLit
peval pexpr =
  case pexpr of
    PLit plit -> Right plit
    PAdd pe1 pe2 -> do
      res1 <- peval pe1
      case res1 of
        PBool _ -> Left "Cannot add PBool"
        PInt n1 -> do
          res2 <- peval pe2
          case res2 of
            PBool _ -> Left "Cannot add PBool"
            PInt n2 -> pure (PInt (n1 + n2))
    ...
peval_Example_1 :: Either [Char] PLit
peval_Example_1 =
  peval $
    PAnd 
      (PLit (PBool True)) 
      (PLit (PBool False))

peval_Example_2 :: Either [Char] PLit
peval_Example_2 =
  peval $
    PEq 
      (PLit (PInt 1)) 
      (PLit (PBool True))

GADTs: Motivation

Phantom types 

So how does this help us... ?

GADTs: Solution

data GLit a where
  GBool :: Bool -> GLit Bool
  GInt  :: Int  -> GLit Int

data GExpr a where
  GLit :: GLit a -> GExpr a
  GAdd :: GExpr Int -> GExpr Int -> GExpr Int
  GAnd :: GExpr Bool -> GExpr Bool -> GExpr Bool
  GEq  :: Eq a => GExpr a -> GExpr a -> GExpr Bool
  GIf  :: GExpr Bool -> GExpr a -> GExpr a -> GExpr a

"Correct by Construction"

The return type of GExpr constructors can depend on the types of the constructor fields.

Exercise 2a & 2b

src/Typelevel/Exercises/Intro/GADTs.hs

geval :: GExpr a -> a
geval gexpr =
  case gexpr of
    GLit glit   -> gevalLit glit
    GAdd e1 e2  -> geval e1 + geval e2
    GAnd e1 e2  -> geval e1 && geval e2
    GEq e1 e2   -> geval e1 == geval e2
    GIf c e1 e2 ->
      if (geval c)
        then geval e1
        else geval e2

Exercise 2b: Solution

gevalLit :: GLit a -> a
gevalLit (GBool b) = b
gevalLit (GInt n)  = n

Type-level Programming in Haskell

  • Type-level Functions
  • Value & Type Promotion
  • Higher-Kinded data
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE KindSignatures        #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds             #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE UndecidableInstances  #-}

Language Extensions

{-# LANGUAGE TypeFamilies #-}

The concept of a type family comes from type theory. An indexed type family in type theory is a partial function at the type level. Applying the function to parameters (called type indices) yields a type. Type families permit a program to compute what data constructors it will operate on, rather than having them fixed statically (as with simple type systems) or treated as opaque unknowns (as with parametrically polymorphic types).

 

- GHC Wiki

{-# LANGUAGE TypeFamilies #-}

("Type level functions")

const :: a -> b -> a
const x _ = x

"Value-level" functions

"Type-level" functions

type family Const a b where
  Const a b = a
Prelude> const 42 "Haskell"
42
Prelude> :set -XTypeFamilies
Prelude> :kind! Const Int [Char]
Const Int [Char] :: *
= Int
{-# LANGUAGE TypeFamilies #-}
  • Evaluated at compile type, during type-checking
  • Domain: Types, Codomain: Types
  • *Cannot be partially applied
  • Denoted with type variables and kind signatures
{-# LANGUAGE TypeFamilies #-}

("Type level functions")

{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE TypeFamilies   #-}

module PartialTypeFamilyApply where

data DConst a b = DConst a

-- | We can partially apply a Type constructor...
type family CurryDConst (a :: *) :: (* -> *) where
  CurryDConst Int = DConst Int

-- | But _not_ a type family!
type family TConst a b where TConst a b = a
type family CurryTConst (a :: *) :: (* -> *) where
  CurryTConst Int = TConst Int
/home/thomasd/github/type-as-specifications/src/PartialTypeFamilyApply.hs:15:3: error:
    • The type family ‘TConst’ should have 2 arguments, but has been given 1
    • In the equations for closed type family ‘CurryTConst’
      In the type family declaration for ‘CurryTConst’
   |
15 |   CurryTConst Int = TConst Int
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
{-# LANGUAGE TypeFamilies #-}

("Type level functions")

GHC's kind language is limited:

data True  -- Declares the type 'True'
data False -- Declares the type 'False'

type family Or (b :: *) (b' :: *) :: * where
  Or True _      = True
  Or _ True      = True
  Or False False = False
\{\ *\ ,\ * \rightarrow *\ ,\ * \rightarrow * \rightarrow *\ ,\ (*\ \rightarrow *)\ \rightarrow *\ ,\ \dots \}
{-# LANGUAGE TypeFamilies #-}

("Type level functions")

data True  -- ^ Declares the type 'True'
data False -- ^ Declares the type 'False'

-- | Type-level Boolean Or
type family Or (b :: *) (b' :: *) :: * where
  Or True  _     = True
  Or _     True  = True
  Or False False = False
  Or Int   Char  = Maybe (IO Bool)  
  • Or can be instantiated with any type of kind *
  • Or can return any type of kind *
{-# LANGUAGE DataKinds #-}
  • Values are promoted to types

  • Types are promoted to kinds

-- | Declares the _kind_ Bool 
-- and the _type constructors_ True and False.
data Bool
  = True    
  | False

-- | Type-level Boolean Or
type family Or (b :: Bool) (b' :: Bool) :: Bool where
  Or 'True  _      = 'True
  Or _     'True   = 'True
  Or 'False 'False = 'False

Exercises 1 & 2

src/Typelevel/Exercises/Basics.hs

{-# LANGUAGE DataKinds #-}

Type-level Natural Numbers

Peano Axioms

  1. 0 is a natural number
  2. For every natural number x, x = x
  3. For all natural numbers x and y, if x = y, then y = x.
  4. For all natural numbers x, y and z, if x = y and y = z, then x = z.
  5. For all a and b, if b is a natural number and a = b, then a is also a natural number.
  6. For every natural number n, S(n) is a natural number.
  7. For all natural numbers m and n, m = n if and only if S(m) = S(n).
  8. For every natural number n, S(n) = 0 is false.
{-# LANGUAGE DataKinds #-}

Type-level Natural Numbers

-- | Declares the _kind_ Nat
-- and the _type constructors_ Zero and Succ 
data Nat 
  = Zero
  | Succ Nat

-- | Type-level natural number addition
type family Add (n :: Nat) (m :: Nat) :: Nat where
  Add 'Zero m = m
  Add ('Succ n) m = Succ (Add n m)

Exercises 3 & 4

src/Typelevel/Exercises/Basics.hs

{-# LANGUAGE UndecidableInstances  #-}
/home/thomasd/github/type-as-specifications/src/Typelevel.hs:86:3: error:
    • Illegal nested type family application ‘Add m (Mult n m)’
      (Use UndecidableInstances to permit this)
    • In the equations for closed type family ‘Mult’
      In the type family declaration for ‘Mult’
   |
86 |   Mult ('S n) m = Add m (Mult n m)
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Regarding Exercises 3 & 4:

GHC can't guarantee that this instance definition terminates.

Type-level Natural Numbers

(w/ more ergonomics!)

-- Type aliases for readability
type One   = 'Succ 'Zero
type Two   = 'Succ ('Succ 'Zero )
type Three = 'Succ ('Succ ('Succ 'Zero ))
type Four  = Mult Two Two
type Five  = Add Two Three
type Six   = Add Three Three
type Seven = Add Four Three
type Eight = Mult Four Two
type Nine  = Mult Three Three

But this is annoying...

{-# LANGUAGE TypeOperators #-}
-- Type alias
type (||) a b = Or a b
infixl 7 ||
-- Type family definitions
type family (n :: Nat) :*: (m :: Nat) :: Nat where
  'Zero     :*: m = 'Zero
  ('Succ n) :*: m = m + (n :*: m)

Haskell Wiki:

"There are three kinds of fixity, non-, left- and right-associativity (infix, infixl, and infixr, respectively), and ten precedence levels, 0 to 9 inclusive (level 0 binds least tightly, and level 9 binds most tightly). If the digit is omitted, level 9 is assumed. Any operator lacking a fixity declaration is assumed to be infixl 9"

Type-level Natural Numbers

(w/ even more ergonomics!)

Prelude GHC.TypeNats> :set -XTypeOperators
Prelude GHC.TypeNats> :set -XDataKinds
Prelude GHC.TypeNats> import GHC.TypeNats
Prelude GHC.TypeNats> :kind! 1 + 1 
1 + 1 :: Nat
= 2
Prelude GHC.TypeNats> :kind! 2 ^ 3
2 ^ 3 :: Nat
= 8
Prelude GHC.TypeNats> :kind! 2 <=? 3
2 <=? 3 :: Bool
= 'True
Prelude GHC.TypeNats> :kind! (1 + 3) ^ 2 - 5
(1 + 3) ^ 2 - 5 :: Nat
= 11

* We will use these type-level natural numbers for the remainder of the workshop

Exercise 5

src/Typelevel/Exercises/Basics.hs

So, we want our kinds to be "kind polymorphic".

{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE PolyKinds #-}
  • GHC will automatically infer polymorphic kinds for unannotated kind signatures
  • No quantification for kind-variables
  • Polykinds implies KindSignatures
type family IfThenElse (c :: Bool) (a :: k) (b :: k) where
  IfThenElse 'True a b  = a
  IfThenElse 'False a b = b

Type-Indexed Lists*

  • Length-Indexed Lists
  • Type-level lists
  • Heterogenous Lists
  • Type-safe Extensible Records
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE ConstraintKinds       #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE KindSignatures        #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds             #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE UndecidableInstances  #-}

Language Extensions

Length Indexed Lists

Exercise 1

  • Part A:
    • Define a GADT that encodes a recursive list structure indexed by its length (as type Nat)
  • Part B:
    • Implement the nappend function that appends two length indexed lists
    • Note: You may have trouble with this. There are two solutions.

Type-level Lists

-- Defined in GHC.Types
data [a]
  = []
  | a : [a]

Prelude> :set -XDataKinds -XTypeOperators
Prelude> :t []
[] :: [a]
Prelude> :t 1 : 2 : 3 : []
[1,2,3] :: Num a => [a]
Prelude> :k '[]
'[] :: [k]
Prelude> :kind! 1 ': 2 ': 3 ': '[]
1 ': 2 ': 3 ': '[] :: [ghc-prim-0.5.3:GHC.Types.Nat]
= '[1, 2, 3]

Type-level Lists

-- Defined in GHC.Types
data [a]
  = []
  | a : [a]

Prelude> :set -XDataKinds -XTypeOperators
Prelude> :t []
[] :: [a]
Prelude> :t 1 : 2 : 3 : []
[1,2,3] :: Num a => [a]
Prelude> :k '[]
'[] :: [k]
Prelude> :kind! 1 ': 2 ': 3 ': '[]
1 ': 2 ': 3 ': '[] :: [ghc-prim-0.5.3:GHC.Types.Nat]
= '[1, 2, 3]

Exercise 1

src/Typelevel/Exercises/Lists.hs

Higher-Order Type Families

Higher-Order Type Families

type family Map (f :: k -> j) (xs :: [k]) :: [j] where
  Map f '[] = '[]
  Map f (x ': xs) = f x ': Map f xs

type family Filter (f :: k -> Bool) (xs :: [k]) :: [k] where
  Filter p '[] = '[]
  Filter p (x ': xs) =
    IfThenElse (p x)
      (x ': Filter p xs)
      (Filter p xs)

Heterogenous Lists

{-# LANGUAGE DataKinds      #-}
{-# LANGUAGE GADTs          #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PolyKinds      #-}
{-# LANGUAGE TypeOperators  #-}

data HList (a :: [*]) where
  HNil  :: HList '[]
  HCons :: x -> HList xs -> HList (x ': xs)

Why wouldn't we annotate the kind of `a` as `[k]`, i.e. why isn't the type level list kind polymorphic?

Exercises 2 & 3

src/Typelevel/Exercises/Lists.hs

{-# LANGUAGE FlexibleInstances #-}
.../types-as-specifications/src/Typelevel/Lists.hs:58:10: error:
    • Illegal instance declaration for ‘Show (HList '[])’
        (All instance types must be of the form (T a1 ... an)
         where a1 ... an are *distinct type variables*,
         and each type variable appears at most once in the instance head.
         Use FlexibleInstances if you want to disable this.)
    • In the instance declaration for ‘Show (HList '[])’
   |
58 | instance Show (HList '[]) where
   |          ^^^^^^^^^^^^^^^^

.../types-as-specifications/src/Typelevel/Lists.hs:61:39: error:
    • Illegal instance declaration for ‘Show (HList (x : xs))’
        (All instance types must be of the form (T a1 ... an)
         where a1 ... an are *distinct type variables*,
         and each type variable appears at most once in the instance head.
         Use FlexibleInstances if you want to disable this.)
    • In the instance declaration for ‘Show (HList (x : xs))’
   |
61 | instance (Show x, Show (HList xs)) => Show (HList (x ': xs)) where
   | 
{-# LANGUAGE ConstraintKinds #-}
  • Any type of kind Constraint can be used as a Constraint 
  • Tuples where each element is of kind Constraint can be used as a Constraint
type ShowNum a = (Show a, Num a)

type family Typ a b :: Constraint
type instance Typ Int  b = Show b
type instance Typ Bool b = Num b
{-# LANGUAGE ConstraintKinds #-}
type family All (c :: k -> Constraint) (xs :: [k]) :: Constraint where
  All _ '[] = ()
  All c (x ': xs) = (c x, All c xs)
type family IsElem (a :: k) (xs :: [k]) :: Bool where
  IsElem a '[] = 'False
  IsElem a (a ': xs) = 'True
  IsElem a (x ': xs) = Elem a xs

type family Elem (a :: k) (xs :: [k]) :: Constraint where
  Elem a xs = IsElem ~ 'True

Exercise 4

src/Typelevel/Exercises/Lists.hs

Live Programming

  • Implement a type family that appends two HList values

    • Data.Type.Equality

    • Prove type equality at the value level

  • Type-safe Extensible Records
    • "access" record field
    • "extend" record

Types as Specifications:

By Thomas Dietert

Types as Specifications:

  • 780