Hacker News new | ask | show | jobs
by IshKebab 3927 days ago
Off-topic, but why are Haskell's function signatures written like this?

    function_name :: Param1Type -> Param2Type -> Param3Type -> ReturnType
To me that just makes no sense. There's no way to logically "read" it. The natural way would be "function_name converts a Param1Type into a Param2Type and then ... what?! Is this a chain of functions?"

Why not have something sensible like this?

    function_name :: Param1Type, Param2Type, Param3Type -> ReturnType
5 comments

A function has ONE parameter and ONE return value. The trick is that these parameters and returned values may be tuples or functions themselves.

So we can have a function which takes a single parameter which is a tuple :

    function_name :: (Param1Type, Param2Type) -> ReturnType
Or we can have a function which takes a single parameter and returns another function:

    function_name :: Param1Type -> (Param2Type -> ReturnType)
Using implied operator precedence, the later is written:

    function_name :: Param1Type -> Param2Type -> ReturnType
And has to be distinguished from a function which takes a function as parameter:

    function_name :: (Param1Type -> Param2Type) -> ReturnType
Because you write functions like this in mathematics as well.

What we usually see is something like this:

    f(x) = y
But if you look at it carefully the function f is a mapping of the domain x to the range y (the arrow is f), written like so:

    x -> y
Of course, the domain, being a set of all permissible values, is the type. So we write the type instead:

    type1 -> type2
Everything in Haskell is a function, and pure functions only ever take ONE parameter. Therefore the commas make no sense. The name in front just aids in naming stuff.

If you have a function that takes 2 parameters, they'd have to be curried. How would you represent curried functions?

    f::type1 -> type2 -> type3 
To make it more concrete, let's look at add instead of f.

So we can define a function like this (let's use Python for its readability):

    def add(a, b): return a + b
But remember, in haskell, functions are pure. Meaning they only map one input to one output. In order to make this happen, we need to split the function up into parts that only take one parameter.

Let's start with the plus operator as a function (it is one in Haskell just made into an infix). To think about it, it'd be something like this:

    plus(a)(b)
Where plus(a) is defined as:

    def plus(a): return plusA
Hence the first part would become a curried function like so, which takes another parameter:

    plusA(b)
Where plusA() is defined as such:

    def plusA(b): return b + a # a is a constant

So if you look at it from the types it was being transformed from:

    plus() # takes a Real, returns plusA.

    plusA(___) # takes a Real, returns a Real. written as (Real -> Real)
So if you put it together, the function signature for plus() is:

plus() takes Real -

    plus :: Real->
plus() returns plusA (which is Real->Real) -

    plus :: Real-> (Real-> Real)

Or in other words we can write it as such

    add :: Real -> Real -> Real
I don't understand one thing about this notation:

   fun :: A -> B -> C -> D
can be bracketed in a bunch of different ways -

   fun1 :: (A -> B) -> (C -> D)
   fun2 :: A -> (B -> C -> D)
etc. and don't these all mean different things?

fun1 would take a function and return a function, whereas fun2 takes an A and returns a curried function of B,C -> D.

The order of operations here is "right associative". So

  A -> B -> C -> D
is equivalent to any of the following

  A -> (B -> C -> D)
  A -> (B -> (C -> D))
  A -> B -> (C -> D)
They're equivalent through currying. But you can only add or remove parentheses for stuff that is on the right of a function arrow. (A -> B) -> (C -> D) is a different thing.
Read:

   fun :: A -> (B -> (C ->D))
Yes, functions need to be bracketed, as they are single arguments with a complex type.
Two things to add to the other comments: the language is named after Haskell Curry, from whom we get the term "currying," which is what we use to describe "the process of transforming a function that takes multiple arguments into a function that takes just a single argument and returns another function if any arguments are still needed".

Secondly, note that in Haskell a space represents function application -- it's not just there to separate terms.

This stuff might seem crazy, but once you start using it for useful things you come to really miss it in other languages. :)

My take, with no pretension of being accurate:

Say we have a function with signature

    f :: Int -> Int -> Int -> Int
Let's say this just add those Ints together.

Now let's create another function by partially applying the initial function: f1 = f 42

f1 signature will be

    f1 :: Int -> Int -> Int
Kind of "consuming" the signature. Repeat this until using up all the parameters. You end up with a function with no parameters, which map to a single Int.

You could say that f3 -> Int (note, not a valid notation for the sake of example). No need for a different symbol.

because in haskell functions are curried,

  function_name :: param1 -> param2 -> return
is a function that if you only give it param1 it returns a function that takes an argument of param2Type and returns returnType. This way you can easily compose functions
parans make it more obvious:

f :: param1 -> (param2 -> return)

ret = (f p1) p2