33
loading...
This website collects cookies to deliver better user experience
Array
and one which uses a custom class ThingList
:things = ["foo", "bar", "baz"]
things.each do |thing|
puts thing
end
things = ThingList.new("foo", "bar", " baz")
things.each do |thing|
puts thing
end
ThingList
introduces a lot of uncertainty versus the more known Array
, especially because as mentioned why else would someone use that instead of an Array
?Hashie
gem tried to implement dot-access (hash[:a]
can be called as hash.a
) and there were all types of issues around that.true
and false
are fairly universal concepts, and as mentioned if they meet your needs you should use them. One thing, however, to watch out for is that they're instances of TrueClass
and FalseClass
, Ruby doesn't really have a concept of Boolean
unless you're using something like Steep or Sorbet.?
in Ruby:1.kind_of?(Integer)
# => true
1 > 2
# => false
1 == 1
# => true
Note:===
behaves very differently in Ruby, but that's a topic for a later discussion
[true, response]
or [false, error]
, but another subject for later.nil
and some of the common usages:[].first
# => nil
{1=>2}[3]
# => nil
nil
should be understood as nothing, we return it when there's nothing to return. In the first case there's no first element of the Array
, and in the second there's no key for 3
.Note: Hash
can have a default value assigned through either Hash.new(0)
or Hash.new { |h, k| h[k] = [] }
which overrides the idea that "nothing" was there, but that's beyond the point being made here.
!nil
is true
and !1
is false
:!nil
# => true
!1
# => false
Boolean
-like values:!!nil
nil
should be avoided unless it's genuinely the case that there's "nothing" there. Consider this case:[1, 2, 3].select { |v| v > 4 }
# => []
Array
which is the "nothing" of this particular case. If we returned nil
instead and tried to do this what do you think might happen?:[1, 2, 3].select { |v| v > 4 }.map { |v| v * 2 }
[1, 2, 3]
there's "nothing" there but in other cases like [4, 5, 6]
? That's valid. One might notice some patterns here with "empty" or "nothing" values, but that strafes hard into Functional Programming territory and a very fun idea you could read more about here if you're particularly adventurous.nil
when it makes sense.!
) methods:"a".gsub!('b', '')
# => nil
[2, 4, 6].select!(&:even?)
# => nil
["a", "b", "c"].reject!(&:empty?)
# => nil
!
methods with some frequency as I've been caught by that more than once, and often times you really don't need them. General rule for me is to avoid mutation and mutating methods unless absolutely necessary as it breaks chaining and a lot of intuition about how Ruby works.@cached_value ||= some_expression
# or
cache[:key] ||= some_expression
some_expression
is false
or nil
it'll reevaluate instead of being "cached" for later use. The suggested alternative is to use defined?
instead:if defined?(@cached_value)
@cached_value
else
@cached_value = some_expression
end
def another_expression
return @cached_value if defined?(@cached_value)
@cached_value = some_expression
end
Hash
es for caching using fetch
which has some additional fun behavior:cache.fetch(:key) { cache[:key] = some_expression }
fetch
does things which may be important to mention here:hash = { a: 1 }
# => {:a=>1}
hash.fetch(:a)
# => 1
hash.fetch(:b, 1)
# => 1
hash.fetch(:b) { 1 }
# => 1
hash.fetch(:b)
# KeyError (key not found: :b)
fetch
on a value which is not present without either a default or provided block it'll raise a KeyError
, which can be very useful for ensuring things are present.true
, false
, and nil
are going to be faster than most other Ruby objects due to being immediate object types. That means there's no requirement for memory allocation on create or indirection on accessing them later, making them faster than non-immediate objects.Integer
type rather than fractional ones. Ruby also offers floats, rationals, and BigDecimal among a few others if you count non-base-10 variants. They're all under the Numeric
class.Note: - As mentioned, BigDecimal
is not required by default: require 'big_decimal'
. It also has a particularly pesky compatibility break in which BigDecimal.new
will break versus BigDecimal()
. I still don't get why they didn't leave it and just alias it, but alas here we are.
times
:10.times do
# executed 10 times
end
3.times do |i|
puts i
end
# 0
# 1
# 2
for
loop equivalency and this may lead to some confusion and introduction of counter variables where one is already built in to cover that case.Integer
s and one that he brings up here is what happens with truncation:5 / 10
# => 0
7 / 3
# => 2
Rational
(because Float
has its own bit of fun we cover later.)Rational
or Float
here:# or Rational(5, 10) or 5 / 10.to_r
5 / 10r
# => (1/2)
# Float
7.0 / 3
# => 2.3333333333333335
Float
is noted as the fastest, but they're not precisely exact. This site has a good explanation as to why, but the short version is not enough digits to represent all numbers, and the more things you do to a Float
the more apparent it becomes as in this example:f = 1.1
v = 0.0
1000.times do
v += f
end
v
# => 1100.0000000000086
Rational
can get around this with more precision, but is slower in general. If you're dealing with any type of money or things which require precision though Float
is a bad idea to use.Rational
instead the book shows this:f = 1.1r
v = 0.0r
1000.times do
v += f
end
v
# => (1100/1)
Float
is probably fine.BigDecimal
in this equation? Let's take a look at the examples provided:v = BigDecimal(1) / 3
v * 3
# => 0.999999999999999999e0
f = BigDecimal(1.1, 2)
v = BigDecimal(0)
1000.times do
v += f
end
v
# => 0.11e4
v.to_s('F')
# => "1100.0"
BigDecimal
uses scientific notation, as the name implies, so it can deal with very large numbers. The book doesn't go into a lot of detail here, and quite frankly I've rarely had to use them in Ruby myself.BigDecimal
or Rational
might be used.Symbol
vs String
and when both are used. I have my personal opinions on this, but will save those for later.Hash#with_indifferent_access
to bypass needing to care about the difference. In the background a lot of Ruby, as the book mentions, will also do this conversion."A string in Ruby is a series of characters or bytes, useful for storing text or binary data. Unless the string is frozen, you append to it, modify existing characters in it, or replace it with a different string."
String
s, Ruby even has the frozen string literal comment to do this that goes at the top of a file:# frozen_string_literal: true
Symbol
would become more difficult to justify, though there would still be some marginal performance gains from their implementation."A symbol in Ruby is a number with an attached identifier that is a series of characters or bytes. Symbols in Ruby are an object wrapper for an internal type that Ruby calls ID, which is an integer type. When you use a symbol in Ruby code, Ruby looks up the number associated with that identifier. The reason for having an ID type internally is that it is much faster for computers to deal with integers instead of a series of characters or bytes. Ruby uses ID values to reference local variables, instance variables, class variables, constants, and method names."
Symbol
, though does get into some important implementation details. More simply a Symbol
is an identifying text to describe a part of your Ruby code.Symbol
representing their name like def add
could be represented as :add
elsewhere in the program, and passed to send
to retrieve the method code:method = :add
foo.send(method, bar)
Caveat: Personally I would prefer method_name
here as method
itself is a Method that can be used to get a method
by name, which can be confusing.
method = "add"
foo.send(method, bar)
String
methods will work on a Symbol
, compounding this.def switch(value)
case value
when :foo
# foo
when :bar
# bar
when :baz
# baz
end
end
Symbol
s as identifying text rather than as text itself. If we were to want to do something with value
, however, Symbol
would not make much sense:def append2(value)
value.gsub(/foo/, "bar")
end
value
works as a String
, so we should ensure a String
is passed to it.Symbol
. Whatever performance gains there are from this are not worth the confusion it incurs on the users, and should be avoided.String
values instead:const map = { a: 1, b: 2, c: 3 };
map['a'] // => 1
map.a // => 1
Hashie
talk from RubyConf, but that's another matter.Symbol
it sure likes to pretend they don't exist and coerce things to prevent users from getting errors in a lot of cases.Array
interesting in Ruby, but that's not the point of this book so I digress. At the least I would highly recommend reading into Enumerable
on the official docs after this chapter to get an idea of what all is possible.[[:foo, 1], [:bar, 3], [:baz, 7]].each do |sym, i|
# ...
end
sym
and i
here. Note that there's a real subtle thing to keep in mind on this versus a Hash
though: You can have multiple instances of :foo
here, but only one in a Hash
which wants unique keys.{ foo: 1, bar: 3, baz: 7 }.each do |sym, i|
# ...
end
Array
solution is likely more correct from a design perspective, but that the Hash
is easier to implement. I would be inclined to agree with that, except in the case mentioned above where things could get complicated.Array
tuples, representing that as a Hash
would be a bad idea. Keep in mind your underlying data when deciding on how to express it in Ruby.album_infos = 100.times.flat_map do |i|
10.times.map do |j|
["Album #{i}", j, "Track #{j}"]
end
end
flat_map
flattens after mapping (transforming) a collection, but this book does assume intermediate Ruby knowledge to be fair.Array
it might look like this (and Rails does something similar):class Array
def index_by(&block)
indexes = {}
self.each { |v| indexes[block.call(v)] = v }
indexes
end
end
album_artists = {}
album_track_artists = {}
album_infos.each do |album, track, artist|
(album_artists[album] ||= []) << artist
(album_track_artists[[album, track]] ||= []) << artist
end
album_artists.each_value(&:uniq!)
album_artists = Hash.new { |h, k| h[k] = Set.new }
album_track_artists = Hash.new { |h, k| h[k] = Set.new }
album_infos.each do |album, track, artist|
album_artists[album].add artist
album_track_artists[[album, track]].add artist
end
Set
can only have unique values, but that also makes the solution more complicated and harder to explain in the first chapter so I can understand why it was written that way.lookup = -> (album, track = nil) do
if track
album_track_artists[[album, track]]
else
album_artists[album]
end
end
def lookup(album, track = nil)
# ...
end
album_artists
and album_track_artists
then? This solution avoids that by using lambda functions, which capture the local context they're defined in through what's called a closure.albums = {}
album_infos.each do |album, track, artist|
((albums[album] ||= {})[track] ||= []) << artist
end
albums = Hash.new do |h, k|
h[k] = Hash.new { |h2, k2| h2[k2] = [] }
end
lookup = -> (album, track = nil) do
if track
albums.dig(album, track)
else
a = albums[album].each_value.to_a
a.flatten!
a.uniq!
a
end
end
Array
-tuple approach takes a lot more memory, but has much faster lookup for a large number of records. The second is far more inefficient on just album
lookups, but excels in nested queries.albums = {}
album_infos.each do |album, track, artist|
album_array = albums[album] ||= [[]]
album_array[0] << artist
(album_array[track] ||= []) << artist
end
albums.each_value do |array|
array[0].uniq!
end
1
to 99
will be the tracks. We could explicitly model the data but that gets pretty messy:TRACK_COUNT = 99
albums = Hash.new { |h, k| h[k] = [Set.new, *([] * TRACK_COUNT)]}
dig
function works with both Hash
and Array
, meaning numbered indexes work here, making the lookup function much simpler:lookup = -> (album, track = 0) do
albums.dig(album, track)
end
album_artists = album_infos.flat_map(&:last)
album_artists.uniq!
lookup = -> (artists) do
album_artists & artists
end
Hash
to key known artists:album_artists = {}
album_infos.each do |_, _, artist|
album_artists[artist] ||= true
end
lookup = -> (artists) do
artists.select do |artist|
album_artists[artist]
end
end
values_at
:lookup = -> (artists) do
album_artists.values_at(*artists)
end
Set
, so let's get to that instead:require 'set'
album_artists = Set.new(album_infos.flat_map(&:last))
lookup = -> (artists) do
album_artists & artists
end
Set
is much faster than the Array
approach, but not quite as fast as the Hash
one. The book recommends the former for the nicer API, and the latter if you need the performance gain.Struct
, especially when I'm in a REPL. Glad to see it here. Jeremy starts with an example here of a normal class:class Artist
attr_accessor :name, :albums
def initialize(name, albums)
@name = name
@albums = albums
end
end
Struct
:Artist = Struct.new(:name, :albums)
Struct
also covers that case:Artist = Struct.new(:name, :albums, keyword_init: true)
Artist.new(name: 'Brandon', albums: [])
Struct
is lighter than a class
but takes longer to look up attributes.Struct
, a new instance is actually a Class
:Struct.new(:a, :b).class
# => Class
Struct.new('A', :a, :b).new(1, 2).class
# => Struct::A
Struct.new
method might look like:def Struct.new(name, *fields)
unless name.is_a?(String)
fields.unshift(name)
name = nil
end
subclass = Class.new(self)
if name
const_set(name, subclass)
end
# Internal magic to setup fields/storage for subclass
def subclass.new(*values)
obj = allocate
obj.initialize(*values)
obj
end
# Similar for allocate, [], members, inspect
# Internal magic to setup accessor instance methods
subclass
end
'A'
to it it'll define a constant on the current namespace with that subclass attached to it. There's a bit of hand-waving on underlying details here, which would definitely take a bit, then the final section on actually making a new instance.class SubStruct < Struct
end
Struct
for most cases.A = Struct.new(:a, :b) do
def initialize(...)
super
freeze
end
end
nil
is literally nothing, and quite frequently errors you see in Ruby are due to one getting in somewhere where the application does not expect it.false
is an instance of FalseClass
, so not sure I get the intent of this particular question when juxtaposed with nil
. Perhaps this would be phrased better on what the intentions of these data types are instead?BigDecimal
types yes, but if a Float
gets on one side not as much.Symbol
to go away because it makes things far more complicated for new Rubyists for very very little real gains, and even trips me up on a semi-frequent basis. I dislike them for the complexity they introduce to the language.Hash
, but not by much. I seem to recall that Set
is implemented in terms of a Hash
anyways so it can't be that far off.Struct.new
and Class.new
I'd think.Symbol
and String
and had a fairly reasoned response to it. I might have liked to see the implications of removing one, but understand that that'd ballon the size of this chapter real quick.album
, which gave a lot more of a chance to explore interesting code. Too many examples feel really basic and don't really show a lot of potential concerns, and I think this book gets that right.true
, false
, and nil
went more into reasoned default values rather than dive into bang methods as much as it did, as those will find more use in a lot of Ruby programs to prevent errors.||=
use.Struct
veered from a very useful overview to a bit into the weeds and lost me.