Yes, you can do this. There are two obvious approaches to this: one is the approach taken by Scheme, and the other is the approach taken by, really, the λ-calculus.
As this is not a language-specific question, and as my Python is very rusty now, I will give initial examples in Racket, although they should be portable Scheme modulo using λ
instead of lambda
. I will try to explain them in a way which makes sense to people who are not familiar with Scheme-family languages though. I'll give an example of the final version in Python.
For clarity: I am using 'late binding' to mean that, if a procedure refers to some variable, and in particular if it is going to call a procedure which is the value of some variable, then that variable binding is checked when the procedure is called, not earlier than that. In particular it may not exist when the procedure is defined. 'Early binding' would then mean that the existence of the binding is checked when the procedure is defined. I'm not using it to mean the far stronger statement that everything must be known about the binding when the procedure is defined: that would make it impossible to define procedures which call other procedures passed to them as arguments, say.
In a 'Lisp 1' – a language which shares a common namespace for procedure bindings and variable bindings – which supports assignment it's fairly hard to see how any stricter notion of early binding than this would make sense, I think.
The first approach: letrec
Scheme has a construct called letrec
which allows you to bind names to objects – usually procedures – which can refer to each other. Although letrec
is a primitive part of the language, it is clear how it is implemented (in particular the implementation is described on p16 of the current standard (PDF link)), and it's possible to implement a simple version of it. Here is how it works
(letrec ((var1 val1) (var2 val2))
form ...)
does the following:
- first bind
var1
, var2
, ... to some kind of special unspeakable
value or values;
- Then assign
val1
to var1
, val2
to var2
, and so on;
- evaluate the body.
Now you can see that, at the point where val1
is evaluated, all of the variables are already bound: the values of those bindings are wrong, but so long as the bindings are not referred to until later, everything will be fine. Well, that's exactly what happens when defining mutually-recursive procedures.
So, in Scheme
(letrec ((p1 (λ (...) ... (p2 ...) ...))
(p2 (λ (...) ... (p2 ...) ...)
(p1 ...))
turns into
(let ((p1 <unspeakable>)
(p2 <unspeakable>))
(set! p1 (λ (...) ... (p2 ...) ...))
(set! p2 (λ (...) ... (p2 ...) ...))
(p1 ...))
And in fact, since Scheme (or Racket) has a macro system, you can write a version of letrec
in it. Here is that in Racket (I have called it rlet
to avoid clobbering letrec
:
(require racket/undefined) ; provides the undefined object
(define-syntax rlet
;; This is certainly not complete or correct
(syntax-rules ()
[(_ ((var val) ...) form ...)
(let ((var undefined) ...)
(set! var val) ...
form ...)]))
And here is a procedure which returns a procedure which will count down between two numbers. This procedure is one of a pair of mutually-recursive procedures, and also closes over the binding of the argument to the outer procedure (and the two procedures also, obvuiously, close over the bindings of each other's names):
(define (make-countdown x)
(rlet ((r1 (λ (it)
(displayln it)
(if (<= it x)
x
(r2 (- it 1)))))
(r2 (λ (it)
(r1 it))))
r1))
You can use Racket's macro expansion tool to see that the definition of make-countdown
is turned into what you would expect:
(define (make-countdown x)
(let ((r1 undefined) (r2 undefined))
(set! r1 (λ (it) (displayln it) (if (<= it x) x (r2 (- it 1)))))
(set! r2 (λ (it) (r1 it)))
r1))))
And we can now test this
> (make-countdown 10)
#<procedure:r1>
> ((make-countdown 10) 20)
20
19
18
17
16
15
14
13
12
11
10
10
(The 10
is printed twice: once by displayln
and once because it's the value).
So this approach does not use late binding: everything is bound when the mutually recursive procedures are defined. But it does use assignment. But assignment is kind of icky: can you do this without assignment?
Without assignment
Well, yes, you can of course: one of the most famous results in all computing is that you can do anything with just λ. At this point I should probably start going on about the Y combinator and making everything incomprehensible, but you don't need to do that: you can do it with something which is much less hairy (but which is, really, an 'unrolled' version of the U combinator).
The trick is that you can pass a procedure as an argument to a procedure, and then call it. So consider this fragment:
(λ (c it)
(if (<= it x)
x
(c c (- it 1))))
This is a procedure which takes two arguments:
c
, a procedure
it
, a number
It then tests it
against some variable x
, and if it's more than it it calls c
passing it two arguments: c
itself and a value one less than x
.
Well, the trick is, of course, that we're going to make c
be this procedure itself. We can also wrap this up so the procedure we call only takes the one argument we care about:
(let ((r (λ (c it)
(if (<= it x)
x
(c c (- it 1))))))
(λ (it) (r r it)))
And, just to be completist about this, let
itself is of course just syntactic sugar for λ: the above is equivalent to this:
((λ (r)
(λ (it) (r r it)))
(λ (c it)
(if (<= it x)
x
(c c (- it 1)))))
(I won't do that again because it makes things unreadable, but it's important that it can be done.)
Well, we need to wrap this up in a 'maker' procedure again to bind x
:
(define (make-countdown x)
(let ((r (λ (c it)
(displayln it)
(if (<= it x)
x
(c c (- it 1))))))
(λ (it) (r r it))))
And now
> ((make-countdown 1) 3)
3
2
1
1
Well, this is not a pair of mutually-recursive procedures, but that's easy to do, you just need to pass both the procedures as arguments to each of them:
(define (make-countdown x)
(let ((r1 (λ (c1 c2 it)
(displayln it)
(if (<= it x)
x
(c2 c1 c2 it))))
(r2 (λ (c1 c2 it)
(c1 c1 c2 (- it 1)))))
(λ (it) (r1 r1 r2 it))))
And this works the same way, with r1
doing the print and test, and r2
doing the decrementing.
So this approach does not use late binding, and does not use assignment. And is moderately painful, of course.
The second approach in Python
Finally, here is the last function expressed as Python. Note that I've used def
to express the local functions: you can do it all with lambda
s but it's very painful.
def mc(x):
def r1(c1, c2, it):
print(it)
if it == x:
return it
else:
return c2(c1, c2, it)
def r2(c1, c2, it):
return c1(c1, c2, it - 1)
return (lambda v: r1(r1, r2, v))
And here is that working:
>>> (mc(0))(2)
(mc(0))(2)
2
1
0
0
OK.
letrec
implementation, which is quite trivial. $\endgroup$