Conventionally-Typed Ruby
The Ruby programming language is made for people. Great effort is put behind keeping the language easy to understand for the human reader. This emphasis is not self-evident. It’s far easier to create programming languages which are easy to read by machines. But this always comes with the trade-off that some overhead has to be delegated to the programmer — not so with Ruby: all overhead must go to the machine. The human should have a tidy table to solve domain problems, not machine problems.
Type annotations for humans
The static-typed vs dynamically-typed discussion is hot. Some people like the additional security they get by a compiler/type-checker. Other people don’t like to read and write type annotations in their source code. Most who are pro static-typing think the overhead is worth and needed. On the other hand pro dynamic-types people say they want to focus on the problem domain without typing-overhead.
I’ve even met people who like to read types. I guess it’s the same kind, that sees beauty in assembler language for its machine-proximity. It’s pure control. I can understand that, but it’s not the kind of beauty I need to be productive in domain-proximity.
Anyways, even pro dynamic-typing people agree that type hints by your IDE are nice. So in the end, I think we mostly agree that type annotations are mainly a tool to enact control over the machine instead of over the problem domain.
Static typing for Ruby
The discussion about type annotations in Ruby is quite old. While Matz (the original creator of Ruby) 10 years ago only rejected syntax proposals (e.g. #5583), he later rejects any type annotations completely (e.g. #9999). With Stripe’s announcement of Sorbet in 2018 the idea of type annotations creep back into our conscience and Matz cannot ignore the ever growing efforts put into the static type checker. With Ruby 3 he agrees to at least standardize a type specification language called RBS. The important part here is that this happens outside of the Ruby language. It enables people to implement their own type checking software. Currently there are Steep, Sorbet and RDL. But all need type annotations in Ruby code. So they do exactly what Matz wants to prevent: “spoil” Ruby with machine-related instructions. I’ve got a naïve idea for a solution right here.
The idea of conventional typing
Why would we add more machine-related information to our beautiful Ruby source code when it’s already so nice and understandable to us humans? We indeed should not. Let me introduce to you a new way of type annotations which we didn’t see yet in the world: the conventional type.
Let’s use what we already have: identifier names. A variable or method called current_user
already carries type information for humans: it’s probably a User
class. longest_distance
and EARTH_RADIUS_KM
are hopefully somehow numeric (e.g. Integer | Float
) and companies
are probably Enumerable
.
This type information is pretty clear to us humans, but let’s make this available to the machines. With some yet-to-be-invented interpretation- and consistency-checking logic we could now automatically generate RBS files from the identifier names we already have in our Ruby source code. Commonly understood variable and method names could lead to reliable type generation. We only yet need to come up with rbs fancy-prototype
.
This may sound very challenging and magic. But isn’t it exactly Ruby’s job to take all the hardship into its interpreter and tools to make the programmer’s life easier? Conventionally-Typed Ruby would exactly tie in with this philosophy because it makes the right guess about what type you mean without spoiling your code.
Update: Conventions and consistency
Static typing in Ruby is in the end a question about consistency. We fear that we don’t write consistent programs and therefore we have linters like Rubocop and now also type checkers. But why would we need to add type annotations into our Ruby code to check for consistency? Wouldn’t it be enough to look at the method calls to determine the most probable type? This should even be possible during runtime: we would count calls and raise on inconsistencies.
So current_user.to_i
would be worth a flag if we do it once, but not if we do it often.
And one thing to think about: could Rubocop be an offline type checker? Isn’t linting and type-checking in the end the same (one offline, the other online)?
Oh, and while we’re at it: why not start talking about contracts?
Update 2: Ask Ruby anything
We actually can ask Ruby for a lot during runtime, for example
user = User.first
=> …
binding.local_variables
=> [:user]
user.class
=> RegisteredUser(…)
Update 3: Rails Examples
Today I encountered examples of measures we take in Rails. We exactly try to convey type information when naming our variables properly.