Notes from The Well-Grounded Rubyist, 3rd Edition.
Table of Contents: Click on the hamburger menu in the upper-right corner of the GitHub display of this file.
# Find the source of a method
SecureRandom.method(:hex).source_location
# => ["/Users/myuser/.rbenv/versions/3.1.3/lib/ruby/3.1.3/random/formatter.rb", 73]
Syntax check a file
ruby -cw main.rb
Find the Ruby installation
irb -r rbconfig
RbConfig::CONFIG['bindir']
# Other keys: rubylibdir, archdir, sitedir, vendordir, sitelibdir, sitearchdir
# (./gems directory is not in this hash but should be next to the 'sitedir' value)
load
checks the current working directory...
load 'loadee.rb'
load '.../extras.rb'
...and then the load path ($:
)
ruby -e 'puts $:'
-
require_relative
is likerequire
but can check thecwd
(without manipulating the load path) -
Commonly used
ruby
command-line switches:-c
(check),-w
(warning),-e
(execute literal)-rname <feature>
(require)
rake --tasks
rake admin:list_documents
Install a gem from a local file
gem install /path/to/my.gem
gem install -l ... # restrict all operations to the local domain
gem install -r ... # prevent operations to the local domain
- Load path (
$:
) changes whenrequire
is used
The method gem
can be used in programs to lock a specific version of a gem
gem "bundler", "1.14.6" # Ruby
obj = Object.new
def obj.talk
puts 'I am an object'
end
Innate behaviors of an object
p Object.new.methods.sort
[
# ...
:object_id,
# ...
:respond_to?,
# ...
:send, # or __send__
# ...
]
Argument sponge
def two_or_more(a, b, *c) # c is an array of the remaining args
# ...
end
Default value
def default_args(a, b, c=1)
# ...
end
dup
duplicates objects- Shallow
freeze
prevents undergoing changesclone
is likedup
but retains frozen status, if any- Also shallow
- Class methods can be overridden
- Last definition wins
Time.new.xmlschema # Errors
require 'time'
Time.new.xmlschema # Succeeds
- Syntactic sugar for methods whose names end in
=
ticket.price=(63.00)
# or
ticket.price = 63.00
-
Methods whose names end in
=
return the assigned value, not the last value of the method -
attr_reader
,attr_writer
,attr_accessor
-
BasicObject
is aboveObject
and is mostly used for DSLs- This is because is has very few methods of its own
Classes can be created like objects
my_class = Class.new
instance_of_my_class = my_class.new
c = Class.new do
def say_hello
puts 'Hello!'
end
end
&:
uses a Proc object to simplify HOF calls
['havoc', 'prodigy'].map(&:capitalize)
Ways to add a class method
class Ticket
end
# ...
def Ticket.most_expensive(*tickets)
tickets.max_by(&:price)
end
class Temperature
def Temperature.c2f(celsius)
celsius * 9.0 / 5 + 32
end
# ...
end
-
Method notation in documentation
- instance method:
Ticket#price
- class method:
Ticket.most_expensive
orTicket::most_expensive
- instance method:
-
Names of constants begin with a capital letter
-
Class/module constants can be accessed outside of a class using
::
- Example:
Math::PI
- Example:
Some predefined constants in Ruby (these can also be seen with ruby -v
)
RUBY_VERSION
RUBY_PATCHLEVEL
RUBY_RELEASE_DATE
RUBY_COPYRIGHT
Use <<
to add a new element to an existing array
Ticket::VENUES << "Big Mike's Place" # Modifying the array that a constant references
Can use is_a?
to determine an object's class hierarchy
Magazine.new.is_a?(Publication)
-
Kernel
is a module thatObject
mixes in- It is where most of Ruby's fundamental methods are defined
-
prepend
is likeinclude
but causes message lookup to the module before the class -
ancestors
shows the order of ancestors for a class or module -
extend
makes a module's methods available as class methods- Does not add the module to the class's ancestor chain, unlike
prepend
andinclude
- Does not add the module to the class's ancestor chain, unlike
-
super
argumentssuper
(no argument list) forwards the method's arguments to the super methodsuper
(empty argument list) sends no arguments to the super methodsuper(a, b, c, ....etc)
sends exactly those arguments to the super method
Get an instance of a method of an object
f = obj.method(:hello)
sf = obj.method(:hello).super_method # nil if no super method
f.call # invoke the method
-
Override
method_missing(method, *args, &block)
to handle any messages that an object does not respond to- Call
super
inmethod_missing
to delegate the missing method to the next ancestor - See more examples in chapter 15
- Call
-
Modules are often used as namespaces
self
references- In a class or module definition, self is the class or module object
- In an instance-method definition, self will be some future object that calls the method
- In singleton methods and class methods, self is the object that owns the method
Using self instead of class names
class C
def self.x
end
end
class C
class << self
def x
end
end
end
- If a method name ends with an equal sign (i.e., setter),
self.method
must include theself.
part, not justmethod
by itself- Otherwise Ruby will think that you are creating a new variable
Every instance variable belongs to the current object (self
) at that poin in the program. Notice the difference here:
class C
def set_v
@v = 'instance variable that belongs to any instance of C'
end
def self.set_v
@v = 'instance variable that belongs to C'
end
end
require 'english'
enables aliases for many built-in global variables$INPUT_LINE_NUMBER
instead of$.
$PID
instead of$$
- etc...
Prefix constants with ::
to start at the top-level
class MyClass
class String
end
def my_func
::String.new('I am a string') # Does not refer to MyClass::String
end
end
- Class variables
- Not class scoped; they are class-hierarchy scoped
- Shared between a class and instances of the class,
- and shared between superclasses and subclasses
- Two at-signs like
@@example
makes a class variable
Initializing a class variable
class Car
@@total_count = 0
#...
end
# But it might be better as a class instance variable (notice the def self...)
class Car
def self.total_count
@total_count ||= 0
end
def self.total_count=(n)
@total_count = n
end
end
Can declare methods-access rules in classes multiple ways
class X
#...
private
def my_func
end
end
class X
#...
def my_func
end
private :my_func
#...
end
Private methods are inherited by subclasses
class A
private
def cheese
puts 'cheese'
end
end
class B < A
def please
cheese
end
end
B.new.please
Protected methods: these can be called on an object x
, as long as the default object (self) is an instance of the same class as x
or of an ancestor of descendant class of x
's class
class C
def initialize(n)
@n = n
end
def n
@n
end
def compare(c)
if c.n > n # Able to do this because n is protected
puts "The other object's n is bigger"
else
puts "The other object's n is the same or smaller"
end
end
protected :n
end
c1 = C.new(100)
c2 = C.new(101)
c1.compare(c2)
Top-level methods are stored as private instances of the Object
class
def talk
puts 'Hello'
end
# Equivalent to:
class Object
private
def talk
puts 'Hello'
end
end
All of the private instance methods that Kernel
provides
ruby -e 'p Kernel.private_instance_methods.sort'
!
has a higher precedence than ==
, so you need parentheses to do this check correctly
if !(x == 1) # ...
if not x == 1 # ... (But do not need parentheses if using 'not' instead of !)
if
andunless
statements evaluate to objects- If none of the clauses are true, it evaluates to nil
Can call a method in an if
statement
if m = /la/.match(name)
# ... do something with m
end
case
statements- use the case equality method,
===
a === b
says, "if I have a drawer labeleda
, does it make sense to putb
in it?- Example:
Integer === 2
is true,Integer === 'hello'
is false
- can have an
else
clause - like
if
statements, they evaluate to a single object
- use the case equality method,
Match multiple values in a case
statement by separating them with commas
case answer
when 'y', 'yes' # matches both 'y' and 'yes'
# ...
else
# ...
end
case
statements can be written with the keyword by itself, like if...elsif
statements
case
when x == 1, y == 2
# ...
else
# ...
end
loop
examples
n = 1
loop do
puts n
n = n + 1
break if n > 9
end
n = 1
loop do
puts n
n = n + 1
next unless n == 10 # next jumps to the beginning of the loop
break
end
while
can be placed at the end of a loop
n = 1
begin
puts n
n = n + 1
end while n < 11
until
can be used as an alterative to while
n = 1
until n > 10
puts n
n = n + 1
end
while
and until
can be used in one-liners like if
and unless
n = 1
n = n + 1 until n == 10
Ruby does have a for
loop
for c in [1, 2, 3]
# ...
Due to precedence, writing blocks with {
/}
can sometimes be favored over do/
end`
puts [1, 2].map do |n| n * 10 end # \___These statements are equal;
puts([1, 2].map) do |n| n * 10 end # / unintended parens placement.
puts [1, 2].map { |n| n * 10 } # <-- Does the expected thing
The code block for times
accepts a parameter, which is the iteration number
5.times { |i| puts i } # Outputs "0\n1\n2\n3\n4", evaluates to 5
Blocks have direct access to variables that already exist
def block_scope_demo_2
x = 100
1.times do
x = 200
end
puts x
end
# => 200
Block-local variables are needed to say "give me a new variable x even if one already exists
# Without block-local, final b will be 3:
a = [1, 2, 3]
b = 100
a.each do |c| # <--------------- no semicolon
b = c
puts "b: #{b}"
end
puts "b: #{b}"
# With block-local, final b will still be 100
a = [1, 2, 3]
b = 100
a.each do |c; b| # <--------------- semicolon b means "give me a new b for this block"
b = c
puts "b: #{b}"
end
puts "b: #{b}"
The beginning of a method or block provides an implicit begin
/end
context
def foo
# ...
rescue
# ...
end
bar do
# ...
rescue
# ...
end
binding.irb
is one of the built-in methods for debugging
&.
is the safe navigation operator, or dig operator
nil&.does_not_exist
# => nil
Three ways to call raise
raise
raise RuntimeError
raise RuntimeError, 'description'
A full Exception example
class MyException < Exception # StandardError is another common parent class
end
begin
raise MyException, '<---- oops ---->'
rescue MyException => e
puts e.backtrace
puts e.message
raise # re-raises the exception
ensure
puts 'This always runs'
end
- Special overloaded methods can use Ruby's built-in syntactic sugar
+ - * / % **
[] []= <<
<=> == > < >= <=
=== | & ^
- Some overloaded methods have odd-looking syntax
- Unary operators
+
and-
becomedef +@
,def -@
- Logical not
!
andnot
becomedef !
- Unary operators
- Example:
obj = Object.new
def obj.+(other)
'I am on strike from math'
end
puts obj + 100
- Bang
!
method recommendations- Don't use
!
except inmethod
/method!
pairs - Don't use
!
notation with destructive behaviors, or vice versa
- Don't use
Calling to_a
on a Struct
returns a summary of attribute settings
Computer = Struct.new(:os, :manufacturer)
laptop1 = Computer.new('linux', 'Lenovo')
laptop2 = Computer.new('os x', 'Apple')
[laptop1, laptop2].map(&:to_a)
# => [["linux", "Lenovo"], ["os x", "Apple"]]
A bare list means several identifiers or literal objects separate by commas (almost like pre-processing)
[1, 2, 3, 4, 5] # a bare list within the literal array constructor brackets
A splat/star/unarray operator is *
unwraps its operand into a bare list
array = [1, 2, 3, 4, 5]
[*array]
# => [1, 2, 3, 4, 5]
- The
Integer
andFloat
methods are like more strict versions of.to_i
and.to_f
"If an object responds to to_str
, its to_str representation will be used when the object is used as the argument to String#+.
class Bob
def to_str
'Bob'
end
end
bob = Bob.new
'Hi ' + bob # Can also use the << operator
# => "Hi Bob'
to_ary
helps you use objects like arrays
class Bob
def to_ary
['b', 'o', 'b']
end
end
bob = Bob.new
[1, 2, 3].concat(bob)
# => [1, 2, 3, "b", "o", "b"]
-
Booleans
nil
andfalse
have a Boolean value of false- Empty class definitions have a Boolean value of false
true
andfalse
are singleton objects ofTrueClass
andFalseClass
- Boolean arguments in positional parameters can be hard to remember what they mean
-
nil
is a singleton object ofNilClass
-
Object
defines three equality-test methods==
- typically redefined by subclasseseql?
- typically redefined by subclassesequal?
- "are they the same object?"- Ruby recommends against redefining this
# For Object, all three behave the same way
a = Object.new
b = Object.new
a == b # false
a.eql?(b) # false
a.equal?(b) # false
# For String, two behave the same way, one does not
string1 = 'text'
string2 = 'text'
string1 == string2 # true
string1.eql?(string2) # true
string1.equal?(string2) # false
# For Integer and Float, == and eql? behave differently
5 == 5.0 # true
5.eql? 5.0 # false
5.equal? 5.0 # false
<=>
(spaceship operator)<
,>
,>=
,<=
,==
,!=
, andbetween?
are defined in terms of<=>
- The
Comparable
module expects<=>
to be defined-1
means less than0
means equal to1
means greater than
class A
# include Comparable # TODO: including this seems optional?
attr_accessor :value
def <=>(other)
self.value <=> other.value
# Or it could be written out explicitly:
# if self.value < other.value
# -1
# elsif self.value > other.value
# 1
# else
# 0
# end
end
end
x = A.new
x = 10
y = A.new
y = 20
x < y # => true
y < x # => false
x == x # => true
- Reflection
- Class-level methods include:
methods
public_instance_methods
/instance_methods
instance_methods(false)
excludes the class's ancestorsObject.instance_methods(false)
evaluates to[]
(remember why?)
private_instance_methods
protected_instance_methods
- Instance-level methods include:
private_methods
public_methods
protected_methods
singelton_methods
- Including a module into a class affects objects that already exist because of how methods are looked up
- Class-level methods include:
Alternatives to '
and "
quoting
# %q{...} is literal, like a single-quoted string but allows single-quotes
%q{' " \n \t #{}}
# => "' \" \\n \\t \#{}"
# %Q{...} a.k.a. %{...} does interpolation and escaping, like a double-quoted string
%Q{' " \n \t #{}} # (%{} is synonymous with %Q)
# => "' \" \n \t "
Delimiters for %
-style notations can be almost anything, not just {...}
%q-A string-
%Q/Another string/
%[Yet another string]
- Heredocs
<<HERE
includes any spaces<<-HERE
switches off the flush-left requirement (endingHERE
can be in the middle of a line)<<~HERE
strips leading whitespace
Confusingly, the <<HERE
does not have to be the last thing on its line
[<<HERE.to_i * 10]
5
HERE
# => [50]
Basic string manipulation
str = 'abcdefghijklmnopqrstuvwxyz'
str[-1] # Last
# => "z"
str[6, 9] # 9 characters starting at [6]
# => "ghijklmno"
str[6..9] # [6] inclusive to [9] inclusive
# => => "ghij"
str['defg'] # Substring search (matched)
# => "defg"
str['asdf'] # Substring search (did not match)
# => nil
str[/q.*u/] # Regex match
# => "qrstu"
To set part of a string to a new value, use []=
, which has the same indexing as []
above
str = 'Hello'
str['ello'] = 'i'
str
# => "Hi"
-
Methods to query strings include:
include?
start_with?
andend_with?
empty?
size
count
- Can count ranges, sets, negations, and moreindex
rindex
- Likeindex
, but starts on the rightord
andchr
take one character and are oppositesencoding
-
Methods to transform strings include
upcase
,downcase
,swapcase
,capitalize
- These have
!
equivalents
- These have
ljust
,rjust
,center
strip
,lstrip
,rstrip
chop
andchomp
clear
- Notice that this modifies a String in-place but does not end in!
replace
anddelete
are also the same way
crypt
applies DES with salt- Example:
'secret'.crypt('salt')
- Example:
succ
encode
-
Methods to convert strings include
to_i
,to_f
,to_c
,to_r
,oct
,hex
to_sym
a.k.aintern
Show the current file's encoding (usually UTF-8)
puts __ENCODING___
UTF-8 escaped character
"\u20AC"
# => "€"
to_s
creates a symbol programmatically
Arrays of strings or symbols can be grep'd
['abc', 'bce', 'efg'].grep(/b/)
# => ["abc", "bce"]
- Symbols have very similar methods as strings
- No bang (
!
) versions because symbols are immutable - Indexing (
[]
) into a symbol returns a string, not another symbol
- No bang (
Hex and octal
0x12 # => 18
0x12 + 12 # => 30
012 # => 10
012 + 12 # => 22
012 + 0x12 # => 28
Date
,Time
, andDateTime
rely on the packagesdate
andtime
for (full) functionality
puts Date.today # 2023-01-11
puts Date.new(1959, 2, 1) # 1959-02-01
puts Date.parse('2003/6/9') # 2003-06-09
puts Date.parse('June 9, 2003') # Parse is pretty flexible
d = Date.today
d.day # => 11
d.saturday? # => false
d.leap? # => false
d >> 1
# => #<Date: 2023-02-11 ((2459987j,0s,0n),+0s,2299161j)>
# ^^--- added 1 month (also see: next, next_year, next_month, prev_day, etc methods)
Date.today.rfc2822
# => "Wed, 11 Jan 2023 00:00:00 +0000"
Time.new # => 2023-01-11 09:53:28.430124 -0600
Time.at(0) # => 1969-12-31 18:00:00 -0600
Time.mktime(2007, 10, 3, 14, 3, 6)
# => 2007-10-03 14:03:06 -0500
Time.parse('2007-10-03 14:03:06 -0500')
# => 2007-10-03 14:03:06 -0500
t = Time.now
t.month # => 1
t.sec # => 29
t.sunday? # => false
t.dst? # => false
t.strftime('%m-%d-%y')
# => "01-11-23"
t - 20 # => 2023-01-11 09:58:09.09056 -0600
# ^^--- subtracted 20 seconds
t >> 1
puts DateTime.new(2009, 1, 2, 3, 4, 5) # 2009-01-02T03:04:05+00:00
puts DateTime.now # 2023-01-11T09:55:42-06:00
puts DateTime.parse('October 23, 1973, 10:34 AM') # 1973-10-23T10:34:00+00:00
dt = DateTime.now
dt.year # => 2023
dt.hour # => 9
dt.minute # => 57
dt.second # => 41
dt.wednesday? # => true
dt.leap? # => false
dt.dst?
dt >> 2
# => #<DateTime: 2023-03-11T09:57:41-06:00 ((2460015j,57461s,51328000n),-21600s,2299161j)>
# ^^--- added two months (also see: next, next_year, next_month, prev_day, etc methods)
DateTime.now.httpdate
# => "Wed, 11 Jan 2023 16:04:31 GMT"
Iterate over a range of Date
or DateTime
objects
d = Date.today
next_week = d + 7
d.upto(next_week) do |date|
puts "#{date} is a #{date.strftime("%A")}"
end
# 2023-01-11 is a Wednesday
# 2023-01-12 is a Thursday
# 2023-01-13 is a Friday
# 2023-01-14 is a Saturday
# 2023-01-15 is a Sunday
# 2023-01-16 is a Monday
# 2023-01-17 is a Tuesday
# 2023-01-18 is a Wednesday
- Converting between
Time
,Date
, andDateTime
to_date
to_datetime
to_time
to_date
Hashes are ordered collections
hash = { red: 'ruby', white: 'diamond', green: 'emerald' }
hash.each_with_index do |(key, value), i|
puts "Pair #{i} is #{key}/#{value}"
end
# Pair 0 is red/ruby
# Pair 1 is white/diamond
# Pair 2 is green/emerald
Destructure an array
(a, b) = [1, 2]
a # => 1
b # => 2
Special ways to create an array
Array.new(3)
# => [nil, nil, nil]
Array.new(3) { |i| 10 * (i + 1) }
# => [10, 20, 30]
# Can you guess why this happens?
a = Array.new(3, 'abc') # => ["abc", "abc", "abc"]
a[0] << 'def' # => "abcdef"
a # => ["abcdef", "abcdef", "abcdef"]
# NOTE: Should have done this instead:
a = Array.new(3) { 'abc' }
# Array() tries to call to_ary, to_a, or just returns a 1-element array
obj = Object.new
def obj.to_a
return [1, 2, 3]
end
Array(obj)
# => [1, 2, 3]
Array('hi')
# => ["hi"]
# %w and %W operators mean "words"; delimited by whitespace
%w(Joe Leo III)
# => ["Joe", "Leo", "III"]
# %W is parsed a like double-quoted string
%W(Joe is #{2018 - 1981} years old.)
# => ["Joe", "is", "37", "years", "old."]
# %i and %I are like %w and %W but for symbols
%i(Joe Leo III)
# => [:Joe, :Leo, :III]
try_convert
is a common class method
Array.try_convert(10) # => nil
Array.try_convert([1, 2, 3]) # => [1, 2, 3]
Array syntactic sugar
a = ['', 'second', 'third', 'fourth', 'fifth', 'sixth']
a[0] = 'first' # equivalent
a.[]=(0, 'first') # equivalent
a[0] # equivalent
a.[](0) # equivalent
# Get more than one element
a[1, 2] # => ["second", "third"]
a[2..4] # => ["third", "fourth", "fifth"]
a.slice(2, 3) # => ["third", "fourth", "fifth"]
a.values_at(0, 2, 4) # => ["first", "third", "fifth"]
Getting sub-array elements
a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
a.dig(1, 1) # => 5
a.dig(100, 500, 55) # => nil
-
Ends of arrays
unshift
andshift
push
andpop
<<
and>>
-
Combining arrays
concat
+
replace
-
Array transformations
# * method joins arrays into strings, like `join`
a = %w(one two three)
a * "-"
# =>"one-two-three"
# compact removes nils
[1, nil, 2, nil].compact
# => [1, 2]
Sample nrandom elements from array
a`
a = [1, 2, 3, 4, 5]
a.sample()
# => 2 # which is random
a.sample(3)
# => [2, 5, 3] # which is also random
- Hashes remember the insertion order of their keys
- Hash transformation:
select
,reject
,compact
,replace
, andclear
work on hashes similarly to arraysinvert
flips the keys and values (but watch out for discarded duplicates)
- Hash querying:
has_key?
,include?
,key?
,member?
,has_value?
,value
,empty?
,size
Inclusive vs Exclusive Ranges
r = 1..100 # inclusive [1,100]
r = 1...100 # exclusive [1,100)
r.begin
r.end
r.exclude_end?
r = 'a'..'z'
r.cover?('a') # true
r.include?('a') # true
r.cover?('abc') # true
r.include?('abc') # false <-- behaves differently than cover?
r.cover?('A') # false
r.include?('A') # false
r = 1.0..2.0
r.include?(1.5) # true
-
Don't use backwards ranges; has unexpected behaviors
-
Set
is a standard library class, so it must berequire
d
names = ['a', 'b', 'c']; name_set = Set.new(names) { |name| name.upcase }
# => #<Set: {"A", "B", "C"}>
name_set << 'd'
# => #<Set: {"A", "B", "C", "d"}>
name_set.delete('A')
# => #<Set: {"B", "C", "d"}>
# intersection: &
# union: + or |
# difference: -
# exclusive or: ^
# Example:
name_set + Set.new(['x', 'y', 'z'])
# => #<Set: {"B", "C", "d", "x", "y", "z"}>
# Merge
tri_state = Set.new(["Connecticut", "New Jersey"])
tri_state.merge(["New York"])
# => #<Set: {"Connecticut", "New Jersey", "New York"}>
# Other methods
# .subset?
# .proper_subset?
# .superset?
# .proper_superset?
- A class that includes
Enumerable
must defined an instance method namedeach
.
# Shows methods defined by Enumerable that are now accessible as a result of defining an each method
Enumerable.instance_methods(false).sort
Enumerable
has queries such as:include?
,all?
,any?
,one?
,none?
- Float and integer ranges are handled differently (e.g., float ranges cannot be iterated)
Enumerable
has selection methods, such asfind
,find_all
/select
,reject
A failure-handling function can be given to find
failure = lambda { 11 }
over_ten = [1, 2, 3, 4, 5, 6].find(failure) { |n| n > 10 }
Example reject
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
a.reject { |item| item > 5 }
# => [1, 2, 3, 4, 5]
Enumerable#grep
uses "threequal" (===
, the "case-equality operator")
colors = %w(red orange yellow)
colors.grep(/o/)
# => ["orange", "yellow"]
# grep has a built-in map over matched
colors.grep(/o/) { |color| color.capitalize }
# => ["Orange", "Yellow"]
# Ranges implement === as an inclusion test
range = 10..20
# => 10..20
range.grep(15)
# => [15]
range.grep(21)
# => []
- The difference between
Enumerable#group_by
andEnumerable#partition
is the return value (hash and array, respectively) Enumerable#take
- get the first n elements- "Constrainable" with
take_while
- "Constrainable" with
Enumerable#drop
- get everything but the first n elements- "Constrainable" with
drop_while
- "Constrainable" with
Enumerable#max
andEnumerable#min
work the way you would think
Relatives of each
- Self-explanatory:
reverse_each
array.each_with_index
vsarray.each.with_index
The slice_
family
array.each_slice
- Block that takes
n
elements, iterating over each group ofn
- Block that takes
array.each_cons
- Block that takes
n
elements, iterating over each index
- Block that takes
slice_before
,slice_after
- group elements by a delimiter- Can be used to split up lines in a file by a delimiter
slice_when
- "test two elements at a time over a collection"
The cycle
method
class PlayingCard
SUITS = %w(clubs diamonds hearts spades)
RANKS = %w(2 3 4 5 6 7 8 9 10 J Q K A)
class Deck
attr_reader :cards
# @param [Integer] number of decks of cards
def initialize(n=1)
@cards = []
SUITS.cycle(n) do |s|
RANKS.cycle(1) do |r|
@cards << "#{r} of #{s}"
end
end
end
end
end
The inject
method, a.k.a. "reduce" or "fold"
[1, 2, 3, 4].inject(0) { |acc, n| acc + n }
# => 10
[1, 2, 3, 4].inject(:+)
# => 10
- The
map!
version ofmap
is actually defined on the classes that implement it, rather thanEnumerable
String
is not an Enumerable
, just very similar
each_byte
,each_char
,each_codepoint
,each_line
$/
is what Ruby uses as the delimiter foreach_line
bytes
Enumerables are sorted by their spaceship operator <=>
,
and if you want to compare (<
, >
, ==
, clamp
, between?
, etc...), you must include Comparable
class Item
attr_accessor :price
def <=>(other)
self.price <=> other.price
end
end
class ItemComparable < Item
include Comparable
end
Sort blocks
["2", 1, 5, "3", "4", "6"].sort { |a, b| a.to_i <=> b.to_i }
# => [1, "2", "3", "4", 5, "6"]
["2", 1, 5, "3", "4", "6"].sort_by(&:to_i)
# => [1, "2", "3", "4", 5, "6"]
- Enumerators
- Are objects (whereas iterators are methods)
- Have an
each
method that is used by theEnumerable
module
e = Enumerator.new do |y| # `y` is a "yielder"
y << 1
y << 2
y << 3
end
e.to_a
# => [1, 2, 3]
e.map { |x| x * 10 }
# => [10, 20, 30]
# etc...
Attach an enumerator to another object (the enumerator's each
serves as a front end to the array's select
)
arr = %w(a b c d)
e = arr.enum_for(:select)
e.each { |n| n.include?('a') }
# => ["a"]
Most built-in iterators return an enumerator when they are called without a block
str = "Hello"
# These behave the same:
str.each_byte { |b| puts b }
(str.each_byte).each { |b| puts b }
Why use an enumerator? One reason is to protect a collection from mutations
class PlayingCard
def initialize
@cards = ['J', 'K', 'Q', 'A']
end
def cards
@cards.to_enum
end
end
x.cards.map { |y| "Face Card: #{y}" }
# => ["Face Card: J", "Face Card: K", "Face Card: Q", "Face Card: A"]
x.cards << 'My Card'
# => undefined method `<<' for #<Enumerator [...]
- Enumerators have
next
andrewind
methods - "An enumerator attaches itself to a particular method on a particular object and uses that method as the foundation method -- the
each
-- for the entire enumerable toolset."
Enumerators help mitigate the problem of creating intermediate objects when method chaining
string = 'An arbitrary string'
string.each_byte.map { |b| b + 1 }
# each_byte spawns an enumerator instead of an array
with_index(1)
will start counting at 1 instead of 0
Full example: XOR a string
class String
def ^(key)
kenum = key.each_byte.cycle
each_byte.map { |byte| byte ^ kenum.next }.pack('C*').force_encoding(self.encoding)
end
end
# Example:
x = 'hello' ^ 'world'
# => "\x1F\n\x1E\x00\v"
x ^ 'world'
# => "hello"
x ^ 'hello'
# => "world"
Lazy enumerators
# This runs forever:
(1..Float::INFINITY).select { |n| n % 3 == 0 }.first(10)
# This terminates:
(1..Float::INFINITY).lazy.select { |n| n % 3 == 0 }.first(10)
# Alternatively:
(1..Float::INFINITY).lazy.select {|n| n % 3 == 0 }.take(10).force
# Syntax
//.class
# => Regexp
%r{}.class
# => Regexp
# Matching
/abc/.match?('123abc123')
# => true
'123abc123'.match?(/abc/)
# => true
/abc/ =~ '123abc123'
# => 3 # (numerical index; otherwise nil if no match)
'123abc123' =~ /abc/
# => 3 # (numerical index; otherwise nil if no match)
- Special characters:
^ $ ? . / \ [ ] { } ( ) + *
- When using the
%r{}
, you do not have to escape/
s
Character class examples
%r{[dr]ejected}.match?('dejected')
# => true
/[a-z]/.match?('123')
# => false
/[a-zA-Z0-9]/.match?('123')
# => true
- Two ways to match any digit
/[0-9]/
/\d/
- Match anything that isn't a digit by capitalizing:
/\D/
- Same for
\w
/\W
(digit/alpha/_) and\s
/\S
(space/tab/newline)
- Same for
match?
vs match
- the latter returns MatchData
x = /1(.*)2(.*)3/.match('1-A-2=B=3')
# => #<MatchData "1-A-2=B=3" 1:"-A-" 2:"=B=">
x.string # or x[0]
# => "1-A-2=B=3"
x.captures[0] # or x[1] or $1
# => "-A-"
x.captures[1] # or x[2] or $2
# => "=B="
Nested captures
/((a)((b)c))/.match('abc')
# => #<MatchData "abc" 1:"abc" 2:"a" 3:"bc" 4:"b">
Named captures
re = %r{(?<first>\w+)\s+((?<middle>\w\.)\s+)(?<last>\w+)}
re = %r{(?<first>\w+)\s+((?<middle>\w\.)\s+)(?<last>\w+)}
x = re.match('Genghis X. Khan')
x[:first]
# => "Genghis"
x[:middle]
# => "X."
x[:last]
# => "Khan"
?
directly after a capture paren means the capture is optional
re = %r{(?<color>\w+)\s*((?<fruit>\w+)?)}
re.match('yellow banana')
# => #<MatchData "yellow banana" color:"yellow" fruit:"banana">
re.match('blue')
# => #<MatchData "blue" color:"blue" fruit:nil>
Other MatchData
methods: pre_match
, post_match
, begin
, end
?
means zero or one of the preceeding character or group+
means one or more of the preceeding character or group
Greedy vs Non-Greedy
+?
is the non-greedy version of+
*?
is the non-greedy version of*
/.*!/.match('abc!123!')
# => #<MatchData "abc!123!">
/.*?!/.match('abc!123!')
# => #<MatchData "abc!">
Specific numbers of repetitions
# Exactly 3 digits then 4 digits
'123-1234'.match(/\d{3}-\d{4}/)
# => #<MatchData "123-1234">
'12-123'.match(/\d{3}-\d{4}/)
# => nil
# Between 1 and 10 digits
'1234567890'.match(/\d{1,10}/)
# => #<MatchData "1234567890">
'1'.match(/\d{1,10}/)
# => #<MatchData "1">
'a'.match(/\d{1,10}/)
# => nil
# 3 or more digits
'12345'.match(/\d{3,}/)
# => #<MatchData "12345">
'12'.match(/\d{3,}/)
# => nil
# Can you spot the difference?
/([A-Z]{5})/.match('David BLACK')
# => #<MatchData "BLACK" 1:"BLACK">
/([A-Z]){5}/.match('David BLACK')
# => #<MatchData "BLACK" 1:"K">
# It is because atoms include subpatterns wrapped in parentheses
Anchors:
^
and$
\A
beginning of string\z
end of string\Z
end of string, excluding newline, if any\b
word boundary
Lookahead Assertions
# "Zero-width positive lookahead assertion"
# Match three digits followed by a dot, but only return the three digits
str = "123 456. 789"
m = /\d+(?=\.)/.match(str)
# => #<MatchData "456">
Lookbehind Assertions
# Match "BLACK" only when preceded by "David "
'David BLACK'.match /(?<=David )BLACK/
# => #<MatchData "BLACK">
'Jack BLACK'.match /(?<=David )BLACK/
# => nil
Non-Capturing Parentheses
# "def" is not part of the MatchData
str = "abc def ghi"
# => "abc def ghi"
m = /(abc) (?:def) (ghi)/.match(str)
# => #<MatchData "abc def ghi" 1:"abc" 2:"ghi">
Conditional Matches
# Match "b" if the first capture (#1) is matched; otherwise match "c"
str = "abc def ghi"
m = /(abc) (?:def) (ghi)/.match(str)
re = /(a)?(?(1)b|c)/ # or: /(?<first>a)?(?(<first>)b|c)/
re.match("ab")
# => #<MatchData "ab" 1:"a">
re.match("b")
# => nil
re.match("c")
# => #<MatchData "c" 1:nil>
Modifiers - These appear after the closing slash
/i
- case insensitive/m
- multiline; useful to use with non-greedy wildcards/x
- changes the meaning of whitespace; allows you to write comments
Regexp.escape("...")
lets you escape normal strings so that they can be dropped into a regular expression
/#{Regexp.escape('a.c')}/ # or Regexp.new('a.c')
#=> "a\\.c"
Example methods that use regular expressions:
String#scan
, String#split
, String#sub
, String#gsub
,
StringScanner#scan_until
, StringScanner#skip
Enumerable#grep
Additionally, like Enumerable#grep
, case statements use case equality (===
) with regular expressions.
case str
when `^y/i`
# ...
when `^n/i`
# ...
end
IO
Objects
- Familiarity with the C standard library helps with understanding
IO
's API IO
objects have methods likeputs
,print
, andwrite
STDERR
,STDIN
, andSTDOUT
areIO
objects available to every programIO
objects are enumerable: they have methods for iteration, such aseach
- Having
each
implies having methods likeselect
$/
is the newline character for a given system, soeach
uses it to determine lines. It can be changed to something else the program needs it.
- Having
$/ == "\n"
# => true
STDIN
,STDOUT
, andSTERR
correspond to$stdin
,$stdout
, and$stderr
- The global variables allow you to redirect to another
IO
, like$stderr = $stdout
- The global variables allow you to redirect to another
- Input is retrieved through
gets
andgetc
- From the keyboard, by default
File
objects
read
reads an entire file into a string- Also available as a class method
readlines
reads an entire file into an array of strings- Also available as a class method
readline
is similar togets
(but raises an error if reading past EOF)- Same for
readchar
andreadbyte
vsgetc
andgetbyte
- Same for
rewind
moves the position pointer back to the beginningpos
to get or set the current positionseek
has different ways of setting the current position
Enumerable
methods are available throughIO
(i.e.,each
)ungetc
puts a character onto the stream; opposite ofgetc
- Output modes
w
write (creates a new file / overwrites an existing)a
append (creates a new file if needed)
- Metadata of a
File
size
gets the size of a file *exist?
empty?
zero?
directory?
/file?
/symlink?
readable?
/writable?
/executable?
File::Stat
has some other attributesfile?
is it actually a file?
Kernel#test
is terser way of executing some of these queries
Directories
Dir
is similar toFile
entries
returns an array of strings of the directory contentsDir['myfile.*']
orDir.glob('myfile.*)
is likels myfile.*
glob
has optional argument bitsFile::FNM_CASEFOLD
for a case-insensitive searchFile::FNM_DOTMATCH
to match hidden files
mkdir
,chdir
,rmdir
/unlink
/delete
Other file-related classes and modules
FileUtils
is a UNIX-like interface to filesFileUtils::DryRun
andFileUtils::NoWrite
show potential outcomes of commandsPathname
- Dissecting a path:
basename
,dirname
,extname
ascend
helps with navigation relative to the current directory
- Dissecting a path:
StringIO
treats strings like IO objects- Can be helpful when writing tests
Tempfile
OpenURI
opens network resources as Files
require 'open-uri'
OpenURI.open_uri('https://rubycentral.org')
x.class
# => Tempfile
x.size
# => 18734
- Objects have two classes
- The class of which it is an instance
- Its singleton class
- Obtainable through
object.singleton_class
- Obtainable through
- Certain
Numeric
subclasses and symbols cannot have methods added to it- Everything else is fair game
<< object
means the anonymous, singleton class of object
# Two similar techniques
str = 'I am a string'
# Technique 1
class << str
def twice
self + ' ' + self
end
end
# Technique 2
def str.twice
self + ' ' + self
end
# NOTE: There is a scope difference between the two (page 419 uses class constants as an example)
Common to see << object
pattern used to define class methods
# More similar techniques
# (there may be more scope or ordering differences here than noted)
class Ticket
attr_accessor :price
class << self
def most_expensive(*tickets)
tickets.max_by(&:price)
end
end
end
class << Ticket
def most_expensive(*tickets)
# ...
# ...
def Ticket.most_expensive(*tickets)
# ...
# ...
- With respect to singleton classes, methods are encountered in the order of:
- Modules included in the singleton class
- The original class
- Modules included in the original class
- This implies that a module can be mixed in multiple times!
Use the ancestors
method to see the hierarchy
m = module M; end
o = Object.new
class << o
include M
ancestors
end
# => [#<Class:#<Object:0x000000010cd30f88>>, M, Object, PP::ObjectMixin, Kernel, BasicObject]
# ^^^^^^^^^^^^-- o's singleton class ^--- mixin to the singleton
Example of modifying a core Ruby class's method
class String
def shout
"#{self.upcase}!"
end
end
Aliasing methods
alias new old
alias_method :new, :old
extend
is an alternative to explicitly opening up a singleton class
# Add the B module to the lookup path of thing by mixing it into its singleton class
thing = Thing.new
thing.extend(B)
# Extend a class object during its declaration
class Thing
extend B
end
# Extend a class object after its declaration
Thing.extend(B)
extend
allows usage of super
module GsubBangModifier
def gsub!(*args, &block)
super || self
end
end
str = 'Hello there'
str.extend(GsubBangModifier)
# Now the possibility of nil has been eliminated from calling gsub!
Refinements allow for temporary, limited-scope changes to a class
module Shout
refine String do
def shout
self.upcase + '!!!'
end
end
end
module MyScope
using Shout # "using Shout" in effect from here to the end of the module
def self.greeting
"hi".shout
end
end
MyScope.greeting
# => "HI!!!"
"hi".shout
# => undefined method `shout' for "hi":String (NoMethodError)
BasicObject
is the top of the Ruby class tree- Subclasses typically use
method_missing
to extend functionality
- Subclasses typically use
- Callable objects
Proc
objects- lambdas
- method objects
Create and call a Proc object
pr = Proc.new { puts "Inside a Proc's block" }
pr.call
# Shortcut:
pr = proc { puts "Inside a Proc's block" }
pr.call
Method block as a Proc
def call_a_proc(&block)
puts "The block is a: #{block.class}"
block.call
end
call_a_proc { puts "This is in the proc's block, called from the method's block" }
Proc as a method block
p = Proc.new { |x| puts x.upcase }
%w{ Cheese Burger }.each(&p)
The &
also doubles as a call to to_proc
.
For example, some objects have their own to_proc
method:
# Hash
h = { a: 1, b: 2, c: 3}
[:a, :c].map(&h)
# => [1, 3]
(:a..:b).map(&h)
# => [1, 2]
# Symbol
%w(cheese burger).map(&:capitalize)
# => ["Cheese", "Burger"]
# Under the hood, it's like this:
# %w(cheese burger).map { |s| s.public_send(:capitalize) }
Procs are closures
def puts_multiply_by(m)
Proc.new { |x| puts x * m }
end
pm = puts_multiply_by(10)
pm.call(12)
# 120
Procs arity: missing arguments default to nil, and extra can be ignored
pr = Proc.new { |x| p x }
pr.call
# nil
# => nil
Lambdas (a "flavor" of the Proc class) on the other hand complain about the wrong number of arguments
lam = lambda { |x| p x }
lam.call
# => wrong number of arguments (given 0, expected 1) (ArgumentError)
lam.call(1, 2)
# => wrong number of arguments (given 0, expected 1) (ArgumentError)
Another difference between lambdas and procs is return
def return_test
l = ->(x, y) { puts "#{x}, #{y}"; return }
l.call(10, 20)
puts 'Still here!'
p = proc { return }
p.call
puts 'You will not see this message!'
end
return_test
# 10, 20
# Still here!
# => nil
# It follows that this would cause an error:
proc { return }.call
# => unexpected return (LocalJumpError)
Method objects
class C
def name
"In name; self is: #{self}"
end
end
c = C.new
m = c.method(:name)
# => #<Method: C#name() (irb):2>
m.owner
# => C
m.call
# => "In name; self is: #<C:0x0000000108b44a98>"
# Bind the method to another object of the same class/subclass
class D < C
end
d = D.new
unbound = m.unbind # or: unbound = C.instance_method(:name)
# => #<UnboundMethod: C#name() (irb):2>
unbound.bind(d).call
# => "In name; self is: #<D:0x0000000108b75f30>"
Potential use of method objects:
# Where A is a superclass of C's superclass
class A
def my_method
"hi"
end
end
class B < A
def my_method
raise 'Should not get here'
end
end
class C < B
# Execute the version of the my_method two classes up the chain
def my_method_from_A
A.instance_method(:my_method).bind(self).call
end
end
C.new.my_method_from_A
# => "hi"
- Synonyms of the
call
methodmult[3, 4] # mult.call(3, 4)
mult.(3,4) # mult.call(3, 4)
The eval
family
eval
- evaluate a string as codeinstance_eval
- a temporary shift in the value of self- Can be called on a block instead of a string
instance_exec
can take arguments and pass them to the block
- Commonly used in initializer blocks (see example below)
- Can be used to peek at instance variables:
obj.instance_eval { @hidden }
- Can be called on a block instead of a string
class_eval
/module_eval
- side trip into the context of a class-definition block- Can be called on a block instead of a string
- Can do things that a regular
class
definition cannot- Open the class definition of an anonymous class
- Use existing local variables (i.e, the surrounding variables) inside a class-definition body
# eval example
eval('2+2')
# => 4
eval("def my_f; 'In my_f'; end")
# => "In my_f"
# instance_eval example
self
# => main
a = :cheese
a.instance_eval { self } # or a.instance_eval('p self')
# => :cheese
# instance_eval example 2, initializer block
class Person
# ...
def initializer(&block)
instance_eval(&block)
end
# ...
def name(name=nil)
@name ||= name
end
# ...
end
joe = Person.new do
name 'Joe'
end
# class_eval example
var = 'initialized variable'
class C
end
# This would error:
# class C
# var
# end
# This does not error:
C.class_eval { var }
# => "initialized variable"
# class_eval example #2
var = 'initialized variable'
class C
# This errors because the def...end block is a new scope
C.class_eval { def talk; var; end }
C.new.talk
# Using define_method prevents this error
C.class_eval { define_method('talk') { var } }
C.new.talk
# => "initialized variable"
# class_eval example #3, method_missing + define_method
class Name
def method_missing(m, args, &block)
self.class.send(:define_method, m) do |args|
instance_variable_set("@#{m.to_s.chop}", args)
end
send(m, args)
end
end
n = Name.new
n.cheese = 'burger'
n
# => #<Name:0x0000000107f75668 @cheese="burger">
Binding
- "Binding objects encapsulate the local variable bindings in effect at a given point in execution"
binding
returns whatever the current binding is- Allow evaluation in the context of a given binding
# Allows str to be visible when the eval executes "puts str"
def use_a_binding(b)
eval('puts str', b)
end
str = 'This is my string'
use_a_binding(binding)
-
History trivia:
Object#tainted?
and$SAFE
are no longer supported or accurate -
Threads
- Receive a block to run on initialization
- Useful methods:
join
,alive?
,stop
,status
,wakeup
Thread
s are similar toFiber
sFiber
s are like reentrant code blocks, likeEnumerator
s
- Thread-local globals
$1
,$2
,...
captures are thread-local globals$?
(set by the last system call of a thread)
- Thread keys are a key-value store for thread-specific symbols or strings
Thread.current[:key] = 'value'
t.fetch(:key) # => "value"
Executing system commands
system('date')
# Thu Mar 7 22:57:17 CST 2024
# => true
system('asdf', exception: true)
# in `system': No such file or directory - asdf (Errno::ENOENT)
$?
# => #<Process::Status: pid 11955 exit 0>
`date` # Backticks can also be used
# => "Thu Mar 7 23:02:43 CST 2024\n"
%x(date) # Yet another way
# => "Thu Mar 7 23:02:43 CST 2024\n"
exec('date') # This replaces the current process!
# Thu Mar 7 23:03:49 CST 2024
# <process exits back to the shell>
d = open('|date')
# Using open:
d = open('|date')
# => #<IO:fd 9>
d.gets
# => "Thu Mar 7 23:05:02 CST 2024\n"
d.close
# => nil
# NOTE: The Open3.popen3 bidirectional method is not shown here
method_missing
example
class Cookbook
attr_accessor :title, :author
def initialize
@recipes = []
end
def method_missing(m, *args, &block)
@recipes.public_send(m, *args, &block)
end
end
class Recipe
attr_accessor :main_ingredient
def initialize(main_ingredient)
@main_ingredient = main_ingredient
end
end
cb = Cookbook.new
recipe_for_cake = Recipe.new('flour')
recipe_for_chicken = Recipe.new('chicken')
# Notice in the next four lines, << and select are delegated to @recipes via method missing
cb << recipe_for_cake
cb << recipe_for_chicken
chicken_dishes = cb.select { |recipe| recipe.main_ingredient == 'chicken' }
chicken_dishes.each { |dish| puts dish.main_ingredient }
-
By default,
method_missing
will not considerrespond_to?
queries ormethod
calls- Must override
respond_to_missing?
to do this
- Must override
-
include
,prepend
,extend
have the hooksincluded
,prepended
, andextended
, respectively. Example:- "Extending an object with a module is the same as including that module in the object's singleton class"
- But the two operations trigger different callbacks (
extended
andincluded
)
- But the two operations trigger different callbacks (
- "Extending an object with a module is the same as including that module in the object's singleton class"
-
More hook names:
method_added
,singleton_method_added
module M
def self.included(c)
puts "#{self} has just been mixed into #{c}"
# NOTE: Could do something crazy, like write `def c.a_class_method; end` here.
end
end
class C
include M
end
# M has just been mixed into C
# => C
Inheritance has the hook inherited
class C
def self.inherited(subclass)
puts "#{self} just got subclasses by #{subclass}"
end
end
class D < C
end
# C just got subclasses by D
# => nil
class E < D
end
# D just got subclasses by E
# => nil
const_missing
example
class C
def self.const_missing(const)
puts "#{const} is undefined; setting it to 1"
const_set(const, 1)
puts "But returning 0 for right now"
0
end
end
C::A
# A is undefined; setting it to 1
# But returning 0 for right now
# => 0
C::A
# => 1
Object capability queries
- Listing non-private methods:
obj.methods
'Test string'.methods.grep(/case/).sort # => [:casecmp, :casecmp?, ...]
- Filtering out the
Kernel
andBasicObject
non-private methods:obj.methods - BasicObject.instance_methods(false) - Kernel.instance_methods(false)
obj.private_methods
obj.protected_methods
- Filtering out the
Kernel
andBasicObject
private methods:obj.private_methods - BasicObject.private_instance_methods(false) - Kernel.private_instance_methods(false)
- Class and module method queries
Object.methods.grep(/methods/).sort
instance_methods
methods
private_instance_methods
private_methods
protected_instance_methods
protected_methods
public_instance_methods
public_methods
singleton_methods
undefined_instance_methods
- When calling any of these with
false
ornil
, it does not report ancestors' methods
- Example: What methods in
Enumerable
are overridden inRange
?Range.instance_methods(false) & Enumerable.instance_methods(false)
singleton_methods
Introspection of variables and constants
local_variables
global_variables
instance_variables
- This is a public method, so it can be called from outside of the object
Tracing execution
caller
- the stack trace as an array of strings
Immutability
- Unhandled exceptions are considered side effects
Object#freeze
andObject#frozen?
help ensure immutability- Does not guarantee immutability for all attributes, though (see previous sections)
- Frozen string literals feature flags are often misunderstood
Higher-order functions
- Examples:
map
,select
,each_byte
, and many more
Method chaining
- Example:
'joe'.upcase.reverse
itself
helps with expressivity; these have the same result:%w(a a a b b c).group_by { |name| name }
%w(a a a b b c).group_by(&:itself)
- Can be specified as a symbol for a default argument, e.g.:
def func(a, b, c, method=:itself)
yield_self
orthen
is liketap
but returns the result of the block instead of the receiver
Currying allows us to evaluate parts of a function rather than the entire function at once
find_multiples = -> (x, arr) { arr.select { |el| el % x == 0 } }
find_multiples_of = find_multiples.curry # Returns a lambda
find_multiples_of.call(3, (0..10))
# => [0, 3, 6, 9]
find_multiples_of_3 = find_multiples_of.(3) # Recall that .(3) is short for .call(3)
find_multiples_of_5 = find_multiples_of[5] # Recall that [5] is short for .call(5)
find_multiples_of_3.(0..10)
# => [0, 3, 6, 9]
find_multiples_of_5[0..10]
# => [0, 5, 10]
curry(n)
has an optional argument n
that means "the curried object will only be evaluated once n arguments have been supplied".
sum_all = -> (*nums) { nums.reduce(:+) }
sum_at_least_four = sum_all.curry(4)
sum_at_least_four = sum_all.curry(4)
# => #<Proc:0x0000000103266a48 (lambda)>
sum1 = sum_at_least_four.(1)
# => #<Proc:0x000000010720ef38 (lambda)>
sum1 = sum_at_least_four.(1, 2) # Could have also been written as: sum1.(2)
# => #<Proc:0x0000000107227948 (lambda)>
sum1 = sum_at_least_four.(1, 2, 3)
# => #<Proc:0x0000000106fedb78 (lambda)>
sum1 = sum_at_least_four.(1, 2, 3, 4)
# => 10 <--- ACTUAL EVALUATION
Methods can also be curried
def add(a, b, c)
a + b + c
end
fun = method(:add).curry
fun[1,2, 3]
# => 6
fun2 = fun[1,2]
fun2[3]
# => 6
An example of lazy evaluation + currying
def find_multiples(num, mult)
(1..Float::INFINITY).lazy.select { |x| x % mult == 0 }.first(num)
end
first_3_multiples = self.method(:find_multiples).curry.(3)
first_3_multiples.(256)
# => [256, 512, 768]
first_5_multiples = self.method(:find_multiples).curry.(5)
# => [256, 512, 768, 1024, 1280]
- NOTE: Ruby does not mandate tail call optimization in its language specification
- For YARV, it must be turned on first
Binding#source_location
# When used in irb
# ...
binding.source_location
# => ["(irb)", 10]
binding.source_location
# => ["(irb)", 11]
# ...
Kernel#yield_self
now has an alias named#then
- Other changes
RubyVM::AbstractSyntaxTree.parse_file
- Endless ranges, for example:
(1..)
Enumerator#+
to generate an enumerator chain of[self, other]
Enumerable#chain
to generate an enumerator chain of[self, *others]
Pattern Matching (Experimental)
require "json"
json = <<END
{
"name": "Alice",
"age": 30,
"children": [{ "name": "Bob", "age": 2 }]
}
END
case JSON.parse(json, symbolize_names: true)
in { name: "Alice", children: [{ name: "Bob", age: age }] }
p age # => 2
end
GC.compact
- Automatic conversion of keyword arguments and positional arguments is deprecated
- Beginless range (experimental)
- Example:
arr[..3]
- Example:
Enumerable#tally
counts the occurrence of each element- Example:
["a", "b", "c", "b"].tally
- Example:
Enumerator::Lazy#eager
generates a non-lazy enumerator from a lazy one
Ractor - "an Actor-model like concurrent abstraction designed to provide a parallel execution feature without thread-safety concerns"
def tarai(x, y, z) =
x <= y ? y : tarai(tarai(x-1, y, z),
tarai(y-1, z, x),
tarai(z-1, x, y))
# sequential version:
# 4.times{ tarai(14, 7, 0) }
require 'benchmark'
Benchmark.bm do |x|
# parallel version:
x.report('parallel'){
4.times.map do
Ractor.new { tarai(14, 7, 0) }
end.each(&:take)
}
end
Fiber#scheduler
- "for intercepting blocking operations"Hash#except
rbs
gem andtypeprof
for static analysis- One-line pattern matching redesign
- Example:
{b: 10, c: 20} => {b:}
setsb = 10
- Example:
in
returns true or false instead of raising an error- Example:
0 in 1
returnsfalse
- Example:
- Endless method definition
- Example:
def square(x) = x * x
- Example:
- Hash values can be omitted
- Example:
{x:, y:}
is syntax sugar for{x: x, y: y}
- Example:
foo(x:, y:)
is syntax sugar forfoo(x: x, y: y)
- Example:
debug
gemerror_highlight
gem
Data
immutable value object class- Similar to
Struct
for immutable support - Example:
Measure = Data.define(:amount, :unit)
weight = Measure.new(amount: 50, unit: 'kg')
weight.with(amount: 40)
returns a new instance rather than mutate the receiver
- Similar to
- WASI-based WebAssmbly support
Regexp.timeout = n
andRegexp.new(..., timeout: n)
- "A proc that accepts a single positional argument and keywords will no longer autosplat"
- Example:
proc { |a, **k| a }.call([1, 2])
- Before:
=> 1
- Now:
=> [1, 2]
- Before:
- Example:
Changes are mostly under the hood: "A new parser named Prism, uses Lrama as a parser generator, adds a new pure-Ruby JIT compiler named RJIT, and many performance improvements especially YJIT"