Base on "Mastering Ruby Closures" book
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
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:
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
>> 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
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
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:
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.
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
Steps to Identify a Closure
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:
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
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
Benefits
Drawbacks
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 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:
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
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.
To replay you need to login. Don't have an account? Sign up for one.