Here's an untyped lambda calculus whose terms are indexed by their free variables. I'm using the singletons
library for singleton values of type-level strings.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
import Data.Singletons
import Data.Singletons.TypeLits
data Expr (free :: [Symbol]) where
Var :: Sing a -> Expr '[a]
Lam :: Sing a -> Expr as -> Expr (Remove a as)
App :: Expr free1 -> Expr free2 -> Expr (Union free1 free2)
A Var
introduces a free variable. A lambda abstraction binds a variable which appears free in the body (if there's one which matches). Applications join up the free variables of the two parts of the expression, removing duplicates (so the free variables of x y
are x
and y
, while the free variables of x x
are just x
). I wrote out those type families:
type family Remove x xs where
Remove x '[] = '[]
Remove x (x ': xs) = Remove x xs
Remove x (y ': xs) = y ': Remove x xs
type family Union xs ys where
Union xs ys = Nub (xs :++ ys)
type family xs :++ ys where
'[] :++ ys = ys
(x ': xs) :++ ys = x ': (xs :++ ys)
type family Nub xs where
Nub xs = Nub' '[] xs
type family Nub' seen xs where
Nub' seen '[] = '[]
Nub' seen (x ': xs) = If (Elem x seen) (Nub' seen xs) (Nub' (x ': seen) (x ': xs))
type family If c t f where
If True t f = t
If False t f = f
type family Elem x xs where
Elem x '[] = False
Elem x (x ': xs) = True
Elem x (y ': xs) = Elem x xs
I tested this out at the interactive prompt:
ghci> :t Var (sing :: Sing "x")
Var (sing :: Sing "x") :: Expr '["x"] -- good
ghci> :t (Lam (sing :: Sing "x") (Var (sing :: Sing "x")))
(Lam (sing :: Sing "x") (Var (sing :: Sing "x")))
:: Expr (Remove "x" '["x"]) -- not so good
I was surprised to learn that the type of the identity function \x. x
is Expr (Remove "x" '["x"])
, not Expr '[]
. GHC seems unwilling to evaluate the type family Remove
.
I experimented a little more and learned that it's not a problem with my type family per se - GHC is happy to reduce it in this case:
ghci> :t (Proxy :: Proxy (Remove "x" '["x"]))
(Proxy :: Proxy (Remove "x" '["x"])) :: Proxy '[]
So: Why won't GHC reduce Remove "x" '["x"]
to '[]
when I query the type of my GADT? In general, when will-or-won't the type checker evaluate a type family? Are there heuristics I can use to avoid being surprised by its behaviour?
It works. GHC seems to be just lazy.
λ *Main > :t (Lam (Proxy :: Proxy "x") (Var (Proxy :: Proxy "x")))
(Lam (Proxy :: Proxy "x") (Var (Proxy :: Proxy "x")))
:: Expr (Remove "x" '["x"])
λ *Main > :t (Lam (Proxy :: Proxy "x") (Var (Proxy :: Proxy "x"))) :: Expr '[]
(Lam (Proxy :: Proxy "x") (Var (Proxy :: Proxy "x"))) :: Expr '[]
:: Expr '[]
λ *Main > :t (Lam (Proxy :: Proxy "x") (Var (Proxy :: Proxy "x"))) :: Expr '["x"]
<interactive>:1:2:
Couldn't match type ‘'[]’ with ‘'["x"]’
Expected type: Expr '["x"]
Actual type: Expr (Remove "x" '["x"])
In the expression:
(Lam (Proxy :: Proxy "x") (Var (Proxy :: Proxy "x"))) ::
Expr '["x"]
I changed definitions so there isn't dependency on singletons library (easier to test in ad-hoc):
{-# LANGUAGE TypeOperators, DataKinds, TypeFamilies, GADTs #-}
import Data.Proxy
import GHC.TypeLits
type family Remove (x :: Symbol) (xs :: [Symbol]) where
Remove x '[] = '[]
Remove x (x ': xs) = Remove x xs
Remove x (y ': xs) = y ': Remove x xs
data Expr (free :: [Symbol]) where
Var :: KnownSymbol a => Proxy a -> Expr '[a]
Lam :: KnownSymbol a => Proxy a -> Expr as -> Expr (Remove a as)
-- App :: Expr free1 -> Expr free2 -> Expr (Union free1 free2)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With