Thomas Dietert
Lambda Conf 2019
Boulder, CO
.../types-as-specifications $ stack setup
.../types-as-specicications $ stack build
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"
Types as program Specifications
What does your program mean?
Decision Tables
Business Requirements
RFCs
Documentation
Types
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 describe data
types describe values
kinds describe types
$$ Int = \{..., -1, 0, 1, ... 9223372036854775807 \}$$
$$17 = \text{<Language Specific Memory Layout>}$$
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
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
Prelude> :kind Int
Int :: *
Prelude> :kind Maybe
Maybe :: * -> *
Prelude> :kind Either
Either :: * -> * -> *
Prelude> :kind Either Bool
Either Bool :: * -> *
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
:: (* -> * -> *)
-> (* -> *)
-> *
-> *
-> *
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
}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GADTs #-}
-- Example:
data Proxy a = Proxy
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!"
-- 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)
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)
{-# 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
-- | 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
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)
data ELit
= LBool Bool
| LInt Int
data Expr
= ELit ELit
| EAdd Expr Expr
| EAnd Expr Expr
| EEq Expr Expr
| EIf Expr Expr Expr
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"
...
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.
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))
So how does this help us... ?
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
The return type of GExpr constructors can depend on the types of the constructor fields.
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
gevalLit :: GLit a -> a
gevalLit (GBool b) = b
gevalLit (GInt n) = n
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# 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 #-}
const :: a -> b -> a
const x _ = x
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 #-}
{-# LANGUAGE TypeFamilies #-}
{-# 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 #-}
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
{-# LANGUAGE TypeFamilies #-}
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)
{-# 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
{-# LANGUAGE DataKinds #-}
Peano Axioms
{-# LANGUAGE DataKinds #-}
-- | 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)
{-# 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)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
GHC can't guarantee that this instance definition terminates.
(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
{-# 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"
(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
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE PolyKinds #-}
type family IfThenElse (c :: Bool) (a :: k) (b :: k) where
IfThenElse 'True a b = a
IfThenElse 'False a b = b
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- 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]
-- 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 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)
{-# 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?
{-# 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 #-}
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
Implement a type family that appends two HList values
Data.Type.Equality
Prove type equality at the value level