Dulguun Otgon
Dulguun Otgon

Reputation: 1949

Passing function as parameter, weird case

I fear that it might come out as extremely cringy, but I don't know where else to ask.

For a while I thought if a function accepts another function as parameter it had to specifically say it in the type, i.e f :: (a -> a -> a) -> b -> c. So that I can pass something like g :: a -> a -> a as its first paramater and concrete type parameter as second, but nothing else.

But I have just discovered that I can apply const :: a -> b -> a to (+) :: Num a => a -> a -> a. And the result is weird (+) const :: Num (a -> b -> a) => (a -> b -> a ) -> a -> b -> a.

Also how is this foldr :: ( a -> b -> b) ... taking (\b g x -> g (f x b)) as it's first parameter.

foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f a bs =
   foldr (\b g x -> g (f x b)) id bs a

What's going on here? I don't even know how to call it, so that I can't do google search or use appropriate title on my question here.

Where can I learn about this? What is this called? What's the application and theory of it?

Upvotes: 1

Views: 136

Answers (2)

Pi Delport
Pi Delport

Reputation: 10618

Any type variable can be instantiated with a function type, too.

The simplest example that gave me an "a ha" moment was to realise that:

($) :: (a -> b) -> (a -> b)

is just a special case of:

id :: a -> a

In other words, you can use id just like $, as an infix application operator:

ghci> negate $ 5
-5
ghci> negate `id` 5
-5
ghci> map ($ 5) [negate,(2*),(1+)]
[-5,10,6]
ghci> map (`id` 5) [negate,(2*),(1+)]
[-5,10,6]

You can literally define ($) as id, just with the type narrowed down to functions:

($) :: (a -> b) -> a -> b
($) = id

So, in summary, anywhere you see a type variable like a, you can choose to instantiate it with a function type like (b -> c).

Upvotes: 1

leftaroundabout
leftaroundabout

Reputation: 120751

There is nothing special about function types. A polymorphic function a -> a can obviously be used as, say, Int -> Int, or String -> String. But it can just as well be used as (Char->Double) -> (Char->Double). In the example you've tried, you use basically

(+) :: (a->b->a) -> (a->b->a) -> (a->b->a)

Parentheses on the result can be ommitted, so this is

(+) :: (a->b->a) -> (a->b->a) -> a -> b -> a

If we supply a const, we then get

(const +) :: (a->b->a) -> a -> b -> a

So far I didn't bother about the Num constraint. That constraint is actually a problem, though it's not completely nonsensical – it is in principle possible to declare a Num instance for function types:

instance (Num r) => Num (a->r) where
  fromInteger = const . fromInteger
  f + g = \x -> f x + g x
  negate f = negate . f
  ...

Then the constraint Num (a->b->a) would be equivalent to Num (b->a), which in turn would be equivalent to Num a alone. You can then actually use addition of functions like const: since this is a two-parameter function, the result of the addition will

> (const + \x y -> y) 1 1
2
> (const + \x y -> x*y) 1 0
1

and so on.

However, just because it's possible to define an instance doesn't necessarily mean it's a good idea; it can also lead to weird unexpected behaviour. In particular, the Num instance for functions fails to fulfill the field axioms. The instance is not defined in the standard library, and IMO this is good. Hence, (+) const does not actually make any sense in Haskell.

Upvotes: 4

Related Questions