0

I don't understand how foo() function is working with that 2 lambdas, this 3 functions togheter perform the factorial calculation.

5/10/2020 UPDATE: I modified the code to understand better how those lambdas are working using global variables and counters inside each function.

"""7. What math operation does the following perform?"""

foo_counter = 0
bar_counter = 0
baz_counter = 0


def foo(f):
    global foo_counter
    foo_counter += 1
    print("foo = %d" % foo_counter)
    return (lambda x: x(x))(lambda x: f(lambda *args: x(x)(*args)))


def bar(f):
    global bar_counter
    bar_counter += 1
    print("bar = %d" % bar_counter)
    return lambda n: (1 if n < 2 else n * f(n - 1))


def baz(n):
    global baz_counter
    baz_counter += 1
    print("baz = %d" % baz_counter)
    return foo(bar)(n)

print(baz(7))

Output:

baz = 1
foo = 1
bar = 1
bar = 2
bar = 3
bar = 4
bar = 5
bar = 6
bar = 7
5040

Process finished with exit code 0

So basically, baz() call bar() using a weird double () () notation foo(bar)(n) and then as @Igor Mikushkin said, foo() is passing the values to bar() using 2 lambdas and also defining function f(lambda *args: x(x)(*args))) inside which finally calls bar() which is the function that performs the factorial. But even with that I don't understand the logic behind, hope someone could help us to understand.

3 Answers3

3

That is the Y-combinator implemented using the U-combinator.

The U and Y combinators both enable recursion using only lambdas. These examples are fine as a learning tool and can teach you about the amazing capability of lambdas and the closure property. However, it makes sense to see the expression in a more familiar form -

def foo(f):
  # ... 
  (lambda x: x(x))(lambda x: f(lambda *args: x(x)(*args)))  #wtf?

Is effectively the same1 as -

def foo(f):
  # ...
  return f(lambda *x: foo(f)(*x))  # try it and see ...

Or because bar returns a single-parameter lambda, we can simplify a bit more -

def foo(f):
  # ...
  return f(lambda x: foo(f)(x))

With eta reduction, that is the same as -

def foo(f):
  # ...
  return f(foo(f))

Which is alpha equivalent to the Y-combinator -

def Y(f):
  # ...
  return f(Y(f))  # a wild Y appears!

However, because the eta-reduced form causes immediate recursion of Y in an applicative-order language (like Python), a stack overflow guaranteed. Leaving the eta-expansion in place allows us to use Y safely -

def Y(f):
  return f(lambda x: Y(f)(x)) # eta expanded Y(f)

def fact (recur):
  def loop (n):
    if n < 1:
      return 1
    else:
      return n * recur(n - 1) # <-- uses recur to recur
  return loop

def fib (recur):
  def loop (n):
    if n < 2:
      return n
    else:
      return recur(n - 1) + recur(n - 2) # <-- uses recur to recur
  return loop

print(Y(fact)(7)) # 5040
print(Y(fib)(10)) # 55

Notice how fact and fib never call themselves by name. Instead, the recursion mechanism is passed as an argument to the function, recur. And instead of returning a result directly, we return a function, loop, which can recur when recur is called.

Python supports recursion though, so this is all just a big lambda song and dance around this more idiomatic program -

def fact (recur):
  def loop (n):
  def fact (n):
    if n < 1:
      return 1
    else:
      return n * recur(n - 1)
      return n * fact(n - 1)  # <-- recur by name; call fact
  return loop

def fib (recur):
  def loop (n):
  def fib (n):
    if n < 2:
      return n
    else:
      return recur(n - 1) + recur(n - 2)
      return fib(n - 1) + fib(n - 2)  # <-- recur by name; call fib
  return loop


print(Y(fact)(7))
print(fact(7)) # 5040

print(Y(fib)(10))
print(fib(10)) # 55

more than one function argument?

Above we see fact and fib as single-parameter functions. Can this pattern work with functions that accept more arguments?

Before we see Y used in more complex scenarios, let's first look at a curried function in Python using def -

def func (x):           # func accepts x and returns inner1
  def inner1 (y):       # inner1 accepts y and returns inner2
    def inner2 (z):     # inner2 accepts z and returns x + y + z
      return x + y + z
    return inner2
  return inner1

func(3)(3)(3) # 9

Now that same function using lambda. Note \ is used for a line break in Python -

func = lambda x: lambda y: lambda z: \
  x + y + z

func(3)(3)(3) # 9

Okay now that we know those two forms are identical, let's put Y to work -

Y = lambda f: \
  f(lambda x: Y(f)(x))

range = lambda r: lambda start: lambda end: \
  [] if start > end else [ start, *r(start + 1)(end) ]

reduce = lambda r: lambda f: lambda init: lambda xs: \
  init if not xs else r(f)(f(init, xs[0]))(xs[1:]) 

add = lambda a, b: \
  a + b

sum = \
  Y(reduce)(add)(0)

nums = Y(range)(3)(9)

print(nums) # [ 3, 4, 5, 6, 7, 8, 9 ]
print(sum(nums)) # 42

but wait...

So if the Y-combinator is meant to enable recursion, why does it have a recursive definition?

Y = lambda f: \         # Y = ...
  f(lambda x: Y(f)(x))  #   recur  with Y ??

This is a simple way to show how Y works, but offloading the actual recursion to Python kinda feels like a cheap trick. Prepare to go off the rails entirely...

U-combinator enters the scene -

U = lambda f: \
  f(f)                         # <-- no named recursion

Y = \
  U(lambda r: lambda f: \
    f(lambda x: r(r)(f)(x)))   # <-- no named recursion

fact = lambda r: lambda n: \
    1 if n < 1 else n * r(n - 1)

print(Y(fact)(7))
# 5040

That's, U. By passing a function to itself as an argument, a function can recur using its parameter instead of its name!

Now because all our functions are pure and nameless, we can show skip intermediate assignments and show lambdas inline -

# print(Y(fact)(7))
print( \
  (lambda f: \
    f(f)) \

  (lambda r: lambda f: \
    f(lambda x: r(r)(f)(x))) \

  (lambda r: lambda n: \
    1 if n < 1 else n * r(n - 1)) \

  (7) \
)
# 5040

The same can be done in the sum(range) example -

# sum = Y(reduce)(add)(0)
sum = \
  (lambda f: \
    f(f)) \
  (lambda r: lambda f: \
    f(lambda x: r(r)(f)(x))) \
  (lambda r: lambda f: lambda init: lambda xs: \
    init if not xs else r(f)(f(init, xs[0]))(xs[1:])) \
  (lambda a, b: \
    a + b) \
  (0)

# nums = Y(range)(3)(9)
nums = \
  (lambda f: \
    f(f)) \
  (lambda r: lambda f: \
    f(lambda x: r(r)(f)(x))) \
  (lambda r: lambda start: lambda end: \
    [] if start > end else [ start, *r(start + 1)(end) ]) \
  (3) \
  (9)

print(sum(nums))
# 42

And as a single pure expression -

# (sum)((range(3)(9)))
print( \
  ((lambda f: \
    f(f)) \
  (lambda r: lambda f: \
    f(lambda x: r(r)(f)(x))) \
  (lambda r: lambda f: lambda init: lambda xs: \
    init if not xs else r(f)(f(init, xs[0]))(xs[1:])) \
  (lambda a, b: \
    a + b) \
  (0)) \
  ((lambda f: \
    f(f)) \
  (lambda r: lambda f: \
    f(lambda x: r(r)(f)(x))) \
  (lambda r: lambda start: lambda end: \
    [] if start > end else [ start, *r(start + 1)(end) ]) \
  (3) \
  (9)) \
)
# 42

I recommend you check out this Q&A for further explanation


1technically...

it's not exactly the same. In the orignial -

def foo(f):
  global foo_counter             
  foo_counter += 1                 # side effect 1
  print("foo = %d" % foo_counter)  # side effect 2
  return (lambda x: x(x))(lambda x: f(lambda *args: x(x)(*args))) # repeats f

By directly using U-combinator (lambda x: x(x)), direct recursion of f is made possible without repeating the side effects 1 and 2.

When we rewrite foo without the U-combinator, we recur foo (instead of just f) and so side effects 1 and 2 are repeated -

def foo(f):
  global foo_counter             
  foo_counter += 1                 # side effect 1
  print("foo = %d" % foo_counter)  # side effect 2
  return f(lambda *x: foo(f)(*x))  # repeats all of foo

This is a minor difference but worth mentioning as it shows the disruptive quality of side effects.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Thanks @Thank you. A lot for the explanation. I will analyze your response! – Axel G. Aguilar May 11 '20 at 10:44
  • Actually this should be accepted answer. I removed my one as this code should be explained in terms of combinators – Igor Mikushkin May 11 '20 at 12:35
  • Thanks @Igor Mikushkin, I wanted to understand also your example but if it is the same explanation as *Thank you* provide it's ok. I appreciate your time and I'll let you know if I have another questions. – Axel G. Aguilar May 11 '20 at 13:14
  • @IgorMikushkin your answer is wonderful and I think you should keep it. I purposefully glazed over some of those things because your explanations covered them so well :D – Mulan May 11 '20 at 15:23
  • 1
    @Thankyou Thank you :) I just felt it shows a wrong direction due to too general advices. It was like the explanation of a house in terms of concrete volumes. Higher-order functions and combinators are actually a foundation and walls in this metaphor. However I think it looks OK after edit – Igor Mikushkin May 11 '20 at 18:34
2

Let's start from the syntax. This invocation foo(bar) returns a function. Then you call it with argument 7: foo(bar)(7). You may rewrite print(foo(bar)(7)) as

f = foo(bar)
print(f(7))

This concept is called higher-order function. You may want to dig into it before trying to understand the puzzle you posted. In Python functions are first-class objects. They are treated as values. You can call a function with a function as a parameter. You can return a function from a function as well.

The code itself is a puzzle. And I have to say it is tough one. It is certainly not the best way to learn higher-order functions because it takes this concept and lift it to the Moon. I advise you to return to it later when you will have deep understanding of the concept. So instead of its explanation I suggest to take a simpler example.

from typing import Callable

def g(x: int) -> int:
    return x*x

def f() -> Callable[[int], int]:
    return g

#1
print(f()(2))

#2
print((f())(2))

#3
h = f()
print(h(2))

For your better understanding I introduced type annotations. So what is happening here? Function f returns a function g that gets an int and returns an int. You call f and get this function g. Then you call returned function with a parameter 2. Here #1, #2, and #3 are absolutely equivalent. I added parentheses to #2 to underline an order of execution. If it is not enough for understanding I recommend you to google first-class functions and higher-order functions.

Regarding the puzzle. The function bar gets a function as an argument and returns a function. It is obvious that both the argument and the returned function should be equivalent. It comes from a factorial recursive definition. So foo is a very fancy way to call some function with a result of this function. It is explained very well in terms of combinators here https://stackoverflow.com/a/61721540/380247. After reading this answer it is obvious that the example is intended for people who knows combinatory logic and are able to find combinator patterns in a code. Although it is possible to understand the code by decomposing it and applying type annotations it is just not the right direction. So I removed my previous advices. Especially you have to deal with infinite function types that are not fully representable in Python type system such as the type of lambda x: x(x). So summarizing to understand it you need to know

  • Higher-order functions and closures
  • Combinatory logic
Igor Mikushkin
  • 1,250
  • 16
  • 25
  • Hi @Igor Mikushkin thanks a lot for your answer. Yes, It is something like a puzzle, actually it is a question founded in a python quiz. I will try to translate those lambdas in order to understand better what is happening. However, talking about that weird syntax with double parenthesis, what is happening? how Python interpreter manage baz function `foo(bar)(7)` inside `foo()` invocation is the name of baz function but the real parameter is outside the invocation. Also in `foo` function it is doing the same double () notation `(lambda x: x(x))(lambda x: f(lambda *args: x(x)(*args)))` – Axel G. Aguilar May 10 '20 at 18:52
  • @AxelG.Aguilar I updated my answer. Hope this helps. – Igor Mikushkin May 10 '20 at 21:42
  • @AxelG.Aguilar One more note. The question is "What math operation does the following perform?". So you may want not to fully understand this exact code but try to simplify it. Or you can just print its result for different values and try to figure out what the math function is. – Igor Mikushkin May 10 '20 at 22:09
  • Hello @Igor Mikushkin, yoyu are right. It is interesting to dig in the concept of higher order functions. I will process the information provided. Also it is cool that you said that it is a tough one :/ - thanks a bunch! – Axel G. Aguilar May 11 '20 at 10:47
0

I discover that doing this:

print(foo(bar)(7))

Will also return 5040

So, what double parenthesis are doing in syntax is something like this?

function_1(function_2)(argument))

I didn't know that it was a valid way to call a function