Ruby Closures

by msypniewski511 in Ruby for Rails

Base on "Mastering Ruby Closures" book

Closures basics

Lexical scoping.

Lexical scoping, also known as static scoping, is a fundamental concept in programming languages that determines how variable names are resolved in nested functions or blocks. In languages with lexical scoping, a variable's scope (the region of the program where it is accessible) is determined by the physical structure of the code, specifically where the variable is declared within the source code. Here’s a detailed breakdown of lexical scoping:

Key Concepts

  1. Scope: The region of a program where a variable is defined and can be accessed.
  2. Lexical (Static) Scope: The scope is determined at compile time based on the program's text (the lexical structure of the code).

How Lexical Scoping Works
When a function is defined, it captures the scope in which it was defined, not the scope in which it is called. This means that if a function is defined within another function, it has access to the variables in the outer function's scope.
Example in JavaScript
Consider the following JavaScript example:

function outerFunction() {
    let outerVariable = 'I am outside!';
    function innerFunction() {
        console.log(outerVariable); // innerFunction can access outerVariable
    }
    innerFunction();
}
outerFunction(); // Output: 'I am outside!'

In this example:

  • outerVariable is defined in the outerFunction's scope.
  • innerFunction is defined within outerFunction and can access outerVariable because of lexical scoping.
  • When innerFunction is called, it logs the value of outerVariable even though outerVariable is not defined within innerFunction's own body.

Contrast with Dynamic Scoping
Dynamic scoping, in contrast, determines the scope based on the call stack at runtime rather than the lexical structure of the code. In dynamically scoped languages, a function can access variables from the scope of any caller in the call stack.

Advantages of Lexical Scoping

  • Predictability: Since variable scope is determined by the code structure, it is easier to understand and reason about the code.
  • Encapsulation: Lexical scoping provides better data encapsulation, as inner functions can only access variables from their own scope and their containing (lexically enclosing) scopes.
>> msg = "outside"
=> "outside"
>> 3.times do
>>    prefix = "inside"
>>    puts "#{prefix} #{msg}"
>> end
inside outside
inside outside
inside outside

>> puts prefix
NameError: undefined local variable or method `prefix' for main:Object
from (irb):6

Free Variables

A free variable is a term used in programming and mathematical logic to describe a variable that is used in a function or an expression but is not locally defined within that function or expression. Instead, it is bound to a variable defined in an outer scope.

Key Concepts

  1. Bound Variable: A variable that is declared within a function or a block of code and is thus local to that scope.
  2. Free Variable: A variable that is used within a function or an expression but is not declared within it. It is defined in an outer scope.

Example in Ruby

>> msg = "outside"
=> "outside"
>> 3.times do
>>    prefix = "inside"
>>    puts "#{prefix} #{msg}"
>> end
inside outside
inside outside
inside outside

Example in JavaScript
Consider the following JavaScript example:

let outerVariable = 'I am outside!';
function outerFunction() {
    function innerFunction() {
        console.log(outerVariable); // outerVariable is a free variable in innerFunction
    }
    innerFunction();
}
outerFunction(); // Output: 'I am outside!'

In this example:

  • outerVariable is defined in the global scope.
  • innerFunction uses outerVariable, but outerVariable is not declared within innerFunction.
  • Hence, outerVariable is a free variable in the context of innerFunction.

Importance of Free Variables
Understanding free variables is essential for grasping how functions access variables from their surrounding scope. This concept is closely related to closures in many programming languages.

Closures

A closure is a function that captures the bindings of free variables in its scope at the time the closure is created, maintaining access to those variables even after the scope in which they were created has finished execution.
Identifying a closure involves understanding the relationship between functions and their lexical environment, specifically how a function captures and retains access to variables from its outer scope. Here are the key rules for identifying a closure:

Rules for Identifying a Closure

  1. Nested Function: There must be a function defined inside another function.
  2. Free Variables: The inner function must reference variables that are defined in the outer function's scope (i.e., free variables).
  3. Persistent Scope: The inner function must retain access to these variables even after the outer function has completed execution.

Steps to Identify a Closure

  1. Locate Nested Functions: Identify if there is a function defined within another function.
  2. Check for Free Variables: Look for variables in the inner function that are not defined within the inner function itself but are defined in the outer function.
  3. Evaluate Scope Retention: Determine if the inner function can be invoked after the outer function has finished executing, maintaining access to its free variables.

Examples
Example in JavaScript

function outerFunction() {
    let outerVariable = 'I am outside!';
    function innerFunction() {
        console.log(outerVariable); // outerVariable is a free variable
    }  
    return innerFunction;
}
const closureFunction = outerFunction();
closureFunction(); // Output: 'I am outside!'

Identifying Closure:

  • Nested Function: innerFunction is defined within outerFunction.
  • Free Variable: outerVariable is used inside innerFunction but is defined in outerFunction.
  • Persistent Scope: innerFunction (now closureFunction) retains access to outerVariable even after outerFunction has returned.

Simulating Classes with Closures ruby

In Ruby, you can simulate classes using closures by creating functions that encapsulate state and behavior. This approach leverages Ruby's ability to define lambdas and procs, allowing you to encapsulate data and methods within a closure. Here's a step-by-step guide to simulate a class using closures:

Step-by-Step Example: Simulating a Class with Closures
Let's create an example to simulate a Person class with attributes name and age, and methods to get and set these attributes.

Step 1: Define the Closure

def create_person(name, age)
  # Encapsulated state
  person_name = name
  person_age = age

  # Methods to operate on the encapsulated state
  {
    get_name: -> { person_name },
    set_name: ->(new_name) { person_name = new_name },
    get_age: -> { person_age },
    set_age: ->(new_age) { person_age = new_age },
    info: -> { "Name: #{person_name}, Age: #{person_age}" }
  }
end

Step 2: Create an Instance

 # Create a new person instance
person = create_person("Alice", 30)

Step 3: Access and Modify the State

 # Accessing the methods
puts person[:get_name].call  # Output: Alice
puts person[:get_age].call   # Output: 30

 # Modifying the state
person[:set_name].call("Bob")
person[:set_age].call(40)

 # Accessing the modified state
puts person[:get_name].call  # Output: Bob
puts person[:get_age].call   # Output: 40
puts person[:info].call      # Output: Name: Bob, Age: 40

Explanation

  1. Encapsulated State: The local variables person_name and person_age hold the state within the create_person method.
  2. Returning Methods: A hash is returned containing lambda functions (acting as methods) that get and set the state.

Second example

3.2.2 :001 > Counter = -> do
3.2.2 :002 >   x = 0
3.2.2 :003 >   {
3.2.2 :004 >     get: -> { x },
3.2.2 :005 >     incr: -> { x += 1 },
3.2.2 :006 >     decr: -> { x -= 1 }
3.2.2 :007 >   }
3.2.2 :008 > end
 => #<Proc:0x00007ff734ac4dc8 (irb):1 (lambda)> 
3.2.2 :009 > c = Counter.call
 => 
{:get=>#<Proc:0x00007ff734ae1fe0 (irb):4 (lambda)>,
... 
3.2.2 :010 > c[:get]
 => #<Proc:0x00007ff734ae1fe0 (irb):4 (lambda)> 
3.2.2 :011 > c[:get].call
 => 0 
3.2.2 :012 > c[:incr].call
 => 1 
3.2.2 :013 > c[:incr].call
 => 2 

Advanced Example: Dynamic Method Addition
You can extend this pattern to dynamically add methods to your simulated class.
Step 1: Define the Closure with Dynamic Method Addition

def create_object
  state = {}
  methods = {}

  # Define a method to add new methods
  add_method = ->(name, &block) { methods[name] = block }

  # Define a method to call added methods
  call_method = ->(name, *args) { methods[name].call(*args) }

  # Return the available methods
  {
    add_method: add_method,
    call_method: call_method
  }
end

Step 2: Create an Instance and Add Methods

 # Create a new object instance
obj = create_object

 # Add methods dynamically
obj[:add_method].call(:greet) { |name| "Hello, #{name}!" }
obj[:add_method].call(:farewell) { |name| "Goodbye, #{name}!" }

Step 3: Call the Added Methods

 # Call the dynamically added methods
puts obj[:call_method].call(:greet, "Alice")    # Output: Hello, Alice!
puts obj[:call_method].call(:farewell, "Alice") # Output: Goodbye, Alice!

Explanation

  1. Dynamic Method Addition: The add_method lambda allows new methods to be added dynamically.
  2. Method Invocation: The call_method lambda enables calling these dynamically added methods with arguments. Benefits and Drawbacks

Benefits

  • Encapsulation: State is fully encapsulated within the closure and accessible only through the defined methods.
  • Flexibility: Allows for dynamic addition of methods, offering flexibility similar to prototype-based inheritance.

Drawbacks

  • Performance: This pattern might have some performance overhead compared to using standard classes.
  • Readability: While powerful, it may be less readable and idiomatic compared to using conventional classes, especially for developers new to Ruby.

Conclusion
Simulating classes with closures in Ruby provides a flexible and powerful way to encapsulate state and behavior, especially useful for small and simple objects. This approach can be particularly advantageous for scenarios requiring dynamic method definitions or lightweight object-like structures without the need for full class definitions.

First Class Functions/First-class Values.

First-class functions are a key feature of many modern programming languages, including Ruby. In languages with first-class functions, functions can be treated like any other variable. This means functions can be:

  1. Assigned to variables.
  2. Passed as arguments to other functions.
  3. Returned from other functions.

Ruby's First-Class Functions
In Ruby, first-class functions are implemented using Proc objects, lambdas, and methods. Here’s a detailed explanation and examples of each.

Proc Objects
A Proc object is a block of code that can be stored in a variable and passed around.

Creating and Using a Proc Object

 # Create a Proc object
my_proc = Proc.new { |x| x * 2 }

 # Call the Proc
puts my_proc.call(5)  # Output: 10

 # Another way to call a Proc
puts my_proc[6]       # Output: 12

Lambdas
Lambdas are similar to Proc objects but with some differences in terms of argument handling and return behavior.

Creating and Using a Lambda

 # Create a lambda
my_lambda = ->(x) { x * 2 }

 # Call the lambda
puts my_lambda.call(5)  # Output: 10

 # Another way to create a lambda
another_lambda = lambda { |x| x * 2 }

puts another_lambda.call(6)  # Output: 12

Differences Between Proc and Lambda

  • Argument Checking: Lambdas check the number of arguments, while Proc objects do not.
  • Return Behavior: return inside a lambda returns from the lambda itself, while return inside a Proc object returns from the enclosing method.

Example of Argument Checking

my_proc = Proc.new { |x, y| x + y }
puts my_proc.call(1)  # Output: nil (y is nil, no error)

my_lambda = ->(x, y) { x + y }
 # puts my_lambda.call(1)  # Error: wrong number of arguments (given 1, expected 2)

Example of Return Behavior

def test_proc
  my_proc = Proc.new { return "Proc return" }
  my_proc.call
  "This won't be reached"
end

def test_lambda
  my_lambda = -> { return "Lambda return" }
  my_lambda.call
  "This will be returned"
end

puts test_proc   # Output: Proc return
puts test_lambda # Output: This will be returned

Methods as First-Class Functions
In Ruby, methods can be converted to Proc objects using the method method and the & operator.

Example of Converting a Method to a Proc

def my_method(x)
  x * 2
end

 # Convert method to Proc
my_proc = method(:my_method).to_proc

 # Call the Proc
puts my_proc.call(5)  # Output: 10

Passing Functions as Arguments
You can pass Proc objects and lambdas as arguments to other functions.

def apply_function(func, value)
  func.call(value)
end

my_proc = Proc.new { |x| x * 2 }
puts apply_function(my_proc, 5)  # Output: 10

my_lambda = ->(x) { x + 3 }
puts apply_function(my_lambda, 5)  # Output: 8

Returning Functions from Functions
A function can return another function.

def multiplier(factor)
  ->(x) { x * factor }
end

double = multiplier(2)
triple = multiplier(3)

puts double.call(5)  # Output: 10
puts triple.call(5)  # Output: 15

Conclusion
Ruby supports first-class functions through Proc objects, lambdas, and methods. This capability allows for more flexible and expressive code, enabling functions to be passed as arguments, returned from other functions, and assigned to variables. Understanding and utilizing first-class functions can greatly enhance the power and flexibility of your Ruby programs.

Blocks

0 Replies


Leave a replay

To replay you need to login. Don't have an account? Sign up for one.