Singletons and You Justin Le https://blog.jle.im (justin@jle.im) - - PowerPoint PPT Presentation

singletons and you
SMART_READER_LITE
LIVE PREVIEW

Singletons and You Justin Le https://blog.jle.im (justin@jle.im) - - PowerPoint PPT Presentation

Singletons and You Justin Le https://blog.jle.im (justin@jle.im) Lambdaconf 2017, May 27, 2017 Preface Slide available at https://talks.jle.im/lambdaconf- 2017/singletons/singleton-slides.html. Preface Slide available at


slide-1
SLIDE 1

Singletons and You

Justin Le https://blog.jle.im (justin@jle.im) Lambdaconf 2017, May 27, 2017

slide-2
SLIDE 2

Preface

Slide available at https://talks.jle.im/lambdaconf- 2017/singletons/singleton-slides.html.

slide-3
SLIDE 3

Preface

Slide available at https://talks.jle.im/lambdaconf- 2017/singletons/singleton-slides.html. GHC extensions (potentially) used: {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeInType #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} import Data.Kind

  • - to get type Type = *

import Data.Singletons

slide-4
SLIDE 4

Safety with Phantom Types

data DoorState = Opened | Closed | Locked deriving (Show, Eq) data Door (s :: DoorState) = UnsafeMkDoor

  • - alternatively

data Door :: DoorState -> Type where UnsafeMkDoor :: Door s

slide-5
SLIDE 5

Other similar examples

◮ State machines (socket connections, file handles,

  • pened/closed)

◮ Refinement types ◮ “Tagged” types (santized/unsantized strings)

slide-6
SLIDE 6

Phantom types in action

closeDoor :: Door 'Opened -> Door 'Closed closeDoor UnsafeMkDoor = UnsafeMkDoor

slide-7
SLIDE 7

Phantom types in action

closeDoor :: Door 'Opened -> Door 'Closed closeDoor UnsafeMkDoor = UnsafeMkDoor

  • penDoor

:: Door 'Closed -> Door 'Opened

  • penDoor UnsafeMkDoor = UnsafeMkDoor
slide-8
SLIDE 8

Phantom types in action

doorStatus :: Door s -> DoorState doorStatus = -- ???? We have a problem.

slide-9
SLIDE 9

Phantom types in action

doorStatus :: Door s -> DoorState doorStatus = -- ???? We have a problem. doorStatus :: Door s -> DoorState doorStatus UnsafeMkDoor = -- s ???

slide-10
SLIDE 10

More Problems

initalizeDoor :: DoorStatus -> Door s initializeDoor = \case Opened -> UnsafeMkDoor Closed -> UnsafeMkDoor Locked -> UnsafeMkDoor

slide-11
SLIDE 11

More Problems

initalizeDoor :: DoorStatus -> Door s initializeDoor = \case Opened -> UnsafeMkDoor Closed -> UnsafeMkDoor Locked -> UnsafeMkDoor Neat, but does this work?

slide-12
SLIDE 12

More Problems

ghci> :t initializeDoor Opened :: Door 'Closed initializeDoor Opened :: Door 'Closed Oops.

slide-13
SLIDE 13

The Fundamental Issue in Haskell

◮ In Haskell, types only exist at compile-time. They are erased

at runtime.

slide-14
SLIDE 14

The Fundamental Issue in Haskell

◮ In Haskell, types only exist at compile-time. They are erased

at runtime.

◮ This is a good thing for performance! Types incur no runtime

  • verhead!
slide-15
SLIDE 15

The Fundamental Issue in Haskell

◮ In Haskell, types only exist at compile-time. They are erased

at runtime.

◮ This is a good thing for performance! Types incur no runtime

  • verhead!

◮ But it makes functions like doorStatus fundamentally

unwritable without fancy typeclasses.

slide-16
SLIDE 16

The Fundamental Issue in Haskell

◮ In Haskell, types only exist at compile-time. They are erased

at runtime.

◮ This is a good thing for performance! Types incur no runtime

  • verhead!

◮ But it makes functions like doorStatus fundamentally

unwritable without fancy typeclasses.

◮ . . . or does it?

slide-17
SLIDE 17

The Singleton Pattern

data SingDS :: DoorStatus -> Type where SOpened :: SingDS 'Opened SClosed :: SingDS 'Closed SLocked :: SingDS 'Locked Creates three constructors: SOpened :: SingDS 'Opened SClosed :: SingDS 'Closed SLocked :: SingDS 'Locked

slide-18
SLIDE 18

The Singleton Pattern

◮ A singleton is a type that has exactly one inhabited value.

slide-19
SLIDE 19

The Singleton Pattern

◮ A singleton is a type that has exactly one inhabited value. ◮ There is only one value of type SingDS 'Opened, and only

  • ne value of type SingDS 'Closed.
slide-20
SLIDE 20

The Singleton Pattern

◮ A singleton is a type that has exactly one inhabited value. ◮ There is only one value of type SingDS 'Opened, and only

  • ne value of type SingDS 'Closed.

◮ The constructor that a SingDS s uses reveals to us what s is.

slide-21
SLIDE 21

The Singleton Pattern

With our new singletons, we can essentially pattern match on types: showSingDS :: SingDS s -> String showSingDS = \case SOpened -> "Opened" SClosed -> "Closed" SLocked -> "Locked"

slide-22
SLIDE 22

The Singleton Pattern

With our new singletons, we can essentially pattern match on types: showSingDS :: SingDS s -> String showSingDS = \case SOpened -> "Opened" SClosed -> "Closed" SLocked -> "Locked" Alone like this, it’s a bit boring. We didn’t need GADTs for this.

slide-23
SLIDE 23

Door Status

doorStatus' :: SingDS s -> Door s -> DoorState doorStatus' = \case SOpened -> \_ -> "Door is opened" SClosed -> \_ -> "Door is closed" SLocked -> \_ -> "Door is locked"

◮ GADT-ness allows us to enforce that the s in SingDS s is the

same as the s in our Door.

slide-24
SLIDE 24

Door Status

doorStatus' :: SingDS s -> Door s -> DoorState doorStatus' = \case SOpened -> \_ -> "Door is opened" SClosed -> \_ -> "Door is closed" SLocked -> \_ -> "Door is locked"

◮ GADT-ness allows us to enforce that the s in SingDS s is the

same as the s in our Door.

◮ Singleton property means that SingDS s has a one-to-one

correspondence with its constructors.

slide-25
SLIDE 25

Door Status

doorStatus' :: SingDS s -> Door s -> DoorState doorStatus' = \case SOpened -> \_ -> "Door is opened" SClosed -> \_ -> "Door is closed" SLocked -> \_ -> "Door is locked"

◮ GADT-ness allows us to enforce that the s in SingDS s is the

same as the s in our Door.

◮ Singleton property means that SingDS s has a one-to-one

correspondence with its constructors.

◮ Pattern matching on that single constructor reveals to us the

type of Door.

slide-26
SLIDE 26

Implicit Passing

class SingDSI s where singDS :: SingDSI s instance SingDSI 'Opened where singDS = SOpened instance SingDSI 'Closed where singDS = SClosed instance SingDSI 'Locked where singDS = SLocked

slide-27
SLIDE 27

Implicit Passing

class SingDSI s where singDS :: SingDSI s instance SingDSI 'Opened where singDS = SOpened instance SingDSI 'Closed where singDS = SClosed instance SingDSI 'Locked where singDS = SLocked doorStatus :: SingDSI s => Door s -> DoorState doorStatus = doorStatus' singDS

slide-28
SLIDE 28

Implicit Passing

class SingDSI s where singDS :: SingDSI s instance SingDSI 'Opened where singDS = SOpened instance SingDSI 'Closed where singDS = SClosed instance SingDSI 'Locked where singDS = SLocked doorStatus :: SingDSI s => Door s -> DoorState doorStatus = doorStatus' singDS ghci> doorStatus (UnsafeMkDoor :: Door 'Locked) Door is locked!

slide-29
SLIDE 29

Initialize Door

initializeDoor' :: SingDS s -> Door s initializeDoor' _ _ = UnsafeMkDoor

slide-30
SLIDE 30

Initialize Door

initializeDoor' :: SingDS s -> Door s initializeDoor' _ _ = UnsafeMkDoor ghci> :t initializeDoor' SOpened initializeDoor SOpened :: Door 'Opened ghci> :t initializeDoor' SClosed initializeDoor SClosed :: Door 'Closed

slide-31
SLIDE 31

Initialize Door

Implicit passing style: initializeDoor :: SingDSI s => Door s initializeDoor = initializeDoor' singDS

slide-32
SLIDE 32

SingDS vs. SingDSI

◮ Really, SingDS s -> is the same as SingDSI s =>

slide-33
SLIDE 33

SingDS vs. SingDSI

◮ Really, SingDS s -> is the same as SingDSI s => ◮ The two are the same way of providing the same information to

the compiler, and at runtime.

slide-34
SLIDE 34

SingDS vs. SingDSI

◮ Really, SingDS s -> is the same as SingDSI s => ◮ The two are the same way of providing the same information to

the compiler, and at runtime.

◮ We can use the two styles interchangebly.

slide-35
SLIDE 35

SingDS vs. SingDSI

◮ Really, SingDS s -> is the same as SingDSI s => ◮ The two are the same way of providing the same information to

the compiler, and at runtime.

◮ We can use the two styles interchangebly. ◮ One is explicitly passing the type, the other is explicitly

passing the type.

slide-36
SLIDE 36

Ditching the phantom

Sometimes we don’t care about what the status of our door is, and we want the type system to relax.

slide-37
SLIDE 37

Ditching the phantom

Sometimes we don’t care about what the status of our door is, and we want the type system to relax. This is essentially the same as saying that the status of our door is a runtime property that we do not want to (or sometimes can’t) check at compile-time.

slide-38
SLIDE 38

Ditching the phantom

data SomeDoor :: Type where MkSomeDoor :: SingDS s => Door s -> SomeDoor

slide-39
SLIDE 39

Ditching the phantom

data SomeDoor :: Type where MkSomeDoor :: SingDS s => Door s -> SomeDoor ghci> let myDoor = MkSomeDoor (initializeDoor SOpened) ghci> :t myDoor myDoor :: SomeDoor ghci> case myDoor of MkSomeDoor d -> doorStatus d Door is opened.

slide-40
SLIDE 40

Runtime-deferred types

initializeSomeDoor :: DoorStatus -> SomeDoor initializeSomeDoor = \case Opened -> SomeDoor (initialiseDoor' SOpened) Closed -> SomeDoor (initialiseDoor' SClosed) Locked -> SomeDoor (initialiseDoor' SLocked)

slide-41
SLIDE 41

Runtime-deferred types

initializeSomeDoor :: DoorStatus -> SomeDoor initializeSomeDoor = \case Opened -> SomeDoor (initialiseDoor' SOpened) Closed -> SomeDoor (initialiseDoor' SClosed) Locked -> SomeDoor (initialiseDoor' SLocked) ghci> let myDoor = initializeSomeDoor Locked ghci> :t myDoor myDoor :: SomeDoor ghci> case myDoor of MkSomeDoor d -> doorStatus d Door is locked.

slide-42
SLIDE 42

The Singletons Library

The singletons library provides a unified framework for creating and working with singletons for different types (not just DoorStatus), and also for functions on those types. http://hackage.haskell.org/package/singletons

slide-43
SLIDE 43

The singletons way

$(singletons [d| data DoorState = Opened | Closed | Locked deriving (Show, Eq) |])

slide-44
SLIDE 44

The singletons way

$(singletons [d| data DoorState = Opened | Closed | Locked deriving (Show, Eq) |]) This creates three types and three constructors:

  • - not the actual code, but essentially what happens

data Sing :: DoorState -> Type where SOpened :: Sing 'Opened SClosed :: Sing 'Closed SLocked :: Sing 'Locked Sing is a poly-kinded type constructor (family):

slide-45
SLIDE 45

The singletons way

And also instance SingI 'Opened where sing = SOpened instance SingI 'Closed where sing = SClosed instance SingI 'Locked where sing = SLocked (SingI is a poly-kinded typeclass)

slide-46
SLIDE 46

Examples

STrue :: Sing 'True SJust SFalse :: Sing ('Just 'True) SOpened `SCons` SClosed `SCons` SNil :: Sing '[ 'Opened, 'Closed ghci> sing :: Sing 'True' STrue

slide-47
SLIDE 47

Other stuff created from the library

Some other convenient features: ghci> fromSing SOpened Opened ghci> let s = toSing Opened ghci> :t s s :: SomeSing DoorStatus ghci> case s of SomeSing SOpened -> "Opened." SomeSing SClosed -> "SClosed." SomeSing SLocked -> "SLocked."

slide-48
SLIDE 48

Non-trivial type logic

knock :: Door s -> IO () knock = -- ?? We want to allow the user to knock on a closed or locked door, but not an opened door.

slide-49
SLIDE 49

Non-trivial type logic

knock :: Door s -> IO () knock = -- ?? We want to allow the user to knock on a closed or locked door, but not an opened door. We can do this simple case using pattern matching, but it’s not always feasible or scalable. We want to define a type relationship that can be used by potentially many functions.

slide-50
SLIDE 50

Singletons to the Rescue

$(singletons [d| canKnock :: DoorState -> Bool canKnock Opened = False canKnock Closed = True canKnock Locked = True |])

slide-51
SLIDE 51

Singletons to the Rescue

$(singletons [d| canKnock :: DoorState -> Bool canKnock Opened = False canKnock Closed = True canKnock Locked = True |]) knock :: (CanKnock s ~ True) => Door s -> IO () knock _ = putStrLn "knock knock!"

slide-52
SLIDE 52

Singletons to the Rescue

$(singletons [d| canKnock :: DoorState -> Bool canKnock Opened = False canKnock Closed = True canKnock Locked = True |]) knock :: (CanKnock s ~ True) => Door s -> IO () knock _ = putStrLn "knock knock!" ghci> knock (initializeDoor SOpened) Compile Error!!!! ghci> knock (initializeDoor SClosed) knock knock!

slide-53
SLIDE 53

Singletons to the Rescue

tryKnock' :: Sing s -> Door s -> IO () tryKnock' s = case sCanKnock s of STrue

  • > knock

SFalse -> \_ -> putStrLn "Cannot knock door!" tryKnock :: SingI s => Door s -> IO () tryKnock = tryKnock' sing

slide-54
SLIDE 54

Singletons to the Rescue

tryKnock' :: Sing s -> Door s -> IO () tryKnock' s = case sCanKnock s of STrue

  • > knock

SFalse -> \_ -> putStrLn "Cannot knock door!" tryKnock :: SingI s => Door s -> IO () tryKnock = tryKnock' sing ghci> tryKnock (initializeDoor SOpened) Cannot knock door! ghci> tryKnock (initializeDoor SClosed) knock knock!

slide-55
SLIDE 55

Vectors

$(singletons [d| data N = Z | S N |]) data Vec :: N -> Type -> Type where VNil :: Vec Z a (:*) :: a -> Vec n a -> Vec (S n) a infixr 5 :*

slide-56
SLIDE 56

The Types demand it

replicateV' :: Sing n

  • > a
  • > Vec n a

replicateV' = \case SZ

  • > \_ -> VNil

SS n -> \x -> x :* replicateV' n x

slide-57
SLIDE 57

The Types demand it

replicateV' :: Sing n

  • > a
  • > Vec n a

replicateV' = \case SZ

  • > \_ -> VNil

SS n -> \x -> x :* replicateV' n x replicateV :: SingI n => a

  • > Vec n a

replicateV = replicateV' sing

slide-58
SLIDE 58

Thank you!

◮ Further confusion:

https://blog.jle.im/entry/verified-instances-in-haskell.html