Haskell function type clarification

We have:

applyTwice f x = f(f x)

This would make sense only if the types of x and f x were equivalent (else how would you be able to pass f x to a function that takes x?). Therefore, you could say that applyTwice takes 2 input arguments:

  • A function that takes a type t and returns the same type t
  • An input of some type

Now this input could be of any type, say t1. But you also have to be able to apply the function to this input, and so the type of the input must also be t. Put together, we get the signature:

applyTwice :: (t -> t) -> t -> t

Remember that only the last term is the return type, and all others are types of the inputs.

Now consider:

applyOnce f x = f x

Simplistically, all it says about the function f is that it should be able to accept an input x of any type. There is nothing about what type the function should return except that applyOnce should also return that same type. Hence, we end up with the signature you see:

applyOnce :: (t1 -> t) -> t1 -> t

where t1 could be any type and t could be any type (same or different from t1), but requiring that the return type of f matches the return type of applyOnce, represented by t here.

TL;DR: Your function signature for a function with n inputs will always have (n+1) terms, and the last term will be the return type of the function.

However, you should note that this is not actually how things work under the hood. All functions in Haskell actually take one argument i.e. they are curried. You can read more about this here.

Return types

What arguments a function takes and what a function returns is quite a tricky question in Haskell, because of special stuff like partial application.

The map function looks like it takes an f :: a -> b, the transformation function and a list xs :: [a] and returns a list of ys :: [b] with each element of xs transformed per f. But map can also be seen as a function which takes a simple function f and returns another function, which acts upon lists and returns lists.

What I do to help me reason about arguments is first separate all types delimited by top level ->s. For example map:

map :: (a -> b) -> [a] -> [b]
-- gives us
-- (a -> b) and [a] and [b]

The rightmost type is what is traditionally called the "return type" when map is called with all its arguments provided. The other types are the arguments I have to use, for example in the function definition.

Take this with a grain of salt, as you will see plenty of examples of definitions of functions which do not use all arguments explicitly to define the function.

Coming to your second question, you apparently mix up the arguments and the function body. Taking applyTwice:

applyTwice f x = f (f x)

f and x are the arguments to applyTwice and f (f x) is the function body. Now f must be a function, since it is applied to an argument x. x's type can be anything, let's call it t. Now f must go from t, the type of x to something else, call it s. But now f x, which has type s is an argument to f again, so the compiler can deduce that s ~ t. Now f (f x) is again of type s, which must be the same as t, which means you end up with:

applyTwice :: (t -> t) -> t -> t

Analogously you can try this for applyOnce and see that its type is (t -> s) -> t -> s, which is just $ from the Prelude.

Let's translate Haskell to English.

applyTwice :: (t -> t) -> t -> t means that applyTwice accepts a function from a t to a t and a value t and returns a t. Indeed, if we look at the definition, we see that it applies a function f to a value x and feeds the result into f once more. This implies - as indicated by the type signature - that f and f x must have the same type t (since f only accepts a t as an argument).

In contrast, applyOnce :: (t1 -> t) -> t1 -> t allows for f x and x to have different types (t and t1 respectively), since this function doesn't feed the output of f (of type t) back into f.

I think the confusion stems from the fact that you fail to distinguish between f (the function) and f x (the result of the function).

Lastly, the type you suggest for applyOnce (namely (t1 -> t) -> t) implies (nay affirms) that you can get the result of a function without providing an argument; remember that (t1 -> t) is the input type (a function) and t is the output type. How do you calculate f(x) without an x?