Four Key Reasons to Learn Markdown
Back-End Leveling UpWriting documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
In his RubyConf 2021 keynote, the creator of Ruby, Yukihiro Matsumoto, announced TypeProf-IDE, a Visual Studio Code integration for Ruby’s TypeProf tool to allow real-time type analysis and developer feedback. In another session, the creator of TypeProf-IDE, Yusuke Endoh, demoed the extension in more detail. This functionality is available for us to try today in Ruby 3.1.0 preview 1, which was released during RubyConf. So let’s give it a try!
First, install Ruby 3.1.0 preview 1. If you’re using rbenv
on macOS, you can install the preview by executing the following commands in order:
brew update
brew upgrade ruby-build
rbenv install 3.1.0-preview1
Next, create a project folder:
mkdir typeprof_sandbox
cd typeprof_sandbox
If you’re using rbenv
, you can configure the preview to be the version of Ruby used in that directory:
rbenv local 3.1.0-preview1
Next, initialize the Gemfile
:
bundle init
Next, let’s set up Visual Studio Code. Install the latest version, then add the TypeProf VS Code extension and the RBS Syntax Highlighting extension.
Open your typeprof_sandbox
folder in VS Code. Next, open the Gemfile
and add the typeprof
gem:
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } #gem "rails" +gem 'typeprof', '0.20.3'
Now install it:
bundle install
To see TypeProf in action, let’s create a class for keeping track of members of a meetup group. Create a file meetup.rb
and add the following:
class Meetup def initialize @members = [] end def add_member(member) @members.push(member) end def first_member @members.first end end
It’s possible you will already see TypeProf add type signatures to the file, but more likely you won’t see anything yet. If not, to find out what’s going on, click the “View” menu, then choose “Output”. From the dropdown at the right, choose “Ruby TypeProf”. You’ll see the output of the TypeProf extension, which will likely include a Ruby-related error. What I see is:
[vscode] Try to start TypeProf for IDE [vscode] stderr: --- ERROR REPORT TEMPLATE ------------------------------------------------------- [vscode] stderr: [vscode] stderr: ``` [vscode] stderr: Gem::GemNotFoundException: can't find gem bundler (= 2.2.31) with executable bundle [vscode] stderr: /Library/Ruby/Site/2.6.0/rubygems.rb:278:in `find_spec_for_exe'
In my case, the command is running in my macOS system Ruby (/Library/Ruby/Site/2.6.0
) instead of my rbenv
-specified version. I haven’t been able to figure out how to get it to use the rbenv
version. As a workaround, I switched to the system Ruby and updated Bundler:
rbenv local system
sudo gem update bundler
rbenv local 3.1.0-preview1
For more help getting the extension running, check the TypeProf-IDE Troubleshooting docs. Of note is the different places that the extension tries to invoke typeprof
from. Ensure that your default shell is loading Ruby 3.1.0-preview1
and that a typeprof
binary is available wherever the extension is looking.
After addressing whatever error you see in the output, quit and reopen VS Code to get the extension to reload. When it succeeds, you should see output like the following:
[vscode] Try to start TypeProf for IDE [vscode] typeprof version: typeprof 0.20.2 [vscode] Starting Ruby TypeProf (typeprof 0.20.2)... [vscode] Ruby TypeProf is running [Info - 9:03:49 AM] TypeProf for IDE is started successfully
You should also see some type information added above the methods of the class:
Well, that’s not a lot of information. We see that #add_member
takes in an argument named member
, but its type is listed as untyped
(which means, the type information is unknown). It returns an Array[untyped]
, meaning an array containing elements whose type is unknown. Then #first_member
says it returns nil
, which is incorrect.
For our first change, let’s look at the return value of #add_member
. It’s returning an Array
, but I didn’t intend to return a value; this is just a result of Ruby automatically returning the value of the last expression in a method. Let’s update our code to remove this unintentional behavior. Add a nil
as the last expression of the method:
def add_member(member) @members.push(member) + nil end
Now the return type is updated to be NilClass
, which is better:
Next, how can we fix the untyped
? Endoh recommends a pattern of adding some example code to the file showing the use of the class. Add the following at the bottom of meetup.rb
:
if $PROGRAM_NAME == __FILE__ meetup = Meetup.new end
Next, type meetup.ad
below the line where meetup
is assigned. (We’ll explain the $PROGRAM_NAME
line in a bit.) An autocomplete dropdown will appear, with add_member
selected:
Because TypeProf can see that meetup
is an instance of class Meetup
, it can provide autocomplete suggestions for methods.
Click add_member
in the list, then type an opening parenthesis (
. VS Code will add the closing parenthesis )
after the cursor, and another popup will appear with information about the method’s arguments:
It indicates that the method takes one argument, member
, and returns nil
. Also note that the type of member
is still listed as untyped
; we’re still working toward fixing that.
Pass a string containing your name as the argument, then add the rest of the code below:
if $PROGRAM_NAME == __FILE__ meetup = Meetup.new meetup.add_member('Josh') first_member = meetup.first_member puts first_member end
What’s the significance of the if $PROGRAM_NAME == __FILE__
conditional? $PROGRAM_NAME
is the name of the currently running program, and __FILE__
is the name of the current source file. If they are equal, that means that this Ruby file is being executed directly, which includes when TypeProf runs the file. So this is a way to provide supplemental information to TypeProf.
When you added this code, the type information should have updated to:
Why does this added code affect the type of information? TypeProf executes the code to see the types that are actually used by the program. By supplying an example of using the class, TypeProf has more type information to work with. Future TypeProf development may allow it to be more intelligent about inferring type information from RSpec tests and usages elsewhere in the project.
Note that TypeProf now indicates that the member
argument is a String
, and that #first_member
may return either a NilClass
or a String
. (The reason it might return a NilClass
is if the array is empty.)
Let’s put our object-oriented design hats on and think about these types. Is it specific to String
s? No, the code doesn’t make any assumptions about what the members are. But TypeProf has coupled our class to one specific other class to use!
To prevent this, we can manually edit the RBS type signatures generated for our class to indicate just how generic we want Meetup
to be.
Create an empty typeprof.rbs
file in your project folder. Next, command-click on the type signature above #add_member
. The typeprof.rbs
file will open, and the RBS type signature for that method will be automatically added to it:
Next, go back to meetup.rb
and right-click the type signature above #first_member
. This adds the signature for that method to the RBS file too, but as a separate class
declaration:
To keep things simpler, edit the RBS file so there’s a single class
with two methods in the same order as in the Ruby file, and save the file:
Now, let’s edit the signature to use type variables. A type variable is a place where, instead of referencing a specific type, you use a variable that can represent any type. Everywhere the same type variable appears, the type must be the same.
First, add a [MemberT]
after the Meetup
class name:
Next, replace the two occurrences of String
with MemberT
:
What this means is, a given Meetup
instance applies to a certain type, called MemberT
. That’s the type of the member
you pass in to #add_member
. That is the same type as what the return value of #first_member
should be. So if you pass in a String
you should get a String
back. If you pass in a Hash
, you should get a Hash
.
Switch back to meetup.rb
. If you don’t see the type signatures updated, you may need to close and reopen meetup.rb
. Then, you should see updated type signatures:
Note that our MemberT
types appear in the signatures of #add_member
and #first_member
. Also note that the signatures have a #
in front of them: this indicates that they’re manually specified in the RBS file.
Now, let’s see what help this gives us. In the statement puts first_member
, start typing .up
after it. Note that an autocomplete dropdown appears and #upcase
is selected:
TypeProf knows that member
is a Meetup
object. Because you passed a String
into the #add_member
method of the meetup
object, TypeProf can tell that meetup
’s type variable MemberT
is equal to the type String
. As a result, it can see that its #first_member
method will also return a String
. So it knows first_member
is a string, and therefore it can suggest String
’s methods for the autocomplete.
Click upcase
to autocomplete it. Now note that first_member.upcase
has a red squiggly underlining it. Hover over it to see the error:
It says [error] undefined method: nil#upcase
. But wait, isn’t first_member
a String
? The answer is maybe. But it could also be a nil
if the meetup hasn’t had any members added to it. And if it is nil
, this call to #upcase
will throw a NoMethodError
. Now, in this trivial program we know there will be a member present. But for a larger program, TypeProf will have alerted us to an unhandled edge case!
To fix this, we need to change the way the type signature is written slightly. In the RBS file, replace (NilClass | MemberT)
with MemberT?
(don’t miss the question mark):
?
indicates an optional type, a case where a value could be a certain type or it could be nil
.
Now, in the Ruby file, wrap the puts
call in a conditional:
first_member = meetup.first_member -puts first_member.upcase +if first_member + puts first_member.upcase +else + puts 'first_member is nil' +end
If the red squiggly under the call to #upcase
doesn’t disappear, close and reopen meetup.rb
to get TypeProf to rerun. After that, if you made the changes correctly, the underline should disappear:
TypeProf has guided us to write more robust code! Note that currently TypeProf requires the check to be written as if variable
; other idioms like unless variable.nil?
and if variable.present?
will not yet work.
If you’d like to learn more about TypeProf-IDE, Endoh’s RubyConf 2021 talk should be uploaded to YouTube within a few months. In the meantime, check out the TypeProf-IDE documentation and the RBS syntax docs. And you can help with the continued development of TypeProf-IDE by opening a GitHub Issue on the typeprof
repo.
Thank you to Yusuke Endoh for his hard work building the TypeProf-IDE integration, for his presentation, and for helping me work through issues using it during RubyConf!
If you’d like to work at a place that explores the cutting edge of Ruby and other languages, join us at Big Nerd Ranch!
Writing documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
Humanity has come a long way in its technological journey. We have reached the cusp of an age in which the concepts we have...
Go 1.18 has finally landed, and with it comes its own flavor of generics. In a previous post, we went over the accepted proposal and dove...