Skip to content

Lambda

Learning Objectives

At the end of this sub-unit, students should

  • understand anonymous function.
  • know how to write anonymous function.

Generalized Operation

Previously, we implemented two functions map_n and filter_n. These two operations are actually quite common. How can we generalize this? Using the technique that we have learnt so far, we can simply say that there is a function F(item) and a predicate P(item)1. We then implement our map_n and filter_n more generally as mapf and filterf.

Map

1
2
3
4
5
def mapf(lst):
  if len(lst) == 0:
    return []
  else:
    return [F(lst[0])] + mapf(lst[1:])

We can then define map_n by first defining F as follows.

def F(item):
  return item * n  # assume n is defined correctly

Filter

1
2
3
4
5
def filterf(lst):
  if len(lst) == 0:
    return []
  else:
    return [P(lst[0])] + mapf(lst[1:])

We can then define filter_n by first defining P as follows.

def P(item):
  return item % n == 0  # assume n is defined correctly

Immediately we the problem with this generalization because there is no place where we can define n correctly. After all, n should only be defined inside the function map_n and filter_n. This is problematic. How does Python implement the generalized version of map and filter?

Anonymous Function

The answer is the concept of an anonymous function. This is known as lambda. Lambda is an expression in Python so it can be used in any place where an expression can be used. In Python, the syntax is shown below.

Syntax

lambda  params  :  expression 

Here, params may be empty. Note that we can only accept expression and not a general statement.

We will not really be discussing the semantics. Instead, we will provide a translation to a named function. But because there is no name for lambda expression, we will use underscore (i.e., _) as the name. The equivalent named function is as follows. We write them side by side.

  • Lambda


    lambda par: expr
    
  • Function Definition


    def _(par):
      return expr
    

What this shows is that if we are confused what the behavior of lambda is, we can imagine that there is a function that perform the computation. The confusing part will be the scoping, which we will touch on later. For now, let us construct a few lambdas to illustrate what anonymous functions we can create. Try to generalize this into several parameters.

lambda : 0  # always 0
lambda : 1  # always 1
lambda x: x + 1  # add 1
lambda x: x * x  # squaring
lambda x, y: x + y  # add x and y
lambda x, y: x * y  # multiply x and y

Anonymous Functions are Functions

Although they are anonymous, they are actually still functions. The following concepts are part of the topic of higher-order functions, but we will only use a simplified version of it. What we want to do now is to assign name to these anonymous function. We do that by assigning the lambda into a variable.

add = lambda x, y: x + y
>>> add(1, 2)
3

Now that it has a name add, we can use it as if add is the following function by our conversion method. This is shown in the sample run above. The equivalent named function is shown below.

def add(x, y):
  return x + y
>>> add(1, 2)
3
Bad Practice

Since lambda is an expression, it can be used in all places where expression can be accepted. In add(1, 2), add is actually an expression. Which means, we can replace add with its lambda definition. This gives us the following near incomprehensible code.

>>> (lambda x, y: x + y)(1, 2)
3

What Lambda Should Do

We already know the capability of lambda. In fact, all lambda can be translated into the equivalent named function. What is the limitation of lambda over named function?

If we look back at the conversion, the first thing we should note is the lack of statement in the named function. The named function only has a single return statement. So the following named function cannot be represented as lambda easily. Still, by some smart conversion below, we can still write it as lambda.

1
2
3
4
def op(x, y, z):
  z = z + y
  y = x * z
  return x + y + z
1
2
3
4
lambda x, y, z:
  z = z + y
  y = x * z
  x + y + z
1
2
3
4
# replace z with (z + y)
# replace y with (x * z)
#   but z is replaced, so we get (x * (z + y))
lambda x, y, z: x + (x * (z + y)) + (z + y)

What about things that lambda really cannot do? Consider a logging mechanism that utilized a mutable list as a log. Here we need complicated tricks to actually represent this as lambda. The trick is to construct an intermediate tuple where the result is the second element of the tuple.

1
2
3
4
log = []
def add(x, y):
  log.append((x, y, x + y))
  return x + y
1
2
3
4
log = []
add = lambda(x, y): (log.append((x, y, x + y)),
                     x + y)[1]
# we ignore the first element of the tuple

As before, instead of forcing our way to solve with lambda, we should embrace lambda as well as its apparent limitation. What you should do with lambda is to create what we call as a pure function. A pure function should only depend on the input parameter and does not modify anything.

Selective Impurities

But pure function is very restrictive. The example using log shows that we can actually use the variables from the scope. So we relaxed our restriction a little bit. There is no name for this kind of function but we can say that this is a straightforward function. Note that this is our own naming convention.

Straightforward Function

A straightforward function is a function that does not modify anything and uses only the parameters or the variable from the scope (or both).

Using straightforward function, we can actually have a way to define F(item) and P(item) that can use n correctly. We do this by defining F(item) and P(item) inside the function map_n and filter_n. Then F(item) and P(item) are both straightforward function because n is now part of the scope.

There is still one more component that is missing to fully use this in mapf and filterf. What is missing is the way for mapf and filterf to use F(item) and P(item) that is defined inside map_n and filter_n. We will use the following trick that you do not have to understand.

The main thing to note is that it can be done. Python has an implementation of map and filter so that we do not have to use such tricks. We only need to know that there is a way.

Map

1
2
3
4
5
6
F = [None]
def mapf(lst):
  if len(lst) == 0:
    return []
  else:
    return [F[0](lst[0])] + mapf(lst[1:])

We can then define map_n by first defining F as follows.

1
2
3
def map_n(lst, n):
  F[0] = lambda item: item * n  # straightforward!
  return mapf(lst)

Filter

1
2
3
4
5
6
P = [None]
def filterf(lst):
  if len(lst) == 0:
    return []
  else:
    return [P(lst[0])] + mapf(lst[1:])

We can then define filter_n by first defining P as follows.

1
2
3
def filter_n(lst, n):
  P[0] = lambda item: item % n == 0  # straightforward!
  return filterf(lst)

  1. Recap, a predicate is simply a function that returns a boolean value.