6

There are many questions based on applyTwice, but none that relate to my problem. I understand that the applyTwice function defined like this:

applyTwice :: (a -> a) -> a -> a
applyTwice f a = f (f a)

applies a function twice. So if I have an increment function:

increment x = x + 1

and do

applyTwice increment 0

I get 2. But I don't understand these results:

applyTwice applyTwice applyTwice increment 0 -- gives 16
applyTwice applyTwice applyTwice applyTwice increment 0 -- gives 65536
applyTwice applyTwice applyTwice applyTwice applyTwice increment 0 -- stack overflow

I also know that

twice = applyTwice applyTwice increment
applyTwice twice 0 -- gives 8

I simply cannot wrap my head around these results, would love it if someone could explain. I apologize if this is something basic, as I'm just learning Haskell.

Micha Wiedenmann
  • 19,979
  • 21
  • 92
  • 137
thehamzarocks
  • 733
  • 5
  • 10
  • 5
    You need to understand that function application is left-associative, so `applyTwice applyTwice increment` is the same as `(applyTwice applyTwice) increment`. So in order to understand what `(applyTwice applyTwice) increment` does, you need to understand what `(applyTwice applyTwice)` does in general, and then figure out what it does to `increment`. – n. m. could be an AI Jul 11 '21 at 08:50
  • If you think of `applyTwice applyTwice` as `2^2` then `applyTwice . applyTwice` is `2*2` and thus gives the same result :) check out [Church encoding](https://en.wikipedia.org/wiki/Church_encoding#Table_of_functions_on_Church_numerals). Unlike exponentiation `applyTwice @(Int->Int) $ applyTwice @Int` the types don't blow up `applyTwice @Int . applyTwice @Int` – Iceland_jack Jul 12 '21 at 18:04

2 Answers2

7

Let's use the informal notation

iter n f = f . f . f . ....  -- n times

Your applyTwice is then simply iter 2.

From the definition, we immediately get:

(iter n . iter m) f 
= iter n (iter m f)
= (f.f. ...) . ... . (f.f. ...)   -- n times (m times f)
= iter (n*m) f

hence, eta contracting,

iter n . iter m = iter (n*m)    -- [law 1]

We also have

iter n (iter m) 
=  -- definition
iter m . iter m . .... . iter m    -- n times
= -- law 1
iter (m*m* ... *m)                 -- n times
= -- power
iter (m^n)                         -- [law 2]

We then have, writing t for applyTwice:

t = iter 2

t t 
= -- previous equation
iter 2 (iter 2)
= -- law 2
iter (2^2)

t t t
= -- left associativity of application
(t t) t
= -- previous equation
iter (2^2) (iter 2)
= -- law 2
iter (2^(2^2))

t t t t
= -- left associativity of application
(t t t) t
= -- previous equation
iter (2^(2^2)) (iter 2)
= -- law 2
iter (2^(2^(2^2)))

and so on.

chi
  • 111,837
  • 3
  • 133
  • 218
3

There is a lot of complexity hiding behind the scenes in the form of invisible type arguments. What happens if we write these arguments out with -XTypeApplications. I have shortened some of the names.

The following twice is instantiated at twice @Int whose type is second-order.

one :: Int
one = twice (+ 1) 0
      ^^^^^
      | 
      twice @Int
        :: (Int -> Int) 
        -> (Int -> Int)

When you apply twice (order-3) to twice (order-2) the type signature becomes more complicated.

two :: Int
two = twice twice (+ 1) 0
      ^^^^^ ^^^^^
      |     |
      |     twice @Int
      |       :: (Int -> Int)
      |       -> (Int -> Int)
      |
      twice @(Int->Int)
        :: ((Int->Int) -> (Int->Int))
        -> ((Int->Int) -> (Int->Int))

And so forth, when you have twice twice twice they are order-4, order-3 and order-2 respectively:

three :: Int
three = twice twice twice (+ 1) 0
        ^^^^^ ^^^^^ ^^^^^
        |     |     | 
        |     |     twice @Int
        |     |       :: (Int -> Int)
        |     |       -> (Int -> Int)
        |     |
        |     twice @(Int->Int)
        |       :: ((Int->Int) -> (Int->Int))
        |       -> ((Int->Int) -> (Int->Int))
        |
        twice @((Int->Int)->(Int->Int))
          :: (((Int->Int)->(Int->Int)) -> ((Int->Int)->(Int->Int)))
          -> (((Int->Int)->(Int->Int)) -> ((Int->Int)->(Int->Int)))

the last example you gave becomes this monstrosity with order-6, order-5, order-4, order-3, order-2 respectively...

{-# Language TypeApplications #-}

five :: Int
five = twice @((((Int->Int)->(Int->Int))->((Int->Int)->(Int->Int)))->(((Int->Int)->(Int->Int))->((Int->Int)->(Int->Int))))
         (twice @(((Int->Int)->(Int->Int))->((Int->Int)->(Int->Int))))
         (twice @((Int->Int)->(Int->Int)))
         (twice @(Int->Int))
         (twice @Int)
         (+ 1) 0

So this is the type of the first twice!!

twice @((((Int->Int)->(Int->Int))->((Int->Int)->(Int->Int)))->(((Int->Int)->(Int->Int))->((Int->Int)->(Int->Int))))
  :: (((((Int -> Int) -> Int -> Int) -> (Int -> Int) -> Int -> Int)
       -> ((Int -> Int) -> Int -> Int) -> (Int -> Int) -> Int -> Int)
      -> (((Int -> Int) -> Int -> Int) -> (Int -> Int) -> Int -> Int)
      -> ((Int -> Int) -> Int -> Int)
      -> (Int -> Int)
      -> Int
      -> Int)
     -> ((((Int -> Int) -> Int -> Int) -> (Int -> Int) -> Int -> Int)
         -> ((Int -> Int) -> Int -> Int) -> (Int -> Int) -> Int -> Int)
     -> (((Int -> Int) -> Int -> Int) -> (Int -> Int) -> Int -> Int)
     -> ((Int -> Int) -> Int -> Int)
     -> (Int -> Int)
     -> Int
     -> Int
Iceland_jack
  • 6,848
  • 7
  • 37
  • 46
  • I really like this explanation! Is there a convenient way to show these type arguments and inferred types of subexpressions in GHCi? I also wanted to point out that the usual terminology would be “order” here (nesting of function arrows), not “rank” (nesting of `forall` quantifiers). These types are all of rank 0, since they’re not polymorphic. Although I guess it’s odd that we rarely refer to a particular ordinal number, except for “first order” (1) vs. “higher order” (≥2), ditto for “higher rank” and “higher kind”. – Jon Purdy Jul 12 '21 at 05:03
  • 1
    @JonPurdy To know the type of `id` in `id True`, one can write `(asTypeOf _ id) True` and get the error `Found hole: _ :: Bool -> Bool` (parentheses are redundant in this specific case). IIRC, some IDEs like intero can show the inferred type on mouseover. – chi Jul 12 '21 at 07:08
  • 1
    I did it by writing `twice @()` and get a suggested fix. I have suggested IDE integration that automatically inserts invisible arguments, I think this is a secret stumbling block for a lot of people when it comes to polymorphism. Thanks for the rank/order catch, it has been fixed. – Iceland_jack Jul 12 '21 at 07:31