As far as I can tell, there are essentially three different kinds of closure in Ruby; methods, procs and lambdas. I know that there are differences between them, but could we not just get away having one type that accommodates all possible use-cases?
Methods can already be passed around like procs and lambdas by calling self.method(method_name)
, and the only significant differences that I'm aware of between procs and lambdas is that lambdas check arity and procs do crazy things when you try to use return
. So couldn't we just merge them all into one and be done with it?
In Ruby, closure is a function or a block of code with variables that are bound to the environment that the closure is called. Or in other words, closure can be treated like a variable that can be assigned to another variable or can be pass to any function as an argument.
Ruby doesn't have first-class functions, but it does have closures in the form of blocks, procs and lambdas. Blocks are used for passing blocks of code to methods, and procs and lambda's allow storing blocks of code in variables.
Blocks are syntactic structures in Ruby; they are not objects, and cannot be manipulated as objects. It is possible, however, to create an object that represents a block. Depending on how the object is created, it is called a proc or a lambda.
In Ruby, a lambda is an object similar to a proc. Unlike a proc, a lambda requires a specific number of arguments passed to it, and it return s to its calling method rather than returning immediately.
As far as I can tell, there are essentially three different kinds of closure in Ruby; methods, procs and lambdas.
No, there are two: methods aren't closures, only procs and lambdas are. (Or at least can be, most of them aren't.)
There are two ways of packaging up a piece of executable code for reuse in Ruby: methods and blocks. Strictly speaking, blocks aren't necessary, you can get by with just methods. But blocks are meant to be extremely light-weight, conceptually, semantically and syntactically. That's not true for methods.
Because they are meant to be light-weight and easy to use, blocks behave different from methods in some respects, e.g. how arguments are bound to parameters. Block parameters are bound more like the left-hand side of an assignment than like method parameters.
Examples:
Passing a single array to multiple parameters:
def foo(a, b) end
foo([1, 2, 3]) # ArgumentError: wrong number of arguments (1 for 2)
a, b = [1, 2, 3]
# a == 1; b == 2
[[1, 2, 3]].each {|a, b| puts "a == #{a}; b == #{b}" }
# a == 1; b ==2
Passing less arguments than parameters:
def foo(a, b, c) end
foo(1, 2) # ArgumentError
a, b, c = 1, 2
# a == 1; b == 2; c == nil
[[1, 2]].each {|a, b, c| puts "a == #{a}; b == #{b}; c == #{c}" }
# a == 1; b == 2; c ==
Passing more arguments than parameters:
def foo(a, b) end
foo(1, 2, 3) # ArgumentError: wrong number of arguments (3 for 2)
a, b = 1, 2, 3
# a == 1; b == 2
[[1, 2, 3]].each {|a, b| puts "a == #{a}; b == #{b}" }
# a == 1; b == 2
[By the way: none of the blocks above are closures.]
This allows, for example, the Enumerable
protocol which always yields a single element to the block to work with Hash
es: you just make the single element an Array
of [key, value]
and rely on the implicit array destructuring of the block:
{one: 1, two: 2}.each {|k, v| puts "#{key} is assigned to #{value}" }
is much easier to understand than what you would have to otherwise write:
{one: 1, two: 2}.each {|el| puts "#{el.first} is assigned to #{el.last}" }
Another difference between blocks and methods is that methods use the return
keyword to return a value whereas blocks use the next
keyword.
If you agree that it makes sense to have both methods and blocks in the language, then it is just a small step to also accept the existence of both procs and lambdas, because they behave like blocks and methods, respectively:
return
from the enclosing method (just like blocks) and they bind arguments exactly like blocks doreturn
from themselves (just like methods) and they bind arguments exactly like methods do.IOW: the proc/lambda dichotomy just mirrors the block/method dichotomy.
Note that there are actually quite a lot more cases to consider. For example, what does self
mean? Does it mean
self
was at the point the block was writtenself
is at the point the block is runAnd what about return
? Does it mean
This already gives you nine possibilities, even without taking into account the Ruby-specific peculiarities of parameter binding.
Now, for reasons of encapsulation, #2 above are really bad ideas, so that reduces our choices somewhat.
As always, it's a matter of taste of the language designer. There are other such redundancies in Ruby as well: why do you need both instance variables and local variables? If lexical scopes were objects, then local variables would just be instance variables of the lexical scope and you wouldn't need local variables. And why do you need both instance variables and methods? One of them is enough: a getter/setter pair of methods can replace an instance variable (see Newspeak for an example of such a language) and first-class procedures assigned to instance variables can replace methods (see Self, Python, JavaScript). Why do you need both classes and modules? If you allow classes to be mixed-in, then you can get rid of modules and use classes both as classes and mixins. And why do you need mixins at all? If everything is a method call, classes automatically become mixins anyway (again, see Newspeak for an example). And of course, if you allow inheritance directly between objects you don't need classes at all (see Self, Io, Ioke, Seph, JavaScript)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With