Exploring higher-order procedures in Ruby through a Scheme's example.
I am going through the SICP program, which uses Scheme for the lessons, and I had a fun time working on the exercise below.
Exercise 1.34: Suppose we define the procedure:
(define (f g) (g 2))
What happens if we (perversely) ask the interpreter to evaluate the combination (f f)? Explain.
f is a procedure that expects a procedure as its argument, and evaluates the body with the substituted arguments as expected. For example:
(define (square x) (* x x))
(f square)
4
(f (lambda (z) (* z (+ z 1))))
6
Now, to answer the exercise, when we call f with itself as its argument, we get an error:
(f f)
Error: call of non-procedure: 2
Let’s walk through Scheme’s substitution model, a helpful tool for getting a picture of what steps the interpreter (roughly) takes:
(f f) ; `g` is `f`
(f 2) ; substitutes `g` for `f` inside the body.
Invokes `f` again with `2` as the argument
(2 2) ; inside the body, substitutes the argument `g`
with `f`'s value, which is now 2,
and since all arguments have been applied,
evaluates the expression, returning the error
`Error: call of non-procedure: 2`
The error is returned because f expects a procedure and receives the primitive number 2 instead. The interpreter cannot evaluate (2 2) since the first 2 is not a procedure.
Now, this was a fun exercise in Scheme. I wanted to transfer the idea to Ruby and find out what Ruby’s interpreter would do.
An example of higher-order procedure in Ruby using a lambda function
Here’s one way of writing the f method in Ruby:
def f(g)
g.call(2)
end
Invoking f passing lambda functions as arguments:
f(->(x) { x * 2 })
=> 4
square_lambda = -> (x) { x * x }
=> #<Proc:0x000000011e1b9fa8 (irb):13 (lambda)>
f(square_lambda)
=> 4
f(->(z) { z * (z + 1) })
=> 6
But if we try to call f as an argument to itself as we did in Scheme, we get an error:
f(square)
in 'square': wrong number of arguments
(given 0, expected 1) (ArgumentError)
That’s because we need to invoke f(method(:f)) instead, which is Ruby’s way of passing a method reference as an argument. Once we do that, we get another error:
f(method(:f))
in 'Object#f': undefined method 'call'
for an instance of Integer (NoMethodError)
This error is a bit similar to what we get in Scheme. Ruby’s interpreter expects the argument to be a ‘callable’ but receives a primitive number instead.
If we inspect what’s going on inside of f, the error becomes clearer to understand:
def f(g)
puts "I am inside of f"
puts "g is a: #{g.inspect}"
puts "----"
g.call(2)
end
Then, invoking f(method(:f)) returns:
f(method(:f))
I am inside of f
g is a: #<Method: Object#f(g) (irb):8>
----
I am inside of f
g is a: 2
----
in 'Object#f': undefined method 'call' for
an instance of Integer (NoMethodError)
It gets easier to walk through Ruby’s interpreter substitution model:
f(method(:f)) # `g` is a reference to
the Object#method of `f(g)`
g.call(2) # substitutes `g` for `f` inside the body.
Invokes `f` again with `2` as the argument
2 2 # inside the body, substitutes the argument
`g` with `f`'s value, which is now 2,
and since all arguments have been applied,
evaluates the expression, returning the error
in 'Object#f': undefined method 'call'
for an instance of Integer (NoMethodError)`
Both interpreters return an error because the argument received is not of a callable type. Not surprising, given that Ruby was inspired by Lisp :)
Of course, you could have simply called f(method(:f)) in Ruby, or (f f) in Scheme to get the answer for the exercise, but what’s the fun in any of that? Plus, the exercise is meant to reinforce your skills of applying the substitution model whenever you are not sure what the interpreter will do.
I’ve mainly used Ruby lambda functions in ActiveRecord callbacks and Enumerables, but exploring building abstractions with procedures in Scheme is calling ;) me to explore designing objects differently in Ruby.
SICP continues exploring high-order procedures by building procedures that returns procedures themselves. It’s a good mental exercise to think like the language’s interpreter.