topics #fedora #git #linux #ruby

Learn Ruby: 3. Blocks, Iterators, and Files

Blocks

A block is simply Ruby code enclosed between do and end:

['Ruby', 'Elixir', 'Dart'].each do |lang|
  puts "Learning {lang}"
end

=>
Learning Ruby
Learning Elixir
Learning Dart

If it fits on one line, you can use curly braces {}:

['Ruby', 'Elixir', 'Dart'].each { |lang| puts "Learning {lang}" }

yield

yield is a special Ruby operator that executes the code passed as a block to a method:

def execute_block
  puts 'Start execution'
  yield
  puts 'Stop execution'
end

execute_block { puts "	>>> Running code from block" }

=>
Start execution
     >>> Running code from block
Stop execution

You can also pass parameters to a block:

def execute_block_with_params(command)
  puts 'Start execution'
  puts yield('Run', command)
  puts 'Stop execution'
end

execute_block_with_params('ls') { |op, cmd| "{op} this command: {cmd}" }

=>
Start execution
Run this command: ls
Stop execution

If the block expects 3 parameters but only 2 are passed via yield, the third will be nil.

If you use yield without passing a block, you'll get an error:

LocalJumpError: no block given (yield)

To avoid this, check whether a block was given using block_given?:

def execute_block
  if block_given?
    puts yield
  else
    puts "no block"
  end
end

You can also pass a block as the last parameter using the & prefix:

def fetch_option(key, &block)
  if block_given?
    options = block.call
    puts "{key} value is {options[key]}"
  else
    puts 'no block'
  end
end

fetch_option(:age) { Hash[name: "Alex", age: 28] }
=> age value is 28

Proc Objects

A Proc works like an anonymous function:

pr = Proc.new { puts "This is Proc" }
pr.call

pr = proc { puts "Short proc" }
pr.call

Procs can accept parameters:

print_name = Proc.new { |name| puts name }
print_name.call('Alex')

Procs can be called in multiple ways:

sum = Proc.new { |a, b| a + b }

sum.call(1, 2)  # => 3
sum.(1, 2)      # => 3
sum[1, 2]       # => 3
sum.yield(1, 2) # => 3

Extra arguments are ignored; missing ones become nil.

Proc with return

If a Proc created inside a method returns with return, the method exits immediately:

def print_numbers(n1, n2)
  block = Proc.new { return 99 }
  puts n1
  block.call
  puts n2  # never reached
end

print_numbers(1, 10)
=>
1
99

Proc with Iterators

add_two = Proc.new { |i| i + 2 }
[1, 2, 3].map(&add_two)
=> [3, 4, 5]

['ruby', 'rails'].map(&:upcase)
=> ['RUBY', 'RAILS']

Lambda

Lambda is a Proc with stricter argument checking:

sum = -> (a, b) { a + b }

sum.call(1, 2)  # => 3
sum[1, 2]       # => 3
sum.(1, 2)      # => 3

Unlike Proc, Lambda raises ArgumentError if the argument count is wrong:

sum.call(1)       # => ArgumentError: wrong number of arguments (given 1, expected 2)
sum.call(1, 2, 3) # => ArgumentError: wrong number of arguments (given 3, expected 2)

Method Objects

The method helper lets you call a method as a Proc object:

class Person
  def say(word)
    puts word
  end
end

mp = Person.new.method(:say)
mp.call("Hello")
=> Hello

Iterators and the Enumerable Module

Iterators come from the Enumerable module and let you traverse arrays, hashes, strings, and more — just like loops, but more idiomatic in Ruby.

[1, 2, 3].each { |i| puts i }

[1, 2, 3].map { |i| i * 10 }
=> [10, 20, 30]

[1, 2, 3, 4, 5].select { |i| i > 2 }
=> [3, 4, 5]

[1, 2, 3, 4, 5].inject(:+)
=> 15

You can chain iterators:

[1, 2, 3, 4, 5].map { |i| i * 2 }.map { |i| i * 10 }.select { |i| i > 50 }.sum
=> 240

Iterators work on various objects:

"Alex".each_char.with_index(1) do |ch, index|
  puts "{index}. {ch}"
end
=>
1. A
2. l
3. e
4. x

{ a: 1, b: 2 }.each { |k, v| puts "{k} - {v}" }

3.times { puts "Hello" }

Custom Iterators with yield

def simple_each
  i = 0
  loop do
    yield self[i]
    i += 1
    break if self.size == i
  end
end

[1, 2, 3].simple_each { |i| i * 10 }

Loops

Ruby has several loop operators, though iterators are preferred in practice:

  • loop — infinite loop, exit with break
  • while — runs while condition is true
  • until — runs while condition is false
  • for in — iterates over a collection
i = 0
loop do
  puts i
  i += 1
  break if i > 5
end

i = 0
while i <= 5 do
  puts i
  i += 1
end

for value in [1, 2, 3] do
  puts value
end

Enumerators

Every iterator returns an Enumerator object. You can step through it manually with .next:

enum = [1, 2, 3].each
enum.next  # => 1
enum.next  # => 2
enum.next  # => 3
enum.next  # => StopIteration: iteration reached an end

Files

The File class (part of IO) lets you work with files.

Writing to a file:

File.open("/tmp/my_file.txt", "a+") do |f|
  f.write("Hello, World")
end

File modes:

r   - read only, from the beginning
r+  - read/write, from the beginning
w   - write only, truncates existing file or creates new
w+  - read/write, truncates existing file or creates new
a   - write only, appends to existing file or creates new
a+  - read/write, appends to existing file or creates new

Opening and closing manually:

f = File.open("/tmp/my_file2.txt", "a+")
f.write "Ruby"
f.close

Reading a file:

File.read("/tmp/my_file2.txt")

File.open("/tmp/my_file2.txt", "r+") do |file|
  arr = file.readlines
  puts arr.inspect
end

Other file operations:

File.rename("/tmp/my_file2.txt", "/tmp/my_file.txt")
File.size("/tmp/my_file.txt")
File.exists?("/tmp/my_file.txt")
File.extname("/tmp/my_file.txt")
File.dirname("/tmp/my_file.txt")

Directory operations:

Dir.glob("*")    # list all files
Dir.glob("*.rb") # list Ruby files

Using FileUtils for shell-like operations:

require 'fileutils'

FileUtils.touch("/tmp/my_file.txt")
FileUtils.pwd()
FileUtils.mkdir("temp_dir")