Ghost Hunting the Perfect API
( with departed proofs )
Unsafe API
Patterns
>:t head
head :: [a] -> a
> head [1,2,3]
1
> head []
*** Exception: Prelude.head: empty list
> head []
-1
Safe API
Patterns
head :: [a] -> a
headMaybe :: [a] -> Maybe a
headMaybe (x:xs) = Just x
headMaybe [] = Nothing
data NonEmpty a = a :| [a]
safeHead :: NonEmpty a -> a
safeHead (x :| xs) = x
Responsibility
Our choice:
- Handle it
- Pass it on
People will pass responsibility in the same direction the code they call does.
Responsibility
When we restrict what we can do, it’s easier to understand what we can do.
identity :: a -> a
take :: Int -> [a] -> [a]
length :: [a] -> Int
Case study
-- merge 2 already sorted lists
unsafeMergeBy
:: (a -> a -> Ordering) -- the comparator
-> [a] -- first list
-> [a] -- second list
-> [a] -- merged list
> x = sortBy compare [4,3,1]
> y = sortBy compare [16,5,6]
> x
[1,3,4]
> y
[5,6,16]
> unsafeMergeBy compare x y
[1,3,4,5,6,16]
> unsafeMergeBy (comparing Down) x y
[5,6,16,1,3,4]
Case study
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi nec metus justo. Aliquam erat volutpat.
-- merge 2 already sorted lists
mergeByMaybe
:: (a -> a -> Ordering) -- the comparator
-> [a]
-> [a]
-- a merged list
-- or none if the list passed
-- was not sorted
-> Maybe [a]
> x = sortBy compare [4,3,1]
> y = sortBy compare [16,5,6]
> fromJust $ mergeByMaybe compare x y
[1,3,4,5,6,16]
In the Wild
- Hackage finds 2000 cases of lookup, followed by fromJust
- Lookup tries to be good, but user has reason to believe key is in map.
How can we reflect constraints on function input values in the function type?
newtype Named name a = Named a
type a ~~ name = Named name a
name :: a -> (forall name. a ~~ name -> t) -> t
name x k = k (coerce x)
class The d a | d -> a where
the :: d -> a
default the :: Coercible d a => d -> a
the = coerce
newtype Named name a = Named a
type a ~~ name = Named name a
phantom type-level names for values
class The d a | d -> a where
the :: d -> a
default the :: Coercible d a => d -> a
the = coerce
A way to introduce names
name :: a -> (forall name. a ~~ name -> t) -> t
name x k = k (coerce x)
unwrap named values
newtype SortedBy name a = SortedBy a
instance The (SortedBy name a) a
import qualified Lists as L
sortBy :: ((a -> a -> Ordering) ~~ comp)
-> [a]
-> SortedBy comp [a]
sortBy comp xs = coerce (L.sortBy (the comp) xs)
mergeBy :: ((a -> a -> Ordering) ~~ comp)
-> SortedBy comp [a]
-> SortedBy comp [a]
-> SortedBy comp [a]
mergeBy comp xs ys =
coerce (L.mergeBy (the comp) (the xs) (the ys))
How can we reflect constraints on function input values in the function type?
> name compare $ \gt -> do
let xs' = sortBy gt xs
ys' = sortBy gt ys
print (the (mergeBy gt xs' ys'))
> x = sortBy compare [4,3,1]
> y = sortBy compare [16,5,6]
> unsafeMergeBy (comparing Down) x y
[5,6,16,1,3,4]
> x = sortBy compare [4,3,1]
> y = sortBy compare [16,5,6]
> fromJust $ mergeByMaybe (comparing Down) x y
*** Exception: Maybe.fromJust: Nothing
Benefits
- mergeBy cannot be called with a different comparator then the sorted lists were created with
- allow the library user to decide when and how to validate api preconditions are met
- achieved many of the benefits of dependent and refinement types, whilst only requiring some minor and well understood extensions to haskell2010
> name compare $ \gt -> do
let xs' = sortBy gt xs
ys' = sortBy gt ys
print (the (mergeBy gt xs' ys'))
An additional example
minimum_O1 :: SortedBy comp [a] -> Maybe a
minimum_O1 xs = case the xs of
[] -> Nothing
(x:_) -> Just x
Ghostly Proofs
data Proof p = QED
axiom :: String -> Proof p
axiom reason = QED
newtype Rev xs = Rev ()
rev_rev
:: Proof (Rev (Rev xs) == xs)
rev_rev =
axiom "reverse reverse is identity"
reverse
:: ([a] ~~ xs)
-> ([a] ~~ Rev xs)
reverse xs =
coerce (P.reverse (the xs))
data p == q
Takeaways
-
Use existential names to discuss values at the type level
-
No runtime overhead
-
Give user combinators and proofs to construct their own safety arguments
-
Is it useful?
-
maybe, it's one approach
-
Links
Takeaways
-
Use existential names to discuss values at the type level
-
No runtime overhead
-
Give user combinators and proofs to construct their own safety arguments
-
Is it useful?
-
maybe, it's one approach
-
Simplified Example
newtype Rev xs = Rev ()
reverse
:: ([a] ~~ xs)
-> ([a] ~~ Rev xs)
reverse xs =
coerce (P.reverse (the xs))
rev_rev
:: Proof (Rev (Rev xs) == xs)
rev_rev =
axiom "reverse reverse is identity"
Ghostly Proofs
data Proof p = QED
axiom :: Proof p
axiom = QED
data p || q
data p && q
data p == q
andElimL
:: Proof (p && q)
-> Proof p
or_introL
:: p
-> Proof (p || q)
-- sitting in phantom type variables- no constructors needed
Usage
data IsCons xs
data IsNil xs
pattern IsCons
:: Proof (IsCons xs)
-> ([a] ~~ xs)
pattern IsNil
:: Proof (IsNil xs)
-> ([a] ~~ xs)
head
:: ([a] ~~ xs ::: IsCons xs)
-> a
head xs =
Prelude.head (the xs)
name [1,3] $ \xs -> case xs of
IsCons proof ->
print (head (xs ...proof))
IsNil proof -> print “nada”
Other lemmas
rev_cons
:: Proof (IsCons xs)
-> Proof (IsCons (Reverse xs))
rev_cons _ = axiom
name xs $ \xs -> case xs of
IsCons proof ->
print (head (xs ...proof))
print (head (reverse xs ...rev_cons proof))
newtype Reverse xs
= Reverse Defn
reverse
:: ([a] ~~ xs)
-> ([a] ~~ Reverse xs)
reverse xs =
defn
(Prelude.reverse (the xs))
Questions
- How do we decide which props to get newtype wrappers?
-
How do we handle props involving multiple values, eg relationships between values?
Safer APIs with Ghosts of Departed Proofs
By ..
Safer APIs with Ghosts of Departed Proofs
- 5,312