""" I've been spending a lot of time recently working on deliberate practice to learn things. I have a list of things "to learn". And I've decided that I should refocus this blog on talking through the things I'm learning. This blog shall be my rubber duck. Or as is may be in my reality my Domo. """
Lambdas are anonymous functions, which means they are not bound to an identifier. So why Lambdas? They seem simple I know, but I never feel like I quite get them especially in languages like Ruby and Python. I wonder if I think about them incorrectly. I mean I understand javaScript and Scala's anonymous functions. So I wanted to take some time and really think about Lambdas. For a start we can look at the wikipedia.
Things we can learn about lambdas:
An argument sent to a higher order function(That's another conversation).
Constructing the result of a higher order function that returns a function.
Lambdas can be used to encapsulate logic that is only needed briefly to avoid littering code with a lot of one-line normal functions.
In many languages lambdas are introduced by the word Lambda(maybe this is what confuses me).
Most lanaguages have some support for anonymous functions. But they look slightly different in each language. Here are implementations of anonymous functions in various languages. More can be found here.
javaScript // es5 (function(x) { return x * x} ) // ex6 x => x * x
Python convention recommends using named functions defined in the same scope instead of lambdas.
Python def make_pow(n): def fixed_exponent_pow(x): return pow(x, n) return fixed_exponent_pow
Ruby uses both Procs and lambdas and they have slightly different behavior.
Ruby # lambda lambda{|x| x % n == 0} # Proc Proc.new { puts "Hello, world!" }
Scala (x: Int, y: Int): Int => x + y
The scala compiler is able to infer the types of the variables and return values under some circumstances.
In scheme named functions are syntactic sugar for anonymous functions bound to names.
Scheme ;;; the declaration (define (pow num) (* num num)) ;;; is syntatic sugar for (define pow (lambda (num) (* num num))) ;;; we can also use just the lambda ((lambda (num) (* num num)) 6)
Go func() string { return "hello world!" }
Using Lambda's for Sorting:
Lambdas are used in many languages to allow for a non traditional sort.
What does this actually mean though? What is a traditional sort. So a traditional sort assumes that you are using an object(most likely a primitive) which has comparison's defined(<, >, =). The language is able to call the comparison functions and sort the objects.
When isn't a traditional sort good enough? If you want to sort by a derivative of the object(the ones place{n % 10}, the number squared, the word in lowercase{this is common as in most languages Z < a}). So in these situations you pass sort a lambda.
javaScript stocks = [{name: "A", value: 2}, {name: "B", value: 1}, {name: "C", value: 10}] stocks.sort() // [{"name":"A","value":2},{"name":"B","value":1},{"name":"C","value":10}] // sort by value without a lambda function compare(a, b) { if (a.value < b.value) { return -1; } if (a.value > b.value) { return 1; } return 0; }; stocks.sort(compare) // [{"name":"B","value":1},{"name":"A","value":2},{"name":"C","value":10}] // sort by value with a lambda stocks.sort((a, b) => { if (a.value < b.value) { return -1; } if (a.value > b.value) { return 1; } return 0; }); // [{"name":"B","value":1},{"name":"A","value":2},{"name":"C","value":10}]
We see here that we are able to create a function inline without adding the "compare" function to the namespace to complete the sort. Now if we need the comparison repeatedly this is likely not better, but if this calculation happens once or is encapsulated so that the compare function is only used in one place we can help keep the larger namespace clear by using the lambda.
This exists in many languages. Here is an example of a similar function in Python.
Python students = [{"grade": "C"}, {"grade": "A"}, {"grade": "F"}, {"grade": "A"}] sorted(students, key=lambda student: student["grade"]) # [{'grade': 'A'}, {'grade': 'A'}, {'grade': 'C'}, {'grade': 'F'}]
Similar patterns exists for functions like fold(reduce), filter, map, any, and all. These allow you to do something to every element in an array using a function, lambda or proc.
Using Lambdas for Closures:
One use of Lambdas is to take advantage of Closures . On a basic level a closure stores the value of a variable where a function was declared for use when the function is called.
javaScript function getAdder (x) {return (y) => {return x + y}} getAdder(2)(5) // 7
So what are we doing here? the function getAdder is returning an anonymous function where the variable x is bound to the argument which was sent to get adder. This allows us to use the function getAdder to build a bunch of different functions from the lambda being returned(e.g. one can add 1 and one can add 10). This saves us a lot of repetition that we would need to write each of these adders. It also allows us to dynamically determine which functions we need so that we don't have to add a new function to add 5 to a number, we can just use getAdder to dynamically generate that function.
Using Lambda's for Currying:
Currying uses closures to allow us to create a function that can take some the needed arguments in smaller groups. We use this when we only know pieces of the data at a specific point in the program to build the final function by giving it data as it is available.
Ruby def divide(x, y) x / y end def divisor(d) return lambda { |x| divide(x, d) } end divide_by_2 = divisor(2) divide_by_2.call(4) # 2
This looks very similar to our closure example, and they are certainly related. Currying is very common in functional programming(especially Scala).
Now python actually recommends against using map or filter on arrays, which we said were common uses of lambdas. Instead list comprehensions are used. Lets look at an example.
Python nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] [x**2 for x in nums] # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
So this looks very different than the other functions we looked at earlier. So what is happening? we are creating a loop where we say for each x in nums(see at the end there). Do x**2. So x**2 is our lambda here, yeah it's not directly labeled as such as we saw earlier, but that is what it is. So basically this is a rewriting of for loops.
We can also add an if statement to the loop.
Python nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] [ x**2 for x in nums if x % 2 == 0 ] # [4, 16, 36, 64, 100]
The argument for this syntax, it is more readable and more Pythonic, but it's doing the same thing that a map with a lambda would do in another language overall.
One usage of lambda you will see a lot in Python is for reduce.
Python from functools import reduce # Valid in Python 2.6+, required in Python 3 import operator reduce(operator.mul, (3, 4, 5), 1)
Reduce allows us to process the list into a single value. A common use of that is the builtin sum function in python which adds up all the values in a list.
One important distinction is that in Python lambda's are actually syntactic sugar for a normal function definition(See 4.7.5) which is the opposite of languages like Scheme. Because of this lambdas can be used anywhere that a function is expected. This is different than Ruby where lambdas must be invoked with the call method to use them like a function. Lambdas in python are limited to a single expression. (A great tutorial on Python lambdas and why they are confusing)
I was wondering how the code differences actually performed so I ran some very small benchmark tests.
Python from datetime import datetime RUNS = 1000000 x = 2 p = 20 now = datetime.now() for i in range(RUNS): y = x**p print("straight", (datetime.now() - now).microseconds) # ('straight', 174238) def pow(x, p): return x**p now = datetime.now() for i in range(RUNS): y = pow(x, p) print("func", (datetime.now() - now).microseconds) # ('func', 241700) lam = lambda x, p: x**p now = datetime.now() for i in range(RUNS): y = lam(x, p) print("lam", (datetime.now() - now).microseconds) # ('lam', 250180)
I tried testing a function a lambda and straight code to see if I could get an idea of differences in overhead. The results: ('straight', 174238), ('func', 241700), and ('lam', 250180). Confirm what we saw in the recommendations when we looked at python lambdas, they are overall only slightly more efficent than a method definition.
Next I wanted to test differences between map and list comprehensions.
Python nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # These both work print [x**2 for x in nums] print map(lambda x: x**2, nums) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] # it gets a little weird when we start wanting to filter also print [x**2 for x in nums if x % 2 == 0] print map(lambda x: x**2, filter(lambda x: x % 2 == 0, nums)) # [4, 16, 36, 64, 100] # And suddenly the readability argument is really striking home. # Times from datetime import datetime nums = list(range(1000)) RUNS = 100 now = datetime.now() for i in range(RUNS): y = [x**2 for x in nums] print("comprehension", (datetime.now() - now).microseconds) # ('comprehension', 11259) now = datetime.now() for i in range(RUNS): y = map(lambda x: x**2, nums) print("map", (datetime.now() - now).microseconds) # ('map', 15277) now = datetime.now() for i in range(RUNS): y = [x**2 for x in nums if x % 2 == 0] print("comp with filter", (datetime.now() - now).microseconds) # ('comp with filter', 13166) now = datetime.now() for i in range(RUNS): y = map(lambda x: x**2, filter(lambda x: x % 2 == 0, nums)) print("map and filter", (datetime.now() - now).microseconds) # ('map and filter', 71215)
So a couple things happened while I was testing this. First it immediately seemed like a bad idea to use map if I needed to also filter. But I also learned something interesting about myself. I didn't know how to implement a map in Python. I've never done it, it's not Pythonic so I always write comprehensions. So it took me 3 attempts(I could have done it faster if I'd looked at the docss) to get a working map. I think maybe that says the most. Well that or the difference between 13166 and 71215 microseconds, there is really no contest.