0

I am trying to understand how to reason about the types for partial applied method chaining . I do not understand why :
:t (+)(+2) is (a->a)->a->a
or why:
:t (+)(+) is (a->a->a)->a->a->a

I mean for the first example I do not understand , when looking at the (+) do i need to look at what it needs a->a->a or what method is before it (+2) (which needs an a).

At the second example i know the first (+) needs a->a->a, but once i fullfill the first method why does the second one need again the same parameters?

Bercovici Adrian
  • 8,794
  • 17
  • 73
  • 152

1 Answers1

3

You've actually slightly misreported the type. There are some all-important typeclass constraints in there:

(+)(+2) :: (Num a, Num (a -> a)) => (a -> a) -> a -> a

So where does this come from? It's actually quite simple. Firstly, the type signature of (+) is

(+) :: Num a => a -> a -> a

Or, rewriting to make the currying explicit:

(+) :: Num a => a -> (a -> a)

Meanwhile, the type of (+2) (which is the result of just doing exactly that partial application) is:

(+2) :: Num a => a -> a

Now when you do (+)(+2), what you are doing is (partially) applying the (+) function to the function (+2). That is, we're treating (+2) as the first argument of (+). In order for this to work, its type - which is Num a => a -> a - must be an instance of Num. So this is why we have a further type contraint, that a -> a must be an instance of Num. (This is never the case by default, but you can define your own instance for numerical functions - typically it would apply both functions to the input and add the results.)

So let's specialise the type signature of (+) to the case where it's applied to a function (a -> a) (which as I've just said must then itself be an instance of Num, as well as a itself). We get:

(+) :: (Num a, Num (a -> a)) => (a -> a) -> (a -> a) -> (a -> a)

or with currying made explicit:

(+) :: (Num a, Num (a -> a)) => (a -> a) -> ((a -> a) -> (a -> a))

That is, it takes an a -> a function and returns a "higher-order function" of type (a -> a) -> (a -> a). So that when we apply this to (+2) we get exactly such a higher-order function:

(+)(+2) :: (Num a, Num (a -> a)) => (a -> a) -> (a -> a)

This is exactly what is reported, since the final pair of parentheses is unnecessary. (This is due to currying again.)

The second case is entirely analagous, except that the function you're applying to is a -> a -> a rather than a -> a.

Robin Zigmond
  • 17,805
  • 2
  • 23
  • 34
  • So you are saying that i can think of the arguments of `+` as `slots` that can be filled with anything (does not matter how deep nested is, if at any point it matches the initial constraints). So for `(+) (+2) (+(+3))` , `+3` must match the constraints and `+(+3)` too, and that's it ? – Bercovici Adrian Apr 03 '19 at 11:28
  • 1
    Broadly, but it sounds like you're thinking in terms that are too complicated. `(+)` takes 2 arguments of the same type, an returns another of the same type - and that type can be anything that's an instance of `Num`. When you supply it a function, the type of that function must be an instance of `Num` - and the partially applied function takes a function of the same type and returns another function of that type. – Robin Zigmond Apr 03 '19 at 11:36
  • But for example if i say : `(+) (2+) (+(+2))` from my reasoning the type should be `(+) (a->a) (a->(a->a)->a)`. The compiler shows `(+) (a->a)->a->a` – Bercovici Adrian Apr 03 '19 at 11:38
  • can you explain your reasoning that comes up with that type? (It's not even a valid type, is there a typo somewhere?) – Robin Zigmond Apr 03 '19 at 11:44
  • 1
    in any case, this is a rather confusing example, and relies on the fact that the literal `2` can stand for *any* instance of `Num`. In your example, the `2` in `(2+)` is actually being treated as a function. (At least I think so. As I said, this is quite a confusing example, and almost certainly nothing like this will come up in real practical code.) – Robin Zigmond Apr 03 '19 at 11:45
  • The first argument of `(+) ` would be `(2+)` which is a `(a->a)`.The second argument is `(+) (+2)` so `+2` would be `(a->a)` Therefore the `(+) (+2)` is `(a->a)->a->a`.So the big `+` would be `(+) (a->a) ((+) (a->a) a)`.So applying in the second argument the `+` we get a `a->(a->a)->a`. – Bercovici Adrian Apr 03 '19 at 13:16
  • 2
    The key point is that `(+)` requires both of its arguments to be the same type - and returns a value of the same type. Here the argument `(2+)` has type `Num a => a -> a`, and `(+(+2))` is `(Num a, Num (a -> a)) => (a -> a) -> a -> a`. To make these the same type, Haskell takes the `a -> a` as the `a` for the `(2+)` (which it can because of the `Num (a -> a)` constraint. So both arguments you've given `(+)` have the type `(a -> a) -> a -> a` (with constraints that I'm eliminating for brevity) - thus this must also be the type of the result. – Robin Zigmond Apr 03 '19 at 13:25
  • "So you are saying that i can think of the arguments of `+` as `slots` that can be filled with anything (does not matter how deep nested is, if at any point it matches the initial constraints)." No, the argument as a whole must match the constraints, no nesting is considered. `+` takes two arguments of any type constrained by the `Num` typeclass. The reason passing functions to `+` typechecks is because Haskell has no way to know if you're later going to be creating a `Num` instance for functions (which is entirely possible, but not recommended). – DarthFennec Apr 03 '19 at 19:53