Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closures in Ruby #1

Closed
dylanninin opened this issue Jul 27, 2016 · 1 comment
Closed

Closures in Ruby #1

dylanninin opened this issue Jul 27, 2016 · 1 comment
Assignees
Milestone

Comments

@dylanninin
Copy link
Owner

Closure

Definition

In programming languages, closures (also lexical closures or function closures) are techniques for implementing lexically scoped name binding in languages with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment:[1] a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.[b] A closure—unlike a plain function—allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.

Application

Closures are used to implement continuation-passing style, and in this manner, hide state. Constructs such as objects and control structures can thus be implemented with closures. In some languages, a closure may occur when a function is defined within another function, and the inner function refers to local variables of the outer function. At run-time, when the outer function executes, a closure is formed, consisting of the inner function’s code and references (the upvalues) to any variables of the outer function required by the closure.

  • First-class functions
  • State representation
  • Define control structure for closures' delay evaluation
  • Communicate privately by altering that environment

Closures in Ruby

Blocks

A code block is a set of Ruby statements and expressions between braces or a do/end pair.

  • The block may start with an argument list between vertical bars.
  • A code block may appear only immediately after a method invocation.
  • The start of the block (the brace or the do) must be on the same logical source line as the end of the invocation.
  • Braces have a high precedence; do has a low precedence.
    • If the method invocation has parameters that are not enclosed in parentheses, the brace form of a block will bind to the last parameter, not to the overall invocation. The do form will bind to the invocation.
  • blocks example
invocation do | a1, a2, ... | 
end

invocation { | a1, a2, ... | }
  • Within the body of the invoked method, the code block may be called using the yield keyword.
  • Parameters passed to yield will be assigned to arguments in the block.
  • A warning will be generated if yield passes multiple parameters to a block that takes just one.
  • The return value of the yield is the value of the last expression evaluated in the block or the value passed to a next statement executed in the block.

block arguments

Blocks argument lists are very similar to method argument lists:

  • You can specify default values.
  • You can specify splat (starred) arguments.
  • The last argument can be prefixed with an ampersand, in which case it will collect any block passed when the original block is called.
  • Block-local variables are declared by placing them after a semicolon in the argument list.

Proc Objects

Ruby’s blocks are chunks of code attached to a method. Blocks are not objects, but they can be converted into objects of class Proc.

  • By passing a block to a method whose last parameter is prefixed with an ampersand. That parameter will receive the block as a Proc object.
  • By calling Proc.new, again associating it with a block
  • By calling the method Object#lambda, associating a block with the call.
  • Using the -> syntax.

The first two styles of Proc object are identical in use. We’ll call these objects raw procs. The third and fourth styles, generated by lambda and ->, add some functionality to the Proc object, as we’ll see in a minute. We’ll call these objects lambdas.

Here’s the big thing to remember: raw procs are basically designed to work as the bodies of control structures such as loops. Lambdas are intended to act like methods. So, lambdas are stricter when checking the parameters passed to them, and a return in a lambda exits much as it would from a method.

calling a Proc

You call a proc by invoking its methods call, yield, or [].

  • The three forms are identical.
  • Each takes arguments that are passed to the proc, just as if it were a regular method.
  • If the proc is a lambda, Ruby will check that the number of supplied arguments match the expected parameters.
  • You can also invoke a proc using the syntax name.(args...). This is mapped internally into name.call(args...).

Procs, break, and next

Within both raw procs and lambdas, executing next causes the block to exit back to the caller of the block. The return value is the value (or values) passed to next, or nil if no values are passed.

def ten_times 
  10.times do |i|
    if yield(i)
        puts "Caller likes #{i}"
    end 
  end
end

ten_times do |number| 
    next(true) if number ==7
end

# => Caller likes 7
#10

Within a raw proc, a break terminates the method that invoked the block. The return value of the method is any parameters passed to the break.

ten_times do |number| 
    break(true) if number ==7
end
# => 
# true

Return and Blocks

A return from inside a raw block that’s still in scope acts as a return from that scope. A return from a block whose original context is no longer valid raises an exception (LocalJumpError or ThreadError depending on the context).

def meth1
   (1..10).each do |val|
      return val 
  end
end

meth1 # => 1

The following example shows a return failing because the context of its block no longer
exists

def meth2(&b) 
  b
end

res = meth2 { return }
res.call

# produces:
#        from prog.rb:6:in `call'
#       from prog.rb:6:in `<main>'
# prog.rb:5:in `block in <main>': unexpected return (LocalJumpError)

And here’s a return failing because the block is created in one thread and called in another:

def meth3
  yield
end

t = Thread.new do 
  meth3 { return }
end

t.join

# produces:
#        from prog.rb:2:in `meth3'
#        from prog.rb:6:in `block in <main>'
# prog.rb:6:in `block (2 levels) in <main>': unexpected return (LocalJumpError)

This is also true if you create the raw proc using Proc.new.

def meth4 
  p = Proc.new { return 99 }
  p.call
  puts "Never get here"
end

meth4 # => 99

A lambda behaves more like a free-standing method body: a return simply returns from the block to the caller of the block:

def meth5
    p = lambda { return 99 } 
    res = p.call
    "The block returned #{res}"
end
meth5 # => "The block returned 99"

Because of this, if you use Module#define_method, you’ll probably want to pass it a proc created using lambda, not Proc.new, because return will work as expected in the former and will generate a LocalJumpError in the latter.

Blocks and Closures

A block is a closure:

  • it remembers the context in which it was defined, and it uses that context whenever it is called.
  • the context includes the value of self, the constants, the class variables, the local variables, and any captured block.
def n_times(thing)
  lambda {|n| thing * n}
end

p1 = n_times(23)
p1.call(3) # => 69
p1.call(4) # => 92

p2 = n_times("hello ")
p2.call(3) # => "hello hello hello "

The method n_times returns a Proc object that references the method’s parameter, thing. Even though that parameter is out of scope by the time the block is called, the parameter remains accessible to the block. This is called a closure—variables in the surrounding scope that are referenced in a block remain accessible for the life of that block and the life of any Proc object created from that block.

Reference

Here list all the references

@dylanninin
Copy link
Owner Author

dylanninin commented Jul 27, 2016

# ---------------------------- Section 5: Rant ----------------------------
#
# This is quite a dizzing array of syntactic options, with subtle semantics differences that are not
# at all obvious, and riddled with minor special cases. It's like a big bear trap from programmers who
# expect the language to just work.
#
# Why are things this way? Because Ruby is:
#
#   (1) designed by implementation, and
#   (2) defined by implementation.
#
# The language grows because the Ruby team tacks on cool ideas, without maintaining a real spec apart
# from CRuby. A spec would make clear the logical structure of the language, and thus help highlight
# inconsistencies like the ones we've just seen. Instead, these inconsinstencies creep into the language,
# confuse the crap out of poor souls like me who are trying to learn it, and then get submitted as bug
# reports. Something as fundamental as the semantics of proc should not get so screwed up that they have
# to backtrack between releases, for heaven's sake! Yes, I know, language design is hard -- but something
# like this proc/lambda issue or the arity problem wasn't so hard to get right the first time.
# Yammer yammer.
# ---------------------------- Section 6: Summary ----------------------------
#
# So, what's the final verdict on those 7 closure-like entities?          
#
#                                                     "return" returns from closure
#                                    True closure?    or declaring context...?         Arity check?
#                                    ---------------  -----------------------------    -------------------
# 1. block (called with yield)       N                declaring                        no
# 2. block (&b => f(&b) => yield)    N                declaring                        no
# 3. block (&b => b.call)            Y except return  declaring                        warn on too few
# 4. Proc.new                        Y except return  declaring                        warn on too few
# 5. proc                                    <<< alias for lambda in 1.8, Proc.new in 1.9 >>>
# 6. lambda                          Y                closure                          yes, except arity 1
# 7. method                          Y                closure                          yes
#
# The things within each of these groups are all semantically identical -- that is, they're different
# syntaxes for the same thing:
#   
#      1. block (called with yield)
#      2. block (&b  =>  f(&b)  =>  yield)  
#      -------
#      3. block (&b  =>  b.call)
#      4. Proc.new  
#      5. proc in 1.9
#      -------
#      5. proc in 1.8
#      6. lambda    
#      -------
#      7. method (may be identical to lambda with changes to arity checking in 1.9)
#
# Or at least, this is how I *think* it is, based on experiment. There's no authoritative answer other
# than testing the CRuby implementation, because there's no real spec -- so there may be other differences
# I haven't discovered.

Reference

additional references

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant