
Gabriella Gonzalez’s September 2 Haskell essay shows how Church-encoded booleans, RebindableSyntax, and ordinary type inference can preserve different result types through one conditional.
if must return the same type—and turns it into a small lesson in information flow: encode booleans as functions, let RebindableSyntax redirect if to a local function, and let Hindley-Milner inference record which branch type reaches the result. 1{-# LANGUAGE RebindableSyntax #-}
example bool = if bool then 5 else "hi!"Int when bool is true and a String when bool is false. A conventional Haskell if expression uses one result type for both branches, so the compiler tries to make 5 and "hi!" share a type. The numeric literal then produces an error because the compiler is trying to treat a String as a numeric type. 1RebindableSyntax extension makes a conditional mean ifThenElse condition thenBranch elseBranch, using whichever ifThenElse is in scope. The extension also implies NoImplicitPrelude, and GHC warns that the feature has fewer checks than ordinary syntax. 2ifThenElse condition thenBranch elseBranch =
condition thenBranch elseBranchcondition should be. Gonzalez answers that question with Church encoding.type Bool = forall a. a -> a -> a
true thenBranch elseBranch = thenBranch
false thenBranch elseBranch = elseBranchtrue receives two branches and returns the first. The encoded false receives the same two branches and returns the second. The boolean has become a pre-formed conditional: the function already knows which branch to select, while the caller supplies the branch values. 1a in type Bool = forall a. a -> a -> a forces both branches and the result to share one type. A function such as this one works:ifThenElse :: Bool -> a -> a -> a
ifThenElse condition thenBranch elseBranch =
condition thenBranch elseBrancha represents one type for all three positions. 1RebindableSyntax connects this function to familiar Haskell spelling. With the extension enabled, the compiler treatsif condition then thenBranch else elseBranchifThenElse that the module provides. The syntax still looks like an if; its type-checking and execution follow the replacement function. 2true thenBranch elseBranch = thenBranch
false thenBranch elseBranch = elseBranch
ifThenElse condition thenBranch elseBranch =
condition thenBranch elseBranchtrue :: thenBranch -> elseBranch -> thenBranch
false :: thenBranch -> elseBranch -> elseBranchtype Bool thenBranch elseBranch result =
thenBranch -> elseBranch -> result
ifThenElse
:: Bool thenBranch elseBranch result
-> thenBranch
-> elseBranch
-> resultresult parameter records the information that an ordinary Bool hides. A true value has the shape Bool thenBranch elseBranch thenBranch; a false value has the shape Bool thenBranch elseBranch elseBranch. Applying ifThenElse to true unifies the result with the thenBranch type. Applying it to false unifies the result with the elseBranch type. 1
example :: (bool :: Bool) -> if bool then Int else Stringexample function can therefore keep the two result types separate:example :: Bool Int String result -> result
example bool = if bool then 5 else "hi!"example true gives the inferred type Int. Calling example false gives the inferred type String. The implementation still has the surface shape of an ordinary conditional because RebindableSyntax directs the syntax to the generalized ifThenElse function. 1not
:: Bool thenBranch elseBranch result
-> Bool elseBranch thenBranch result
not bool thenBranch elseBranch =
bool elseBranch thenBranchelse branch immediately:(&&)
:: Bool intermediateResult elseBranch result
-> Bool thenBranch elseBranch intermediateResult
-> Bool thenBranch elseBranch resultintermediateResult variable connects the result of the second boolean to the branch consumed by the first boolean. Disjunction has the same structure with the first boolean's thenBranch on the early path:(||)
:: Bool thenBranch intermediateResult result
-> Bool thenBranch elseBranch intermediateResult
-> Bool thenBranch elseBranch resultthenBranch, elseBranch, or intermediateResult. 1true and false loses the distinction between their selected branches:bools :: [Bool result result result]
bools = [true, false]toString returns String for both cases:toString :: Bool String String result -> result
toString bool = if bool then "true" else "false"
map toString [true, false]example function has different branch types, so the same list cannot satisfy its input type:map example [true, false]Int and another that produces String. A homogeneous list has no single result type to use for both calls. The refinement therefore degrades to Bool result result result, and a consumer must return one common type. 1AssertionFailed type:data AssertionFailed = AssertionFailed
type family Successful result :: Constraint where
Successful AssertionFailed =
Unsatisfiable (\"Assertion failed\")
Successful _ = ()
assert
:: Successful result
=> Bool result AssertionFailed result
-> result
-> resulttrue condition produces a normal result and satisfies Successful. A statically known false condition produces AssertionFailed, which the type family turns into a compiler error. The runtime implementation can remain a simple conditional; the type-level result makes the known failure visible while the program is being checked. 1if: DataKinds, TypeFamilies, and the type-error facilities from GHC.TypeLits. The extra extensions matter because the program is asking the compiler to turn a type-level result into a diagnostic. The core trick remains the same: a function's result type records which branch information reaches the end. 1RebindableSyntax changes several built-in forms and replaces the ordinary Prelude bindings with whatever names are in scope. GHC describes the extension as experimental and recommends Core Lint when a program relies on the desugared result. A local module that uses the trick therefore takes responsibility for its custom syntax and its altered assumptions about literals, conditionals, and imports. 2"Church encoding" is a technique for encoding data structures and operations on those data structures using pure functions and nothing else.— Gabriella Gonzalez, "Dependent if expressions without dependent types".
The reason this all works is because our newBooltype tracks information flow at the type-level.— Gabriella Gonzalez, "Dependent if expressions without dependent types".
This is all possible with ≈20 lines of ordinary functional code making use of vanilla type system features.— Gabriella Gonzalez, "Dependent if expressions without dependent types".
haskellforall.com
ghc.gitlab.haskell.org
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.