argument permutation and fundeps
C T McBride
C.T.McBride@durham.ac.uk
Thu, 10 May 2001 13:13:01 +0100 (BST)
Hi
This is a long message, containing a program which makes heavy use of
type classes with functional dependencies, and a query about how the
typechecker treats them. It might be a bit of an effort, but I'd be
grateful for any comment and advice more experienced Haskellers can
spare the time to give me. Er, this ain't no undergrad homework...
I'm a dependently typed programmer and a bit of an old
Lisphead. What's common to both is that it's fairly easy to write
`program reconstruction operations', like the argument permutation
function (of which flip is an instance), specified informally thus:
Given a permutation p on [1..n] and an n-ary function f,
permArg p f x_p(1) .. x_p(n) = f x_1 .. x_n
It's easy in Lisp because you can just compute the relevant
lambda-expression for permArg p f syntactically, then use it as a
program: of course, there's no type system to make sure your p is
really a perm and your f has enough arguments. To give permArg a
precise type, we have to compute an n-ary permutation of the argument
types for an n-ary function space. That is, we need a representation
of permutations p from which to compute
(1) the type of the permutation function
(2) the permutation function itself
This isn't that hard with dependent types, because we can use the same
kinds of data and program as easily at the level of types as we can at the
level of terms.
I'm relatively new to Haskell: what attracted me is that recent extensions
to the language have introduced a model of computation at the type level
via multi-parameter classes with functional dependencies. Although
type-level Haskell programming is separate from, different to, and not
nearly as powerful as term-level Haskell programming, it's still possible
to do some pretty interesting stuff: here's permArg as a Haskell program...
Although we can't compute with types over a data encoding of
permutations, we can use type classes to make `fake' datatypes one
level up. Here's a class representing the natural numbers. Each type
in the class has a single element, which we can use to tell the
typechecker which instance of the class we mean.
> class Nat n
> data O = O
> instance Nat O
> data (Nat n) => S n = S n
> instance (Nat n) => Nat (S n)
Now, for each n in Nat, we need a class Fin n with exactly n instances. A
choice of instance represents the position the first argument of an n-ary
function gets shifted to. We can make Fin (S n) by embedding Fin n with
one type constructor, FS, and chucking in a new type with another, FO.
> class (Nat n) => Fin n x | x -> n
> data (Nat n) => FO n = FO n
> instance (Nat n) => Fin (S n) (FO n)
> data (Nat n,Fin n x) => FS n x = FS n x
> instance (Nat n,Fin n x) => Fin (S n) (FS n x)
The class Factorial n contains n! types---enough to represent the
permutations. It's computed in the traditional way, but this time the
product is cartesian...
> class (Nat n) => Factorial n p | p -> n
> instance Factorial O ()
> instance (Factorial n p,Fin (S n) x) => Factorial (S n) (x,p)
The operation InsertArg n x r s, where x is in Fin (S n), takes an
n-ary function type s and inserts an extra argument type r at
whichever of the (S n) positions is selected by x. The corresponding
function, insertArg permutes a (S n)-ary function in r -> s by
flipping until the first argument has been moved to the nominated
position. FS codes `flip the argument further in and keep going'; FO
codes `stop flipping'.
> class (Nat n,Fin (S n) x) =>
> InsertArg n x r s t | x -> n, x r s -> t, x t -> r s where
> insertArg :: x -> (r -> s) -> t
> instance (Nat n) => InsertArg n (FO n) r s (r -> s) where
> insertArg (FO _) f = f
> instance (Nat n,Fin (S n) x,InsertArg n x r s t) =>
> InsertArg (S n) (FS (S n) x) r (a -> s) (a -> t) where
> insertArg (FS _ x) f a = insertArg x (flip f a)
PermArg simply works its way down the factorial, performing the InsertArg
indicated at each step. permArg is correspondingly built with insertArg.
> class (Nat n,Factorial n p) =>
> PermArg n p s t | p s -> t, p t -> s where
> permArg :: p -> s -> t
> instance PermArg O () t t where
> permArg () t = t
> instance (InsertArg n x r t u,PermArg n p s t) =>
> PermArg (S n) (x,p) (r -> s) u where
> permArg (x,p) f = insertArg x (\r -> permArg p (f r))
This code is accepted by the Feb 2000 version of Hugs, of course with
-98 selected.
Let's look at some examples: the interesting thing is how the typechecker
copes. Here's the instance of permArg which corresponds to flip
Main> permArg (FS (S O) (FO O),(FO O,()))
ERROR: Unresolved overloading
*** Type : (InsertArg (S O) (FS (S O) (FO O)) a b c,
InsertArg O (FO O) d e b,
PermArg O () f e)
=> (a -> d -> f) -> c
*** Expression : permArg (FS (S O) (FO O),(FO O,()))
OK, I wasn't expecting that to work, because I didn't tell it the type
of the function to permute: however, the machine did figure it out. Look
at where it got stuck: I'd have hoped it would compute that e is f, and hence
that b is d -> f, then possibly even that c is d -> a -> f. Isn't that
what the functional dependencies say?
On the other hand, if I tell it the answer, it's fine.
Main> :t permArg (FS (S O) (FO O),(FO O,())) :: (a -> b -> c) -> b -> a -> c
permArg (FS (S O) (FO O),(FO O,())) :: (a -> b -> c) -> b -> a -> c
First, a nice little monomorphic function.
> elemChar :: Char -> [Char] -> Bool
> elemChar = elem
There's no doubt about the type of the input, but this happens:
Main> permArg (FS (S O) (FO O),(FO O,())) elemChar
ERROR: Unresolved overloading
*** Type : (InsertArg (S O) (FS (S O) (FO O)) Char a b,
InsertArg O (FO O) [Char] c a,
PermArg O () Bool c)
=> b
*** Expression : permArg (FS (S O) (FO O),(FO O,())) elemChar
What am I doing wrong? I hope my program says that c is Bool, and so on.
Again, tell it the answer and it checks out:
Main> :t (permArg (FS (S O) (FO O),(FO O,())) elemChar)
:: [Char] -> Char -> Bool
permArg (FS (S O) (FO O),(FO O,())) elemChar :: [Char] -> Char -> Bool
Adding some arguments gives enough information about `the answer' to
get rid of the explicit typing.
Main> permArg (FS (S O) (FO O),(FO O,())) elemChar ['a','b','c'] 'b'
True :: Bool
It's the same story with arity 3...
Main> :t permArg (FS (S (S O)) (FO (S O)),(FS (S O) (FO O),(FO O,()))) ::
(a -> b -> c -> d) -> (c -> a -> b -> d)
permArg (FS (S (S O)) (FO (S O)),(FS (S O) (FO O),(FO O,()))) ::
(a -> b -> c -> d) -> c -> a -> b -> d
Main> permArg (FS (S (S O)) (FO (S O)),(FS (S O) (FO O),(FO O,()))) foldl
ERROR: Unresolved overloading
*** Type : (InsertArg (S (S O)) (FS (S (S O)) (FO (S O))) (a -> b -> a) c d,
InsertArg (S O) (FS (S O) (FO O)) a e c,
InsertArg O (FO O) [b] f e,
PermArg O () a f)
=> d
*** Expression : permArg (FS (S (S O)) (FO (S O)),(FS (S O) (FO O),(FO O,())))
foldl
Main> :t (permArg (FS (S (S O)) (FO (S O)),(FS (S O) (FO O),(FO O,()))) ::
(a -> b -> c -> d) -> (c -> a -> b -> d)) foldl
permArg (FS (S (S O)) (FO (S O)),(FS (S O) (FO O),(FO O,()))) foldl ::
[a] -> (b -> a -> b) -> b -> b
Main> permArg (FS (S (S O)) (FO (S O)),(FS (S O) (FO O),(FO O,())))
foldl [1,2,3] (+) 0
6 :: Integer
So, am I failing to explain to Hugs why PermArg and InsertArg are programs,
despite the explicit functional dependencies, or is the typechecker just not
running them? It seems to be expanding PermArg's step case ok, but not
executing the base case, leaving InsertArg blocked. Can anyone shed some
light on the operational semantics of programming at the type level?
Having, said all that, I'm really impressed that even this much is possible
in Haskell. It's so nice to be able to write a type that says exactly what I
mean.
Cheers
Conor