Why Lists themselves don't work
In the narrowest sense of the question,
Is there a standard Haskell function (or pattern) to extract the contents of a list and feed them as though they are the ordered positional arguments to a function?
this has a very simple answer "no". The reason why such a function does not exist is because the type signature cannot easily make sense. The type signature you're asking for is:
applyToList :: (a -> c) -> [a] -> d
where the c
either has the form a -> c'
(in which case we recurse the definition) or else has the type d
itself. None of the Prelude functions have wacky type signatures.
If you actively ignore this and try anyway, you will get the following error:
Prelude> let applyToList f list = case list of [] -> f; x : xs -> applyToList (f x) xs
<interactive>:9:71:
Occurs check: cannot construct the infinite type: t1 ~ t -> t1
Relevant bindings include
xs :: [t] (bound at <interactive>:9:52)
x :: t (bound at <interactive>:9:48)
list :: [t] (bound at <interactive>:9:19)
f :: t -> t1 (bound at <interactive>:9:17)
applyToList :: (t -> t1) -> [t] -> t -> t1
(bound at <interactive>:9:5)
In the first argument of ‘applyToList’, namely ‘(f x)’
In the expression: applyToList (f x) xs
The problem here is that when the typechecker tries to unify c
with a -> c
it constructs an infinite type; it has a cycle-detection algorithm which stops it, so it prematurely errors out.
There is a more fundamental problem here, which is the question of what applyTo (+) [3]
should yield. Here (+)
has type n -> n -> n
for some n
. The morally right answer is (3+) :: n -> n
; but if you really want to consume all of the arguments of your list you probably want it to return undefined :: n
instead. The reason that you cannot use the morally right answer is because you reject the definition applyTo f = f . head
(which is typeable and does the above). The problem in the abstract is that the length of [3]
is not known until run-time. You could insert an arbitrary expression in there. You could try to run this on an array which has length 1 if Goldbach's conjecture is true, or else length 2 if it is not; is the type system supposed to solve Goldbach's conjecture for you?
How to make them work
That last point actually contains the solution. We need to annotate the functions with their arities:
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FlexibleContexts, FunctionalDependencies, UndecidableInstances #-}
-- no constructors; these exist purely in the type domain:
data Z
data S n
class Reducible f a x | f -> a x where applyAll :: f -> [a] -> x
newtype Wrap x n = Wrap x -- n is a so-called "phantom type" here
rewrap :: Wrap x n -> Wrap x (S n)
rewrap (Wrap x) = Wrap x
wrap0 :: x -> Wrap x Z
wrap0 = Wrap
wrap1 = rewrap . wrap0
wrap2 = rewrap . wrap1
wrap3 = rewrap . wrap2
wrap4 = rewrap . wrap3
apply :: Wrap (a -> x) (S n) -> a -> Wrap x n
apply (Wrap f) a = Wrap (f a)
instance Reducible (Wrap x Z) a x where
applyAll (Wrap x) [] = x
applyAll (Wrap x) _ = error "function called on too many arguments"
instance (Reducible (Wrap y n) a x) => Reducible (Wrap (a -> y) (S n)) a x where
applyAll (Wrap f) [] = error "function called on too few arguments"
applyAll w (x : xs) = applyAll (apply w x) xs
You can then write something like:
wrap3 (\x y z -> (x + y) * z) `applyAll` [9, 11, 2]
and it will rightly construct 40
.
As you can tell, this involves a lot of baggage, but it's all necessary to tell the compiler "hey, this function is going to have three arguments so a list of length 3 is perfect for it" in a fully generic way.
Of course, writing applyAll (wrap3 ___) ___
is tedious. However, if you're trying to build a library of functions with arbitrary arities, you can probably work in some extra functions which manage those arities for you.
You may also want to annotate the length of your lists with Z
, S Z
, etc. -- in this case I think you can get an applyAll
which does currying. Also, as another answer pointed out, you might be able to get a good distance with having multiple constructors for Wrap
which move the recursion into the data type itself -- possibly being able to remove some of those nasty language extensions.