Some thoughts on Ruby
Feb 2, 2017I’m a well-known Ruby fanboy among my circle of friends. Even people who aren’t into programming will occasionally make a comment about it, which I think is an indication that I talk about how great the language is too often.
In truth, it’s not surprising that people might know this about me. I really, really like Ruby. I don’t think it’s perfect for all tasks—hell, I think it’s downright terrible in some contexts—but I do think it is a fantastic language, and it’s probably my favorite to write. This blog post is going to serve as a bit of a high-level introduction to Ruby as a language, as well as a justification for my fanboy-ism.
Dynamically-typed for a reason
Ruby is a dynamically-typed language. This means that you lose any semblance of type safety—if you have a method that expects a string, but you pass it a number, you’re going to get an exception at runtime. This is a huge penalty in terms of ease-of-use in the long-run, and, in many languages, you don’t really get anything in return except the fact that you don’t have to type typenames everywhere. With modern type systems that can infer almost everything, even this benefit is basically non-existent, making dynamic typing a negative quality for many languages.
In Ruby, however, dynamic typing is merely a tradeoff for metaprogramming capability.
Ruby uses metaprogramming in a huge number of contexts, and the result is greatly increased expressiveness.
Let’s think, for example, of the OpenStruct
class.
This class basically lets one access a hash with foo.bar
syntax instead of foo[:bar]
syntax, which is useful in a variety of contexts.
How can we define such a class? Simple:
class OpenStruct
def initialize(hash)
@hash = hash
end
def method_missing(meth, *args)
return super unless args.empty?
@hash[meth]
end
end
What if we want to be able to set properties, as well?
class OpenStruct
def initialize(hash)
@hash = hash
end
def method_missing(meth, *args)
if meth.to_s[-1] == "="
return super unless args.length == 1
@hash[meth.to_s[0..-2].to_sym] = args.first
else
return super unless args.empty?
@hash[meth]
end
end
end
This is easy to do in Ruby, but quite difficult in other languages.
Some refer to metaprogramming with the term “magic”. I would contend that metaprogramming is only magic if you don’t understand what’s going on. I would further argue that most commonly-used metaprogramming in Ruby is fairly easy to understand.
Take, for example, ActiveRecord. It’s an ORM which automatically constructs objects from database tables. So, if you have a file which says:
class User < ActiveRecord::Base
end
And a table with columns name, id, last_login
, you can already do:
user = User.find(10) # user with id 10
puts user.name # => Value in name column
puts user.last_login # => Value in last_login column
user.update(last_login: DateTime.now) # => Automatically updates table
This can seem a big magical on the surface, but it’s actually quite easy to understand. What’s happening, conceptually, is pretty simple:
- On startup, ActiveRecord obtains the schema from the database
- It maps tables to classes with a simple scheme (“users” => “User”, “blog_posts” => “BlogPost”)
- For each table, it:
- Takes each column and defines a method to access it, using the
define_method
method. - Takes each column and defines a method to set it, using the same method
- Takes each column and defines a method to access it, using the
This isn’t really magic, it’s just an algorithm for handling tables.
The fact that the algorithm is adding and removing methods doesn’t really matter–it’s still something hta’ts relatively easy to follow.
Of course, ActiveRecord also gives you other nice goodies, like type coerceon.
It also gives some not-so-nice goodies, like the surpress
method, but I guess we can all just blame DHH for that kind of stuff.
Blocks!
Ruby also scores major points for how its blocks work. In Ruby, you can send a block of code along to a method pretty easily:
foo.bar do |x| # x is an argument to this block
puts x
end
This construct seems to be simple, but it’s actually quite powerful.
As closures
Blocks can be used as vanilla closures, enabling the use of higher-order functions.
The Enumerable
API, for example, uses closures to perform a variety of transformsations:
# obtain an array of the odd numbers / 2, from 1 to 100
(1..100).to_a.keep_if{|x| x % 2 == 1}.map(&:to_f).map{|x| x / 2.0}
These transformations are slightly more composable than list comprehensions, because you can also hold blocks in variables, and convert objects to blocks.
I used this on a recent project, FifthedSim.
One of the classes in FifthedSim is Distribution
, which is essentially a probabilty distribution.
It maps an input number to a probability.
Distribution
can be turned into a proc
object, a type of block in Ruby.
This allowed me to implement the percent_lower
method, which gives the probability of a number being lower than a given value, as follows:
def precent_lower
@min.upto(num -1).map(&self).inject(:+)
end
For DSLs
Blocks have another awesome property, though.
With the special method instance_eval
, they can be used to implement DSLs.
intsance_eval
takes a proc, and executes it inside the instance of a given class.
This is best demonstrated with an example:
class Foo
def initialize(&block)
instance_eval(&block)
end
attr_accessor :bar, :baz, :hack, :fraud
end
f = Foo.new do
self.bar = 10
self.baz = 10
self.hack = "Mike"
self.fraud = "Jay"
end
puts f.inspect
Simply put, instance_eval changes the definition of “self”. This allows for the creation of expressive DSLs, such as RSpec, an example of which you can find here. It almost reads like English. Just as cool is Sequel, which let you query tables like this:
items.where{price * 2 < 50}.sql
#=> "SELECT * FROM items WHERE ((price * 2) < 50)
instance_eval
has friends, like class_eval
, which executes in the context of a class.
This allows us to do things like:
Person = Struct.new(:name, :job, :age)
def is_fraud?
job == "fraud"
end
end
To define methods
As it turns out, you can also use a block of code to define a method. Let’s say you wanted to implement a class which represented a roll of the dice. You may wish to know whether the value was above average, below average, or exactly averge. You could define three methods, but that’s a lot of boilerplate. Let’s use metaprogramming instead:
class DiceExpression
{"" => :==, "above_" => :>, "below_" => :<}.each do |k, v|
self.send(:define_method, "#{k}average") do
value.send(v, selfaverage)
end
end
end
We create three blocks here, each of which is used to define a method. Nowe we can use:
roll.below_average? # => false
roll.average? # => true
roll.above_average? # => false
Easy!
Package Management
Ruby’s package management system is called rubygems. It’s incredibly easy to use, both as a gem publisher or user, and indexes thousands of useful gems.
For an individual application, you can manage dependencies with Bundler. Bundler smartly resolves dependencies to avoid ridiculously deep trees, and keeps your gem versions in check in a smart way. The DSL to use bundler is also nice and easy to use. There’s a reason that other package managers, like Cargo, based their layout on Bundler—it’s simply awesome.
Happiness
Ruby is a language designed to maximize programmer happiness. For this reason, it has a lot of really nice, small features.
Method Naming
For example, ?
is a valid character to end a method name.
This allows us to write code that reads nicely:
array.empty?
string.blank?
No Parenthesis
Ruby also doesn’t necessitate the ()
on methods in most contexts, which also helps with readability:
if hash.has_key? :job
hash[:job]
else
raise ArgumentError, "has no job!"
end
Expressions everywhere
Most things are expressions in ruby. Thus, we can do this:
job = if name == "mike"
:hack
elsif name == "jay"
:fraud
else
nil
end
Hell, even defining a class is an expression, so this is totally valid:
class Foo
puts def bar
"bar"
end # => prints ':bar'
end
End-of-line Conditionals
Ruby also allows end-of-line conditionals, which allow yet more expressiveness:
raise ArgumentError, "Not a number" unless arg.is_a? Fixnum
Perl-ish String Processing
Ruby has some great String Processing.
Regexes are a built-in type in Ruby, and they use an engine similar to that of Perl. Thus, they’re technically not regular expressions, as they allow lookahead and lookbehind. They also allow stuff like this:
"I am 10, 13, or maybe 14".gsub(/\d+/){|m| m.to_i + 1}
# => "I am 11, 14, or maybe 15"
Strings also come with ljust
and rjust
built-in—no need to import a left-pad module.
Ruby handles string encodings in a sensible manner. This was added in version 1.9, and, strangely, people actually upgraded, unlike certain other languages.