circuitpython_lambda.png
MIT's introduction to computer science class as a 'badge' that honors the lambda as an anonymous function that can make functions

Notice that since we've pulled the good_enough and improve functions out, they're not hidden out of sight any more. Python has another capability that we can use to address this: lambdas.

Lambdas are another great thing that comes from the language Lisp. A lambda is essentially an anonymous function: a function object with no name. You might say "Then how do you use it if you can't get to it by using it's name?" That's a valid point, except that lambdas are meant to be throw away functions: used once and discarded so there's seldom a reason to hold onto them. Combine this with the ability to pass functions into other functions and be returned by them, and we have some pretty cool abilities.

The syntax to create a lambda is fairly simple:

lambda parameter_1, ..., parameter_n : expression

The one limitation of lambdas in Python is that they can contain just a single expression that gets implicitly returned. That's not usually a problem, since it's all that's typically needed.

Let's revisit that last example.

def sqrt_good_enough(x, guess):
    def square(a):
        return a * a
    return abs(square(guess) - x) < 0.001

def sqrt_improve(x, guess):
    def average(a, b):
        return (a + b) / 2
    return average(guess, x / guess)

We will start by inlining the square and average functions.

def sqrt_good_enough(x, guess):
    return abs(guess * guess - x) < 0.001

def sqrt_improve(x, guess):
    return average((guess + x / guess) / 2)

So now each of these is the return of a single expression. They can easily be replaced by lambdas:

sqrt_good_enough = lambda x, guess: abs(guess * guess - x) < 0.001

sqrt_improve = lambda x, guess: (guess + x / guess) / 2

This creates two lambdas and assigns them to variables that have the name we used for the original functions. Now we can do the same as we did before:

sqrt = solver(sqrt_good_enough, sqrt_improve, 1.0)
>>> sqrt(25)
5.00002

However, we can do one better. If we don't need to save these lambdas (and as I said, we seldom do), we can just create them and pass them directly into solver:

sqrt = solver(lambda x, guess: abs(guess * guess - x) < 0.001, 
              lambda x, guess: (guess + x / guess) / 2, 
              1.0)
>>> sqrt(25)
5.00002

In a similar vein, the make_adder function from before can be drastically simplified by returning a lambda:

def make_adder(x):
    return lambda a: a + x

Something Hardware Related

This example is from a project for an upcoming guide (at the time of writing this) so not all the code is finished or available. But enough around this example is to be a good demo.

The project uses a Crickit and makes heavy use of digital IO. How many still isn't certain, but it will be more than the Crickit or the MCU board provides by themselves. That means some Crickit IO will be used as well as some on-board IO. These will be primarily driven by mechanical switches which will need to be debounced.

I ported a debouncer class to CircuitPython for a previous project that worked directly against an on-board digital input. That wouldn't work for the new project since dealing with the two input sources is done differently. The solution was to generalize the debouncer to work with a boolean function instead of a pin.

Here's the two relevant methods (we'll have a deep dive into CircuitPython's object-oriented features in a later guide):

class Debouncer(object):
    ...
    
    def __init__(self, f, interval=0.010):
        """Make an instance.
           :param function f: the function whose return value is to be debounced
           :param int interval: bounce threshold in seconds (default is 0.010, i.e. 10 milliseconds)
        """
        self.state = 0x00
        self.f = f
        if f():
            self.__set_state(Debouncer.DEBOUNCED_STATE | Debouncer.UNSTABLE_STATE)
        self.previous_time = 0
        if interval is None:
            self.interval = 0.010
        else:
            self.interval = interval

    ...

    def update(self):
        """Update the debouncer state. Must be called before using any of the properties below"""
        now = time.monotonic()
        self.__unset_state(Debouncer.CHANGED_STATE)
        current_state = self.f()
        if current_state != self.__get_state(Debouncer.UNSTABLE_STATE):
            self.previous_time = now
            self.__toggle_state(Debouncer.UNSTABLE_STATE)
        else:
            if now - self.previous_time >= self.interval:
                if current_state != self.__get_state(Debouncer.DEBOUNCED_STATE):
                    self.previous_time = now
                    self.__toggle_state(Debouncer.DEBOUNCED_STATE)
                    self.__set_state(Debouncer.CHANGED_STATE)

The relevant thing is the instance variable f. It's a function that gets passed in to the Debouncer constructor and saved. In the update function/method it's used to get a boolean value that is what's being debounced. The function f is simply some function that has no parameters and returns a boolean value.  In the main project code some debouncers get created. Let's have a look at what those functions are that are being passed to the debouncers.

def make_onboard_input_debouncer(pin):
    onboard_input = DigitalInOut(pin)
    onboard_input.direction = Direction.INPUT
    onboard_input.pull = Pull.UP
    return Debouncer(lambda : onboard_input.value)

    
def make_criket_signal_debouncer(pin):
    ss_input = DigitalIO(ss, pin)
    ss_input.switch_to_input(pull=Pull.UP)
    return Debouncer(lambda : ss_input.value)

Here we have two functions: one to create a debouncer on an on-board digital pin, and one to create a debouncer on a crickit (aka Seesaw) digital pin. The setup of the pin's direction and pullup is a bit different in each case. Each of these sets up the pin as appropriate and creates (and returns) a debouncer using a lambda that fetches the pin's value. This is an example of a closure as well: the pin that the lambda fetches the value of is part of the scope (of the respective make_*_debouncer function) that goes along with the lambda. As far as the debouncer is concerned, it's a function that returns a boolean; it neither knows or cares how what the function does beyond that. It could be applied to any noisy boolean function, for example reading the Z value from an accelerometer to determine whether a robot is level. A Z value greater than 9 m/s2 would be a good indicator of that*. As the robot moves about, that value won't be steady. Debouncing would clean it up and answer the question "is the robot level" as opposed to "is the robot level at the instant the accelerometer was read". Assuming sensor is an accelerometer interface object that is in scope, the code to create that debouncer would be something like ():

robot_level = Debouncer(lambda : sensor.accelerometer[2] >= 9.0)

*Assuming the robots is on the surface of the Earth where the gravitational constant it ~ 9.8 m/s2.

This guide was first published on Aug 08, 2018. It was last updated on Mar 31, 2024.

This page (The Function With No Name) was last updated on Mar 08, 2024.

Text editor powered by tinymce.