16

It's quite easy to define an operator like

(@) :: [x -> y] -> [x] -> [y]

which takes a list of functions and a list of inputs and returns a list of outputs. There are two obvious ways to implement this:

  1. Apply the first function to the first input, the second function to the second input, etc.
  2. Apply every function to every input.

Either is equally trivial to define. The nice thing about it is that you can now do something like

foo :: X -> Y -> Z -> R

bar :: [X] -> [Y] -> [Z] -> [R]
bar xs ys zs = [foo] @@ xs @@ ys @@ zs

This generalises to an arbitrary number of function arguments.


So far so good. Now for the problem: How to I change the type signature for @@ such that the type signature for bar becomes

bar :: [X] -> [Y] -> [Z] -> [[[R]]]

It's not hard to implement a function with this type; either of these will do it:

bar xs ys zs = map (\ x -> map (\ y -> map (\ z -> foo x y z) zs) ys) zs
bar xs ys zs = map (\ z -> map (\ y -> map (\ x -> foo x y z) xs) ys) zs

I'm not fussy about which result I get. But I can't figure out how to tweak the @@ operator to do this.


An obvious thing to try is

(@@) :: [x -> y] -> [x] -> [[y]]

It's not hard to implement this, but it won't help you. Now you have

[foo] @@ xs :: [[Y -> Z -> R]]

which is not a valid input to @@. There's no obvious way to know how many levels of lists to reach through to get to the function; clearly this approach is a dead end.

I've tried several other possible type signatures, but none of them takes me closer to the answer. Can somebody either give me a solution, or explain why none exists?

Dan Burton
  • 53,238
  • 27
  • 117
  • 198
MathematicalOrchid
  • 61,854
  • 19
  • 123
  • 220
  • 10
    Irrelevant to the question, however your original `(@@)` (2nd version) is equivalent to the `Applicative` instance for lists. The first version, which applies each function to each input, is the `Applicative` instance for the `ZipList` newtype. – John L Dec 16 '11 at 10:52
  • What's the difference between 'each' and 'every'? – Prateek Dec 16 '11 at 11:17
  • 2
    @Prateek: Difference between `[ f x y | x <- xs, y <- ys]` and `[ f x y | x <- xs | y <- ys]`. – MathematicalOrchid Dec 16 '11 at 12:01
  • @Prateek same as the difference between `zipWith ($)` and `ap`. – JB. Dec 16 '11 at 14:52
  • 1
    Yes I could guess that those are the only two reasonable ways to do implement `(@)`. But it's still not clear from the words 'each' and 'every' mean. They seem to mean the same thing here. – Prateek Dec 16 '11 at 16:21
  • I've taken the liberty to clarify the difference. Revert my edit if I'm off the mark. – Dan Burton Dec 16 '11 at 19:35
  • Yeah, it's not the greatest description possible. I figured everybody would know what I meant anyway - and it's not especially central to the actual question... – MathematicalOrchid Dec 16 '11 at 21:07

2 Answers2

8

You've already hit upon why this is troublesome. Your function (@@) is applied to inputs of different types (e.g. [x->y], [[x -> y]], etc. This means that your type signature for @@ is too restrictive; you will need to add some polymorphism to make it more general enough to use it with nested lists. Since Haskell implements polymorphism with type classes, that's a good direction to try.

As it happens, with this problem if you know the type of the LHS, you can uniquely determine both the RHS and the result. When the input has type [a->b], the RHS must be [a] and the result must be [[b]]. This can be simplified to an input of a->b, RHS of [a], and result of [b]. Since the LHS determines the other parameters and result, we can use either fundeps or type families to represent the other types.

{-# LANGUAGE TypeFamilies, UndecidableInstances #-}

class Apply inp where
  type Left inp    :: *
  type Result  inp :: *
  (@@) :: inp -> [Left inp] -> [Result inp]

Now that we have a type class, we can make the obvious instance for a function:

instance Apply (a -> b) where
  type Left (a -> b)   = a
  type Result (a -> b) = b
  (@@) = map

The list instance isn't too bad either:

instance Apply f => Apply [f] where
  type Left [f]   = Left f
  type Result [f] = [Result f]
  l @@ r = map (\f -> f @@ r) l
  -- or    map (@@ r) l

Now our class method @@ should work with arbitrarily deep lists. Here are some tests:

*Main> (+) @@ [1..3] @@ [10..13]
[[11,12,13,14],[12,13,14,15],[13,14,15,16]]'s

let foo :: Int -> Char -> Char -> String
    foo a b c = b:c:show a

*Main> foo @@ [1,2] @@ "ab" @@ "de"
[[["ad1","ae1"],["bd1","be1"]],[["ad2","ae2"],["bd2","be2"]]]

You might want to look at the printf implementation for further inspiration.

Edit: Shortly after posting this, I realized that one could generalize the container type in my Apply class from List to an Applicative, then use the applicative instance instead of map. This would allow for both regular list and ZipList behavior.

John L
  • 27,937
  • 4
  • 73
  • 88
  • This is actually a very simular to the answer to [this question](http://stackoverflow.com/questions/8478067/truth-tables-from-anonymous-functions-in-haskell) and as the OP pointed out `printf`. – John F. Miller Dec 16 '11 at 18:09
  • Other fun tests: `[negate, id] @@ [1..3]` and `[(+), (-)] @@ [1..3] @@ [11..13]` – Dan Burton Dec 16 '11 at 19:32
  • 1
    That's a good thorough answer and all... but I was hoping there was some way to avoid needing a typeclass. Ultimately classes are desugared during compilation, so there must be _some_ way of doing it. I suppose it's just a question of how inconvenient it turns out to be... Certainly it's looking like it might take an Oleg to solve it that way. o_O – MathematicalOrchid Dec 16 '11 at 21:04
  • @MathematicalOrchid - even if you don't use a type class, you'll still need a way to select the appropriate function based upon the argument types. You could probably manually emulate type class dictionaries via `Data.Typeable`, but that's the best I've got. – John L Dec 16 '11 at 21:34
  • 1
    @JohnL: Type inference and parametric polymorphism can do the job just fine, actually--see my answer. – C. A. McCann Dec 16 '11 at 21:39
7

Actually, this doesn't need type classes at all! You lose a little bit of convenience by avoiding type classes, but that's all.

The key point is that, despite reusing a single combinator, polymorphism allows the type of each use to be different. This is the same principle behind Applicative-style expressions like f <$> xs <*> ys <*> zs, and the end result here will look similar. As such, we'll do it for any Functor, not just lists.

The difference between this and the Applicative version is that we go deeper into the nested Functors with each step. The necessary polymorphism requires flexibility at the innermost layer, so to accomplish this we'll use a continuation-ish trick where the result of each combinator is a function that accepts a transformation to use at the innermost layer.

We'll need two operators, one that starts the chain and one that continues it incrementally. Starting with the latter:

(@@) :: (Functor f) => (((a -> b) -> f c) -> r) -> f a -> (b -> c) -> r
q  @@ xs = \k -> q (\f -> k . f <$> xs)

This takes a new argument on the right, and the expression in progress on the left. The result takes a function k, which specifies what to do to get the final result. k is combined with whatever the expression in progress already has, and both are mapped over the new argument. This is convoluted, but should look familiar to anyone who's taken CPS-style code apart.

(<@) :: (Functor f, Functor g) => f (a -> b) -> g a -> (b -> c) -> f (g c)
fs <@ xs = (<$> fs) @@ xs

The chain is started by simply mapping everything else over the first argument.

Unlike the simpler Applicative case, we also need to explicitly end the chain. As with the Cont monad, the simplest way to do this is apply the result to the identity function. We'll give that a helpful name:

nested = ($ id)

Now, we can do things like this:

test2 :: [X -> Y -> R] -> [X] -> [Y] -> [[[R]]]
test2 fs xs ys = nested (fs <@ xs @@ ys)

test3 :: [X -> Y -> Z -> R] -> [X] -> [Y] -> [Z] -> [[[[R]]]]
test3 fs xs ys zs = nested (fs <@ xs @@ ys @@ zs)

Not quite as pretty as the type class version, but it works.

C. A. McCann
  • 76,893
  • 19
  • 209
  • 302
  • That's pretty damned sweet. This is the kind of answer I was hoping for! Thank you. – MathematicalOrchid Dec 16 '11 at 22:13
  • @MathematicalOrchid: In this specific case, I'd personally be more likely to use type classes--the result looks nicer when used, and is arguably simpler once you know how it works. But approaches like the one I've outlined are still worth thinking about, for when other approaches are unavailable for some reason. – C. A. McCann Dec 16 '11 at 22:20
  • 1
    @MathematicalOrchid: Oh, and since you mentioned Oleg in a comment on John L's answer--as you may be aware, continuations are one of his other specialties besides type hackery. So in a way, this may actually be the Oleg-style approach you wondered about. – C. A. McCann Dec 16 '11 at 22:23
  • All I know about Oleg is that everything he writes is utterly incomprehensible (but presumably brilliant). I just knew there must be some way of doing this, and I couldn't figure out what it was... – MathematicalOrchid Dec 16 '11 at 22:25
  • Can't believe I didn't think of this; I've even used it before myself. – John L Dec 16 '11 at 22:45