Today I learned that bundle open
is a way to easily look inside Ruby gems!
Notes to future self:
Watch out for integer division!
9 / 10 # => 0 🤔
Order of operations matters!
Float(9/10) # => 0.0 🤔🤔
Ruby has you covered!
Float(9)/10 # => 0.9 😀
9.to_f/10 # => 0.9 😀
9.fdiv(10) # => 0.9 😀
The latest Ruby Weekly newsletter linked to a helpful blog post about magic comments (or pragmas) in Ruby by Mehdi Farsi. It’s a short read and I appreciated getting some more info on a topic I’ve never thought too much about. A couple of his examples made me a little suspicious though, so I did some more digging.
When specifying encoding, he demonstrates that the first magic comment is applied and any other encoding comments are ignored. That makes intuitive sense to me: you can’t change encoding part way through a file (at least I really hope you can’t, encodings cause enough confusion as it is!!!).
For his next two tests, Mehdi simply gave the magic comment two different settings at the top of the file and observed that the setting in the second one takes effect. He then concluded that only the last one is processed while the others are skipped.
Another possible conclusion is that both were processed and the different settings were in effect for different parts of the file. The way to test that would be to use a bigger example and turn the settings on and off multiple times:
# indentation-precedence-test.rb
# warn_indent: true
puts "\n*** Indentation Warnings: true ***"
def bad_indentation_1
"#{__method__} (line #{__LINE__})"
end
p bad_indentation_1
# warn_indent: false
puts "\n*** Indentation Warnings: false ***"
def bad_indentation_2
"#{__method__} (line #{__LINE__})"
end
p bad_indentation_2
# warn_indent: true
puts "\n*** Indentation Warnings: true ***"
def bad_indentation_3
"#{__method__} (line #{__LINE__})"
end
p bad_indentation_3
# warn_indent: false
puts "\n*** Indentation Warnings: false ***"
def bad_indentation_4
"#{__method__} (line #{__LINE__})"
end
p bad_indentation_4
In this test we try to turn the indentation warnings on and off (and on and off again). When I run it with ruby indentation-precedence-test.rb
I get the following output:
indentation-precedece-test.rb:6: warning: mismatched indentations at 'end' with 'def' at 4
indentation-precedece-test.rb:24: warning: mismatched indentations at 'end' with 'def' at 22
*** Indentation Warnings: true ***
"bad_indentation_1 (line 5)"
*** Indentation Warnings: false ***
"bad_indentation_2 (line 14)"
*** Indentation Warnings: true ***
"bad_indentation_3 (line 23)"
*** Indentation Warnings: false ***
"bad_indentation_4 (line 32)"
Despite having four badly indented methods we only received two warnings; one for the first method (lines 4-6) and one for the third method (lines 22-24). These two methods were (badly) defined after turning warnings on. Conversely, the other two methods don’t cause warnings. They were (badly) defined after turning warnings off. This confirms that all of the warn_indent
settings in the magic comments are applied, simply changing the state from that point in the file onwards.
But what about in other files when you require them? Does the setting carry over?
# indentation-test-requiring.rb
puts "\n*** Main file, Indentation Warnings: default ***"
def bad_indentation_1
"#{__method__} (line #{__LINE__})"
end
p bad_indentation_1
# warn_indent: true
puts "\n*** Main file, Indentation Warnings: true ***"
def bad_indentation_2
"#{__method__} (line #{__LINE__})"
end
p bad_indentation_2
require_relative 'indentation-test-inherit.rb'
# warn_indent: false
puts "\n*** Indentation Warnings: false ***"
def bad_indentation_4
"#{__method__} (line #{__LINE__})"
end
p bad_indentation_4
require_relative 'indentation-test-override.rb'
puts "\n*** Main file, Indentation Warnings: did they change??? ***"
def bad_indentation_6
"#{__method__} (line #{__LINE__})"
end
p bad_indentation_6
# indentation-test-inherit.rb
puts "\n*** New file, Indentation Warnings: Do they inherit??? ***"
def bad_indentation_3
"#{__method__} (line #{__LINE__})"
end
p bad_indentation_3
# indentation-test-override.rb
# warn_indent: true
puts "\n*** Another new file, Indentation Warnings: true ***"
def bad_indentation_5
"#{__method__} (line #{__LINE__})"
end
p bad_indentation_5
In this example we only get two warnings, one for method 2 in the main file and one for method 5 in the “override” file (that we explicitly turned them back on for).
This shows us a few things:
But do the same rules hold for frozen string literals?
Let’s start by confirming the default state for frozen string literals is off:
# frozens-test.rb
string_1 = 'frozen? 1'
p "string_1.frozen?: #{string_1.frozen?}" # => "string_1.frozen?: false"
Now let’s turn it on for the next one:
# frozens-test.rb
string_1 = 'frozen? 1'
p "string_1.frozen?: #{string_1.frozen?}" # => "string_1.frozen?: false"
# frozen_string_literal: true
string_2 = 'frozen? 2'
p "string_2.frozen?: #{string_2.frozen?}" # => "string_2.frozen?: false"
Wait, what!? It’s not frozen! Unlike the indentations warning, it seems this particular magic comment has to come at the top of the file. But in Mehdi’s test he had another comment before it. Let’s test if all comments and whitespace are okay:
# frozens-test-2.rb
# frozen_string_literal: true
# Random comment after turning string literals on...
#... and lots of white space before we turn them back off
# frozen_string_literal: false
# Different comment types are fine...
=begin
If you want to read
a block-commented Haiku
this will have to do
=end
# Other pragmas (magic comments) are fine too
# # warn_indent: true
# We can still turn them on...
# frozen_string_literal: true
string_3 = 'frozen? 3'
p "string_3.frozen?: #{string_3.frozen?}" # => "string_3.frozen?: true"
It turns out comments are fine, white space is fine, and other pragmas (magic comments) are fine.
So maybe it just needs to be before any strings?
# frozens-test-3.rb
2 + 2
# frozen_string_literal: true
string_4 = 'frozen? 4'
p "string_4.frozen?: #{string_4.frozen?}" # => "string_4.frozen?: false"
Nope! Adding 2 numbers together (or even defining an empty method), prevents it from being used. In my testing, the only vaguely code-like things that seem to be allowed before this magic comment are pointless references to existing objects:
# frozens-test-4.rb
# nil is okay:
nil
# Mentioning special variables (like the last exception) seems to be okay
$!
# Referencing the args array is okay too, even if it's not empty
$*
# Referencing the file path is also fine
__FILE__
# frozen_string_literal: true
string_5 = 'frozen? 5'
p "string_5.frozen?: #{string_5.frozen?}" # => "string_5.frozen?: true"
And just when you thought I’d plumbed the boring depths of this, I found one last exception to the rule:
# frozens-test-5.rb
# It doesn't like special variables on consecutive lines!!!
$!
$*
# frozen_string_literal: true
string_6 = 'frozen? 6'
p "string_6.frozen?: #{string_6.frozen?}" # => "string_6.frozen?: false"
So in conclusion:
# frozen_string_literal
has to come before any (useful) codeWhile I was thinking about security I decided to double check that I’m practicing what I preach.
When building a Rails app I don’t want to roll my own security (too easy to miss things) but I want something that’s simpler and lighter weight than Devise. There are lots of good options in this space but I usually reach for Clearance… I respect the Thoughtbot team’s approach to development, they actively maintain Clearance for use in their own projects and they were responsive when I flagged an issue using Clearance with the upcoming Rails 6 release.
Anyway, I kind of knew that Clearance used BCrypt and salting, but when I looked in my database schema I couldn’t find a column for storing the random salt. After doing a little digging, I discovered that bcrypt-ruby, quite cleverly, securely generates the salt for you and then appends it to the end of the now salted and hashed password. All you need to do is store the result in a single field.
Not only does this prevent well-meaning but clueless developers (i.e. me) from doing silly things with the salt (in a mis-guided attempt at “making it more secure”), but it also simplifies the interface of the library.
On a related note, bcrypt-ruby
cleverly overrides the ==
method to let you compare a plaintext password (e.g. that someone just provided at log in) with the stored hash/salt combo to see if it’s a match. Jesus Castello has written a helpful article that just happens to explain this exact mechanism.
All in all, it’s an excellent example of making it easy for the user (in this case application developers) to do the right thing.
Today I learned that Ruby modules are actually implemented as classes under the hood.
When you include
a module (which is kind of a class in disguise), it actually gets dynamically inserted into the inheritance tree.
Consider the example of a school. We might have a Person class to model every person in the school with some common attributes:
class Person
attr_accessor :given_name
attr_accessor :surname
end
Then we might have subclasses for students, staff and parents to model attributes specific to each group. Focusing on the student, our program might look like this:
class Person
attr_accessor :given_name
attr_accessor :surname
end
class Student < Person
attr_accessor :year_level
end
To add attributes to staff and students that don’t apply to parents we could define a module and mix it in. Let’s say we want to be able to add students (e.g. Harry and Ron) and staff (e.g. Minerva McGonagall) to a house (e.g. Gryffindor). Our program now looks more like this:
class Person
attr_accessor :given_name
attr_accessor :surname
end
module House
attr_accessor :name
attr_accessor :colour
end
class Student < Person
include House
attr_accessor :year_level
end
Previously Student inherited directly from Person but now the House module (or, more accurately, a copy of it) gets inserted as the new parent class of Student. Under the hood the inheritance hierarchy now looks more like Student < House < Person
.
That’s all well and good but Ruby doesn’t support multiple inheritance; each class can only have one parent. So how does including multiple modules work? It turns out, they each get dynamically inserted into the inheritance tree, one after another.
Stretching our example a little further, if we had some attributes common to parents and students, we might use another module. Perhaps we want to record the household so we can model that Ron, Ginny and Mr & Mrs Weasley are all part of the “Weasley” household (I’m stretching this example quite far, hopefully it doesn’t snap and take out an eye). Our student class might now look like this:
class Person
attr_accessor :given_name
attr_accessor :surname
end
module House
attr_accessor :house_name
attr_accessor :house_colour
end
module Household
attr_accessor :household_name
attr_accessor :address
end
class Student < Person
include House
include Household
attr_accessor :year_level
end
Under the hood, Ruby first inserts House as the new parent (superclass) of Student but then on the very next line it inserts Household as the parent of Student. The inheritance chain is now Student < Household < House < Person
.
Episode 40 of Remote Ruby was so thought-provoking I added it back to my podcast queue.
After listening to it again my previous thoughts on Graphiti (at the end of that post) still stand. But something else jumped out at me this time…
Lee speaks passionately about Graphiti and Vandal enabling product owners or “business people” (I cringe at the label “non-technical”) to essentially explore the schema and see the relationships between data, much like they already do in Excel (or Business Intelligence tools).
It brings to mind what I wrote last night about the many levels of abstraction between one line in my Rails app and the hundreds of thousands of CPU operations that result.
There’s a huge productivity benefit to me not having to know much about CPU instructions, assembly language, C, virtual machines or abstract syntax trees just to query a database from a Rails app.
Similarly, I think there could be a huge productivity benefit if business people could examine existing queries to understand how their app works or point out logic flaws to the developers… or modify an existing query to extract some data without having to wait for a new API endpoint or report to be developed… or write a new query to demonstrate a product requirement or a feature they would like added.
Just like good developers work hard to understand the business they are supporting, I wonder if Graphiti and Vandal could be the lever that helps good business people better understand the app the developers are building.