Apologies in advance if this has been asked before or is too basic, but I am completely new to Haskell and have been experimenting in GHCi when I accidentally defined a function which I would have expected to throw an error.
I have two simple functions f
and g
defined as such
f = (+)
g = (-1)
Now obviously f
takes two arguments and g
takes one. f
only returns one, however, and so I (incorrectly) tried composing them like so
h x = g (f x)
To my surprise, this did not raise an error. The resulting type of h
is claimed by GHCi to be h :: (Num a, Num (a -> a)) => a -> a -> a
. This type signature is cryptic to me. Does it mean a
needs to be both a number and a function at the same time?
Moreover, when I try applying h
to seemingly any numerical arguments it throws an error. So, I guess my question is - what have I actually defined? What is h
and how does it work? Playing with it a bit more I see that it is equivalent to h = g . f
. This is especially confusing because I was under the impression the composition operator (.)
only accepts one-argument functions? I imagine there is a trick with currying happening here, but I'm not sure what it is exactly. As a corollary to the above, how can I correctly define the composition of f
and g
in point-free notation given that f
takes two arguments and . expects one-argument functions only?
There are a number of things here that I think are leading to confusion.
To start: g
is probably not what you think it is. By your description it seems you think it is the function that subtracts 1
from the input. This would have type
g :: Num a => a -> a
It is however the number negative one, and it has type
g :: Num a => a
This is an annoying fact about the ambiguity of the subtraction sign in mathematics that Haskell inherits. To write the function which subtracts one you should write
g :: Num a => a -> a
g = subtract 1
You could also write:
g :: Num a => a -> a
g x = x - 1
I would strongly suggest, that you add type signatures to functions you define. This way you get a check that the type signature you predict is the same as the one the compiler finds (or at least the two unify). This is a good idea for beginners and experience Haskell users. It's just really helpful.
Now I'm not going to go through the steps to how the compiler arrives at the final type of (Num a, Num (a -> a)) => a -> a -> a
, since it is a bit tedious and not very enlightening.
I will talk about what it means and why it is allowed. You ask:
Does it mean a needs to be both a number and a function at the same time?
Which is very close but not the case. It requires two preconditions:
a
is a number.a -> a
is a number.The latter is both a function and a number at the same time. Which is allowed in Haskell. In Haskell you can add new instance
s to existing types. So for example you could make a new type, MyInt
, and define an instance
of Num
for it, this would mean it acts like a number. Similarly if you wanted to add an instance for an existing type you could. You could make certain types of function or even all functions into Num
s if you wanted to.
And so Haskell allows you to make functions which take things that are both numbers and functions at the same time. This particular case is sort of silly. I can't think of a practical scenario where you would ever want to use h
, but the language isn't checking that, it's just checking that it follows the rules of the type system.
Lastly you ask:
This is especially confusing because I was under the impression the composition operator
(.)
only accepts one-argument functions? I imagine there is a trick with currying happening here, but I'm not sure what it is exactly.
You are correct that this has to do with currying. In Haskell two-argument functions are also one-argument functions. For example:
(+) :: a -> a -> a
is also:
(+) :: a -> (a -> a)
(the function arrow ->
is "right associative" meaning that when there are two of them in sequence without parentheses, the parentheses are implicitly placed around the right arrow)
You can describe (+)
as a function which takes a number and produces the function which adds that number. This is the core concept in currying. Functions which take multiple arguments can be thought of as functions which take one argument and produce another function.
If this explanation is still confusing try looking around online. There are people who are probably better at explaining it than me. (You can ask in the comments too, I can try to help).
It is possible to have functions types which must have one argument. For example Int -> Int
cannot except two arguments because Int
cannot be a function.
However if the output of a function is polymorphic, then there's no way to exclude the possibility of functions. So in the example subtract 1
has the type:
g :: Num a => a -> a
That a
is polymorphic so there could be a scenario where it is a function.
I can get how this might be a but uncomfortable. As you get better with Haskell however, this discomfort will go away.