7

As an exercise I'm trying to recreate Lisp's apply in Haskell. I do not intend to use this for any practical purpose, I just think it's a nice opportunity to get more familiar with Haskell's type system and type systems in general. (So I am also not looking for other people's implementations.)

My idea is the following: I can use GADTs to "tag" a list with the type of the function it can be applied to. So, I redefine Nil and Cons in a similar way that we would encode list length in the type using a Nat definition, but instead of using Peano numbers the length is in a way encoded in the tagging function type (i.e. length corresponds to the number of arguments to the function).

Here is the code I have so far:

{-# LANGUAGE GADTs #-}

-- n represents structure of the function I apply to
-- o represents output type of the function
-- a represents argument type of the function (all arguments same type)
data FList n o a where
  -- with Nil the function is the output
  Nil :: FList o o a
  -- with Cons the corresponding function takes one more argument
  Cons :: a -> FList f o a -> FList (a -> f) o a

args0 = Nil :: FList Int Int Int -- will not apply an argument
args1 = Cons 1 args0 -- :: FList (Int -> Int) Int Int
args2 = Cons 2 args1 -- :: FList (Int -> Int -> Int) Int Int
args3 = Cons 3 args2 -- :: FList (Int -> Int -> Int -> Int) Int Int

listApply :: (n -> o) -> FList (n -> o) o a -> o
-- I match on (Cons p Nil) because I always want fun to be a function (n -> o)
listApply fun (Cons p Nil) = fun p
listApply fun (Cons p l) = listApply (fun p) l

main = print $ listApply (+) args2

In the last line, my idea would be that (+) will be of type Int -> Int -> Int, where Int -> Int corresponds to the n in (n -> o) and o corresponds to the last Int (the output) [1]. As far as I can tell, this type seems to work out with the type of my argsN definitions.

However, I get two errors, of which I will state the component that seems relevant to me:

test.hs:19:43:
    Could not deduce (f ~ (n0 -> f))
    from the context ((n -> o) ~ (a -> f))
      bound by a pattern with constructor
                 Cons :: forall o a f. a -> FList f o a -> FList (a -> f) o a,
               in an equation for ‘listApply’

and

test.hs:21:34:
    Couldn't match type ‘Int’ with ‘Int -> Int’
    Expected type: FList (Int -> Int -> Int) (Int -> Int) Int
      Actual type: FList (Int -> Int -> Int) Int Int
    In the second argument of ‘listApply’, namely ‘args2’

I'm not sure how to interpret the first error. The second error is confusing me since it does not match with my interpretation stated marked with [1] earlier.

Any insights into what is going wrong?

P.S: I'm more than willing to learn about new extensions if that would make this work.

Sam van Herwaarden
  • 2,321
  • 14
  • 27
  • 2
    Note that `a -> b -> c` is the same as `a -> (b -> c)` **not** `(a -> b) -> c`. It seems like you got this backwards. That's why when you try to unify `n -> o` with `Int -> Int -> Int` you get that `n` is `Int` and `o` is `Int -> Int`. – Bakuriu Sep 17 '15 at 10:56
  • Right so the clue is that `((->) r)` is not associative? I guess I wasn't too actively aware of that and sort of assumed it was. But when I think about it, of course not :) Then I just need to reconsider where there messes up my code (that's not obvious to me yet). – Sam van Herwaarden Sep 17 '15 at 11:02
  • 2
    You might also like [How do I define Lisp's apply in Haskell?](http://stackoverflow.com/q/6168880/791604). – Daniel Wagner Sep 17 '15 at 19:17

2 Answers2

9

You got it almost right. Recursion should follow the structure of GADT:

{-# LANGUAGE GADTs #-}
-- n represents structure of the function I apply to
-- o represents output type of the function
-- a represents argument type of the function (all arguments same type)
data FList n o a where
  -- with Nil the function is the output
  Nil :: FList o o a
  -- with Cons the corresponding function takes one more argument
  Cons :: a -> FList f o a -> FList (a -> f) o a

args0 = Nil :: FList Int Int Int -- will not apply an argument
args1 = Cons 1 args0 -- :: FList (Int -> Int) Int Int
args2 = Cons 2 args1 -- :: FList (Int -> Int -> Int) Int Int
args3 = Cons 3 args2 -- :: FList (Int -> Int -> Int -> Int) Int Int

-- n, not (n -> o)
listApply :: n -> FList n o a -> o
listApply fun Nil = fun
listApply fun (Cons p l) = listApply (fun p) l

main = print $ listApply (+) args2

three :: Int
three = listApply (+) (Cons 2 (Cons 1  Nil))

oof :: String
oof = listApply reverse (Cons "foo" Nil)

true :: Bool
true = listApply True Nil -- True

-- The return type can be different than the arguments:

showplus :: Int -> Int -> String
showplus x y = show (x + y)

zero :: String
zero = listApply showplus (Cons 2 (Cons 1 Nil))

Must say, that this looks quite elegant!


Even OP doesn't ask for other's people implementation. You can approach problem a bit differently, resulting in a different looking but neat API:

{-# LANGUAGE KindSignatures #-}
{-# LANGuAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE AllowAmbiguousTypes #-}

import Data.Proxy

data N = O | S N

p0 :: Proxy O
p1 :: Proxy (S O)
p2 :: Proxy (S (S O))
p0 = Proxy
p1 = Proxy
p2 = Proxy

type family ArityNFun (n :: N) (a :: *) (b :: *) where
  ArityNFun O a b = b
  ArityNFun (S n) a b = a -> ArityNFun n a b

listApply :: Proxy n -> ArityNFun n a b -> ArityNFun n a b
listApply _ = id

three :: Int
three = listApply p2 (+) 2 1

oof :: String
oof = listApply p1 reverse "foo"

true :: Bool
true = listApply p0 True

showplus :: Int -> Int -> String
showplus x y = show (x + y)

zero :: String
zero = listApply p2 showplus 0 0

Here we could use Nat from GHC.TypeLits, but then we'd need UndecidableInstances. The added sugar is not worth the trouble in this example.

If you want to make polymorphic version, that's also possible, but then index is not (n :: Nat) (a :: *) but (as :: [*]). Also making plusN could be a nice exercise, for both encodings.

phadej
  • 11,947
  • 41
  • 78
  • Awesome! Earlier I had some errors which seemed to indicate to me that I had to tell the type-checker more about what the output of `fun` would be, that's why I made it `(n -> o)`. But apparently the problem was something else and I solved that in the mean time. Thanks! – Sam van Herwaarden Sep 17 '15 at 11:07
  • Hmm. I suggested an edit (to change `args0 = Nil :: FList Int Int Int` to `args0 = Nil :: FList b b a` instead (I think it's nice that also works), I just want to say this was not with the goal of putting down the answer (last time I suggested an edit my name didn't appear so I didn't think it would do that, rather the opposite). But now it has been marked as deliberately destructive, just to clarify, that was not my intention! I can't retract the edit :/ – Sam van Herwaarden Sep 17 '15 at 11:44
  • Ok good it seems the edit will be rejected. Sorry for this and thanks again for the answer! – Sam van Herwaarden Sep 17 '15 at 11:59
  • 2
    @Sam van Herwaarden, this definition: `data FList r a where Nil :: FList a a; Cons :: a -> FList r a -> FList (a -> r) a` looks better. You can also make `FList` polymorphic: `data FList a b where Nil :: FList a a; Cons :: a -> FList b c -> FList (a -> b) c`. – effectfully Sep 17 '15 at 17:37
  • 2
    @user3237465 I see, with your first version the arguments and the result are of the same type, in the second version arguments can be of different types. OP's encoding enforces that arguments are of the same type, but the result can be different. – phadej Sep 17 '15 at 18:00
  • "OP's encoding enforces that arguments are of the same type, but the result can be different." — my bad, didn't observe this. – effectfully Sep 17 '15 at 18:06
  • @user3237465: I don't really like the first definition for the reason phadej mentions. I like the second definition very much though. It seems kind of magical actually. – Sam van Herwaarden Sep 17 '15 at 18:16
  • My mind is kind of blown... In GHCI: `:t Cons () $ Cons (Just False) (Cons True Nil)` shows `:: FList (() -> Maybe Bool -> Bool -> o) o` as type. So we have a type that represents a polymorphic list, but the structure of the list is represented as a function type. I really need to let this sink in a bit. Thanks :) – Sam van Herwaarden Sep 17 '15 at 18:43
  • 1
    @SamvanHerwaarden I wrote down another encoding, I find it closer to something, what I'd write in fully dependent language. – phadej Sep 17 '15 at 19:12
  • Thanks! Most of these extensions are unfamiliar to me, I will have a look at them and try to understand the code. – Sam van Herwaarden Sep 17 '15 at 19:33
  • 1
    @Sam van Herwaarden, this [can](http://lpaste.net/141202) be made polymorphic too. BTW, in fully dependent language with type inference for constructor-headed functions we even [can](http://stackoverflow.com/a/29179509/3237465) omit these proxies and handle dependent types and universes. – effectfully Sep 17 '15 at 20:32
  • @SamvanHerwaarden I learned a long while ago that SO *really* doesn't like edit suggestions that change actual code. – Ørjan Johansen Sep 17 '15 at 23:14
  • Is GHC likely to offer runtime-optimized `Nat` anytime soon? It sounds like it can't even figure out induction on typelits :/ – dfeuer Sep 18 '15 at 01:21
  • @dfeuer AFAIK `InjectiveTypeFamilies` will allow to freely go from `Nat` to peano `N`. And if someone really needs big stuff up there, then we could ask for `NatFold (s :: Nat -> Nat) (z :: Nat) :: Nat` to be built-in. – phadej Sep 18 '15 at 04:17
  • @phadej, I don't understand. How do injective type families help? I'm not sure what `NatFold` is supposed to be, but I suppose it would be possible to `unsafeCoerce` one's way into the `Nat` eliminator (barf). – dfeuer Sep 18 '15 at 04:21
2

Not the same thing, but I suspect you'll be interested in the free applicative functor, which the free library provides. It goes something like this (based on the implementation in free, but using a :<**> constructor instead of Ap):

data Ap f a where
  Pure :: a -> Ap f a
  (:<**>) :: f x -> Ap f (x -> a) -> Ap f a

You can think of these as a heterogeneously-typed list with elements of types f x0, ..., f xn, terminated by Pure (f :: x0 -> ... -> xn -> a). This is like a "syntax tree" for applicative computations, allowing you to use the regular applicative methods to build up a "tree" that can be separately run by interpreter functions.


Exercise: implement the following instances:

instance Functor f => Functor (Ap f) where ...
instance Functor f => Applicative (Ap f) where ...

Hint: the Applicative laws provide a recipe that you can use to implement these.

Luis Casillas
  • 29,802
  • 7
  • 49
  • 102
  • Thanks. I didn't manage to derive the whole `Applicative` instance (I struggled with the `Ap x y <*> z` case) but that was definitely interesting and instructive. – Sam van Herwaarden Sep 17 '15 at 22:54