Hacking together a multi-statement lambda isn't quite as bad as pyrospade makes out: sure we *could* compose a bunch of monadic functions using bind, like in Haskell, but since we're in the impure world of Python, we might as well use side-effects to achieve the same thing.
I cover a few ways to do this on [my blog](http://chriswarbo.net/blog/2012-11-17-anonymous_closures_in_python.html).
For example, Python guarantees to evaluate the elements of a tuple in order, so we can use `,` much like an imperative `;`. We can replace many statements, like `print`, with expressions, like `sys.stdout.write`.
Hence the following are equivalent:
def print_in_tag_def(tag, text):
print "<" + tag + ">"
print text
print "</" + tag + ">"
import sys
print_ = sys.stdout.write
print_in_tag_lambda = lambda tag, text: (print_("<" + tag + ">"),
print_(text),
print_("</" + tag + ">"),
None)[-1]
Note that I've added a `None` at the end, and extracted it using `[-1]`; this sets the return value explicitly. We don't have to do this, but without it we'd get a funky `(None, None, None)` return value, which we may or may not care about.
So we can sequence IO actions. What about local variables?
Python's `=` forms an statement, so we need to find an equivalent expression. One way is to mutate the contents of datastructure, passed in as an argument. For example:
def stateful_def():
foo = 10
bar = foo * foo
foo = 2
return foo + bar
stateful_lambda = (lambda state: lambda *_: (state.setdefault('foo', 10),
state.setdefault('bar', state.get('foo') * state.get('foo')),
state.pop('foo'),
state.setdefault('foo', 2),
state.get('foo') + state.get('bar'))[-1])({})
There are few tricks being used in `stateful_lambda`:
- The `*_` argument allows our lambda to take *any* number of arguments. Since this allows *zero* arguments, we recover the calling convention of `stateful_def`.
- Calling an argument `_` is just a convention which says "I'm not going to use this variable"
- We have one ("wrapper") function returning another ("main") function: `lambda state: lambda *_: ...`
- Thanks to [lexical scope](http://en.wikipedia.org/wiki/Scope_(computer_science)#Lexical_scoping), the argument of the first function will be in-scope for the second function
- Accepting some arguments now and returning another function to accept the rest later is known as [currying](http://en.wikipedia.org/wiki/Currying)
- We immediately call the "wrapper" function, passing it an empty dictionary: `(lambda state: ...)({})`
- This lets us assign a variable `state` to a value `{}` without using an assignment statement (eg. `state = {}`)
- We treat keys and values in `state` as variable names and bound values
- This is less cumbersome than using immediately-called lambdas
- This allows us to mutate the values of variables
- We use `state.setdefault(a, b)` instead of `a = b` and `state.get(a)` instead of `a`
- We use a tuple to chain together our side-effects, like before
- We use `[-1]` to extract the last value, which acts like a `return` statement
Of course this is pretty cumbersome, but we can make a nicer API with helper functions:
# Keeps arguments and values close together for immediately-called functions
callWith = lambda x, f: f(x)
# Returns the `get` and `setdefault` methods of a new dictionary
mkEnv = lambda *_: callWith({},
lambda d: (d.get,
lambda k, v: (d.pop(k), d.setdefault(k, v))))
# A helper for providing a function with a fresh `get` and `setdefault`
inEnv = lambda f: callWith(mkEnv(), f)
# Delays the execution of a function
delay = lambda f x: lambda *_: f(x)
# Uses `get` and `set`(default) to mutate values
stateful_lambda = delay(inEnv, lambda get, set: (set('foo', 10),
set('bar', get('foo') * get('foo')),
set('foo', 2),
get('foo') + get('bar'))[-1])