Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how lambda works with reduce

I was trying to understand how reduce works through this website. The example they have mentioned is quite good and easy to understand.

http://book.pythontips.com/en/latest/map_filter.html#reduce

a = reduce((lambda x, y: x * y), [1, 2, 3, 4])

The above function will multiple each and every number in list and assign to a.

However, I got totally stumped when I came across following function in project.

def compose(*fns):
    return reduce(lambda acc, fn: lambda *args: acc(fn(*args)), fns, lambda _: _)

could someone help me breakdown this function to understand what it's suppose to do

like image 495
Gaurang Shah Avatar asked Mar 09 '23 10:03

Gaurang Shah


2 Answers

Not very readable at first sight. Let's decompose:

First, def compose(*fns): means that the compose function will receive an unknown number of arguments.

Next, let's decompose the reduce function:

reduce(
   lambda acc, fn: lambda *args: acc(fn(*args)),
   fns,
   lambda _: _
)

As the doc indicates, reduce takes 3 arguments:

def reduce(function, iterable, initializer=None):

So, in your case: the function is lambda acc, fn: lambda *args: acc(fn(*args)), fns is the iterable, and it will be initialize with lambda _: _

The initializer indicates that the arguments of compose will be functions. lambda _: _ is a "neutral element" for function (the same as "0" for addition or "1" for multiplication). I guess it's there essentially when fns is empty.

Now for the main part:

lambda acc, fn: lambda *args: acc(fn(*args))

this is a function that take two functions acc and fn and returns the lambda function lambda *args: acc(fn(*args)).

Let's take an example:

>>> reduce((lambda acc, fn: acc ** fn), [1, 2, 3, 4])
1
>>> reduce((lambda acc, fn: fn ** acc ), [1, 2, 3, 4])
262144

Here acc and fn are not function, but integers. acc is the "accumulated/reduced" so far, and fn is the "next" step.

With functions, it will be the same acc will be the "called functions" so far, and fn the next function.

So lambda acc, fn: lambda *args: acc(fn(*args)) will return a (lambda) function, that will return acc(fn(the_arguments)).

reduce(lambda acc, fn: lambda *args: acc(fn(*args)), fns, lambda _: _) will then returns a function consisting of applying each functions of fns to its args, with identity by default (lambda _: _).

Let's take an example:

>>> def square(x):
...  return x**2
...
>>> def increment(x):
...   return x+1
...
>>> def half(x):
...   return x/2
...
>>> compose(square, increment, half)
<function <lambda> at 0x7f5321e13de8>
>>> g=compose(square, increment, half)
>>> g(5)
9

So, g(x) = square(increment(half(x)))


With Kasramvd's example:

compose(max, min)([[2, 4], [3, 5]])

is the same as:

max(min([[2, 4], [3, 5]]))

min([[2, 4], [3, 5]]) will returns [2,4] and max([2,4]) is 4. Thus compose(max, min)([[2, 4], [3, 5]])=4

like image 188
fredtantini Avatar answered Mar 11 '23 00:03

fredtantini


lambda expressions can be difficult to follow, and especially so with this one, which returns a new function, also defined using lambda.

Here is that same expression, but with some different line spacing:

def compose(*fns):
    return reduce(lambda acc, fn: lambda *args: acc(fn(*args)), 
                  fns,
                  lambda _: _)

Now I'll further expand this by exploding the lambdas passed to reduce as a regular def statements:

def compose_2_fns(f, g):
    # take 2 functions and return a new function that calls the first with the
    # result of calling the second
    def composed(*args):
        return f(g(*args))
    return composed

def _initial(x):
    return x

def compose(*fns):
    return reduce(compose_2_fns, fns, _initial)

Recall that reduce works by giving it a method that takes 2 arguments, a sequence of objects (in this case, a sequence of functions), and an optional initial value.

reduce(reduce_fn, objs, first_obj)

If no initial value is given, reduce will take the first object in the sequence, as if you had called it like:

reduce(reduce_fn, objs[1:], objs[0])

Then the reduce function is called like this:

accumulator = first_obj
for obj in objs:
    accumulator = reduce_fn(accumulator, obj)
return accumulator

So what your posted reduce statement is doing is building up a big function by combining several smaller ones.

functions = (add_1, mult_5, add_3)
resulting_function -> lambda *args: add_1(mult_5(add_3(*args)))

So that:

resulting_function(2) -> (((2 + 3) * 5) + 1) -> 26
like image 43
PaulMcG Avatar answered Mar 11 '23 00:03

PaulMcG