Lambdas: what do you mean I will use them?

Python has lambdas, but with python, the lambda is a very reduced lambda that Guido even hesitated about calling lambda.  I would suggest that with python, almost everything that can be done using lambdas, will be easier to understand if rewritten without the lambda.

Main Points of the page:

Background

Moving from python to kotlin in regards to lambdas, is not about the simple change in syntax. A major consequence of the syntax change is that lambdas play a greatly expanded role in programs written in the kotlin idiom. As the full power of kotlin lambdas becomes clear, a whole new tool in coding is revealed.

This page reviews lambdas, explains why they limited and usually undesirable in python but not limited and highly desirable in kotlin. TL:DR: read the headings and cherry pick reading which sections to read.

Ok, what is a lambda again?

Basically, lambdas are a ‘literal code’.  Imagine if you could not use literals in function calls or expressions, and instead literals have to be defined in a special ‘literal’ statement. The ‘hello world’ program would become:

lit greeting: "Hello World"
print(greeting)

simple code would get longer:

#this code would no longer be legal
people = 3
ears = people * 2

#instead we would have this
lit people: 3
lit doubler: 2
ears = people * doubler

Now it could be argued that by forcing our literals to have names, the code becomes more self documented. But it could be that the role of  “hello world” is no clearer by having it named ‘greeting’ and it could even be that naming ‘2’ doubler actual is confusing rather than helpful.  Note that while the declaration of ‘people’ can no longer use a general expression, this change has little real impact.

While with strings and integers forcing a special declaration and not allowing literals just seems silly, without lambdas, code literals are always moved to a special declaration. Here is an example in python declaring code with the special ‘def’ syntax, and also in the same way any other variable is declared by using a lambda.

def last_name(full_name_string):
     return full_name_string.split()[-1]
last_name_2 = last_name
last_lambda = lambda x: x.split()[-1]

>>> last_name("bill smith")
'smith'
>>> last_name2("fred bloggs")
'bloggs'
>>> last_lambda("tom jones")
'jones'

This is not an example of the real use of lambdas, but to illustrate how a ‘def’ is just giving a name to a block of code.  All three variables reference functions that work exactly the same.  But to define ‘last_name’, the ‘def’ syntax was used.  It may not even seem obvious that the ‘def’ was simply defining a variable ‘last_name’ and giving it the value of a block of code. Once defined, ‘last_name’ works like any other variable, only the definition requires a special syntax.  In contrast ‘last_lambda’ is defined like any other variable, direct from the code.  The lambda code works just like a literal for code, allowing use of code anywhere.

The above example may help with understanding, but it not the case where ‘code literals’ are needed.   In the example with numeric literals, forcing a declaration to have a special syntax did not really increase the code needed  ( syntax of ‘lit people: 3’  vs ‘people = 3’).  It was not being able to use a literal mid statement that resulted in more code.

ears = people * 2
# with no literals in an expression became
lit doubler: 2
ears = people * doubler

This is what happens with code without lambdas.  A forced declaration (using ‘def’) in place simply inserting the code where it is needed. It is where ‘code literals’ can be used mid expression that lambdas are really useful.

Simple example Use of Python Lambdas

def last_name(full_name_string):
  return full_name_string.split()[-1]
>>> my_list = ["bill smith", "fred blogs", "tom jones"]
>>> sorted(my_list, key=last_name)
['fred blogs', 'tom jones', 'bill smith']
>>> sorted(mylist, key=lambda full_name_string: full_name_string.split()[-1])
['fred blogs', 'tom jones', 'bill smith']

This is an example of the real use of lambdas.  This code takes a list of strings that are initially sorted alphabetically using the overall string, which results in sorting by first name, and re-sorts the list and returns the same list sorted by last name.

The code contrasts using a ‘def’ to define the code block, with of course gives the block of code a name and assigns it to a variable, with the lambda form where the code is a ‘literal’ within the call to sorted. Certainly the lambda saves declaring the function and saves two lines of code. The lambda also keeps the code definition where it is used, but not everyone will find things that readable.

Introducing Kotlin Lambdas.

The approach in Kotlin is to make lambdas, or “code literals” as easy as using String literals or Int literals. In fact even the “hello world” can have a lambda added.

fun main(args: Array){
   println({"hello world"}())
}

The block ‘{ “hello world” }’ acts as a simple lambda function that returns the string hello world.  Lambdas return the value of the last statement in the lambda, and in this example there is only one statement. If you try this without the () brackets, it will print “() -> kotlin.String” because what is being printed is a an unnamed function that returns a kotlin string.

So creating a lambda is as simple as creating a code block. This demonstrates how you can create a lambda code block anywhere, but is not a typical use case. The () after the lambda block execute the code at that point, but the main case for lambda is to pass the code block to a function, so the receiving function can then have the () and execute the lambda at later time.

Lambdas as Function Parameters

The main use case for lambdas is to pass code snippets to functions. Functions which accept functions as parameters (or return functions as a result) are called higher-order functions.  In kotlin, all function parameters specify the type of data they will accept, so the function parameters definition has to specify that the parameter will be a function.

fun addem1(p1: Int, p2:()->Int):Int{
   return p1 + p2()
}
fun addem2(p1:(number: Int)->Int, p2:(String, String)->Int):Int{
    return p1(1) + p2("1", "b")
}

val added1 = addem1(1, {2})

val added2 = addem2( {it}, {string1, _ -> string1.toInt() })

The above example, the addem1 function has p1 as simple Int parameter for comparison, and p2 as a function with no parameters that returns an Int.

The addem2 function has p1 declared as accepting a lambda with a single Int parameter  named ‘number’ and returning an Int, and p2 declared with as accepting a lambda with two String parameters without names specified and again returning an Int.  The optional parameter name ‘number’ in the function declaration purely helps prompt as to the use of the parameter at the time the function is being called, and is not used elsewhere.

In the call to addem2, the first lambda supplied is ‘{it}’.  Within lambdas that accept parameters, the list of names of the parameters separated by ‘,’ characters and followed by the ‘->’ appears prior to the code.  However, when there is one parameter, this parameter is named ‘it’ by default, so ‘{it}‘ is the same as ‘{ it -> it }‘ which is ‘my parameter is named it, and the result is the value of this parameter‘.   The second lambda names  names the parameters ‘string1’ and ‘‘ , and then returns of the lambda returns ‘string1’ as an Int.  The name ‘‘ is reserved to indicate the parameter is not used.  An unused parameter can have any name, but the ‘_’ name documents the parameter will not be used.

The Python Example in Kotlin

Now, the practical example of the same solution that was provided above in python.  Below are two examples of the previous ‘last name’ sort. The first example is ‘beginner’ mode, with the second example in the preferred form.

val lst = listOf("bill smith", "fred blogs", "tom jones")

// first example....no 'shortcuts' used, the long way
lst.sortedBy({fullName -> fullName.split(" ").last()})
[ fred blogs, tom jones, bill smith ]

// now...second example the preferred way
lst.sortedBy{it.split(" ").last()}
[ fred blogs, tom jones, bill smith ]

Note that at the start of the block we can give names to the parameters ” fullName ->” is declaring the parameter as “fullName”.  The block then proceeds as any other block.  There is no limit to how many statements in the block, nor on having constructs such as “if” or “when” or loops within the block.  The final statement executed in the block declares the return value.

The ‘preferred way’ example, has two changes from the first ‘no shortcuts’ solution. Consider these two sentences in English, “I am looking for the correct key, is the correct key long?  And is the correct key also grey?”   Now consider “I am looking for the correct key, is it long? And is it also grey?”   Repeating the name when what we are talking about is not necessary and the name can be replaced by “it”.  Kotlin takes the same approach. Instead of the “lastName ->” to give a name to what we are talking about, the parameter can just be called “it” during our block.  So the “lastName ->” at the beginning of the block can be omitted and when in place of ‘lastName.split(” “)’ the code just has ‘it.split(” “)’. Like in English, ‘it’ should only be clear when it is obvious what we mean by “it”, but using “it” is then often preferable to the longer way of saying what we mean.

So, firstly, omitting the parameter declaration gives the parameter a name of ‘it’ by default, reducing the clutter for simple lambdas such as this.

Next the brackets, ( ), around the parameter list to ‘sortedBy’ have been omitted. In place of ‘({it.split(” “)}), the code has simply ‘{it.split(” “)}.

This is actually the result of two separate rules which are designed to make code using lambdas present the problem in clearer manner.  Firstly, the lambdas can follow the parameter list, to avoid needing to have brackets around the code.  This become most important when function calls are nested.  Secondly, if a function with lambda values as parameters would otherwise have an empty parameter list, then the brackets may be omitted. See the following:

// first ....no 'shortcuts' used, the long way
lst.sortedBy({lastName -> lastName.split(" ").last()})

// next step use 'it' to save an actual name for parameter
lst.sortedBy({ it.split(" ").last() })

// and now we can have our lambda move after the parameter list for 'sortedBy'
lst.sortedBy(){ it.split(" ").last()}

// finally, sortedBy now has an empty parameter list () ..so the () can be omitted
lst.sortedBy{ it.split(" ").last() }

Beyond the Limitations of Python Lambdas

Kotlin Lambdas can inherit from a target environment, python Lambdas cannot.

Consider the use of kotlin in dsls like kotlinx.html. The DSL (domain specific language) is built using lambdas. The lambdas of the DSL can have their own language, because they have access to an enclosing environment. In addition to the variables and methods of the enclosing scope of the lambda, there can be an entire additional vocabulary from a target environment. There is no parallel to this in python, and the power comes from the combination of lambdas and extension functions.

The class for the enclosing class prefixes the () in the parameter definition.  In the below example ‘func: ()-> Unit’ would be a function that takes no parameters and returns the ‘Unit’ value (Unit is the equivalent of the python None).  “func: TestEnv.()->Unit’ is a lambda that will be an extension function for an instance of the TestEnv class.

Consider this code:

class TestEnv(){
    var envVar = 3
    fun testInEnv(func: TestEnv.()->Unit){
        func()
    }
    fun envDoubler(value: Int) = value * 2
}
fun main(args: Array){
    val test = TestEnv()
    val newNum = 4
    test.testInEnv ({ envVar = envDoubler(newNum) })
}

The lambda code can set ‘envVar’ in the target environment to a new value, and use the ‘envDoubler’ method for the calculation. This is simplistic example to keep the sample code concise, but imagine a class with an embedded file or socket connection and the power of allowing lambda code to access that file or socket. Providing access to an additional environment revolutionises the power of lambdas in many way other than enabling DSLs.

Significant whitespace is great, but it killed the lambda.

Lambdas in python are just clumsy. I love the significant whitespace of python. It ensures easy to read code, and solves the ‘balancing braces’ challenge of other languages. But it is hard to escape the fact a good lambda syntax and significant whitespace are difficult to combine.

Python blocks Rule! (But don’t do lambdas)

The rules for a block in python is that a block can follow any python line that ends with a ‘:’. A block is indented from the line above, and ends when the line following the block is no longer indented.

To write a block in a ‘braces language’ like kotlin, most blocks will need a dedicated line following the block to just the closing brace. Without this extra ‘}’ line the code is less readable. In python a cleanly written minimal ‘block’ requires no additional lines, only one line for line of code in the block. The code is always readable without decisions by the programmer to make the code readable, and the code simple requires one less line.

These extra lines can add up and make code longer, and it is easy to get the indenting wrong making the code hard to read. Winner: python.

Python syntax: python blocks kill the lambda

With lambda, you need to place a block in the middle of an expression, and you just cannot do that with a conventional python block. So python has a custom syntax, where the lambda code is limited to just one expression, and unlike every other ‘:’ in python, a block cannot follow the ‘:’ in a lambda. The syntax is

lambda :

Reasons to avoid lambdas in python:

    • A def is often more readable
    • Multi statement blocks are not even possible using lambdas
    • List iterations (and dict and tuple iterations) replace some of the most common lambda usecases

No ‘like lambda to the…’ jokes here! The result is that python lambdas are crippled.

.

Conclusion.

Kotlin allows for extremely powerful lambdas, with no limitation to a single expression, as well as very concise and highly readable lambda code.  This changes how problems are solved by making lambdas a far more important part of solutions in the idiom of the language.

4 thoughts on “Lambdas: what do you mean I will use them?”

Leave a comment