There was a point in time where most computers were primarily used as BASIC REPLs. BASIC is a language (or, more accurately, a family of languages) that are designed to be simple.
As an example, a few years ago I wrote a dialect of BASIC which I call xBASIC. As far as BASICs go, it's honestly not super BASIC-y. This is what a simple xBASIC program might look like:
for x = 10 to 20 print x next x
This program counts from 10 to 20, with each number on a new line.
But I'm not that interested in talking about the BASIC languages. I'm more interested in talking about the (typical) BASIC REPL.
REPLs are what allow you to type code and have it evaluated in real-time. For example, with Ruby, you can type irb
and it drops you into a Ruby REPL, where you can type Ruby code and have it run immediately.
Modern REPLs aren't terrible, but really they're optimized for single-line, quick debugging. In contrast, the BASIC REPL was usually also your text editor, so it had to work for writing multi-line, full programs. For this, BASIC REPLs used the concept of line numbers. The program up above might be written now like this:
10 for x = 10 to 20 20 print x 30 next x
When the BASIC REPL sees a line number, it doesn't execute it immediately. Instead it saves it to the current program. This makes it easy to update lines, or add lines in between. For example, maybe you want to print "hi" before each number. All you need to do is type 15 print "hi"
and your program has been updated. Maybe you don't want to print the number at all - type 20
and it'll remove that line from the program.
Contrast that to writing a multi-line program in something like IRB. If you want to create, update, or delete a line, you need to rewrite the entire program in your REPL, changing what you need. It's super cumbersome if what you're working on is more than 2 or 3 lines.
So, over the last few days, I wrote Rubic. Rubic is a BASIC-inspired Ruby REPL. Here's what a session in Rubic might look like:
> 10 puts "what is your name?" > 20 name = gets.chomp > 30 puts "nice to meet you, #{name}!" > > list 10 puts "what is your name?" 20 name = gets.chomp 30 puts "nice to meet you, #{name}!" > run what is your name? stephen nice to meet you, stephen! => nil > # but wouldn't it be nice if we printed the last line a few times? => nil > 25 for _ in 0..5 > 35 end > list 10 puts "what is your name?" 20 name = gets.chomp 25 for _ in 0..5 30 puts "nice to meet you, #{name}!" 35 end > run what is your name? stephen nice to meet you, stephen! nice to meet you, stephen! nice to meet you, stephen! nice to meet you, stephen! nice to meet you, stephen! nice to meet you, stephen! => 0..5 > # but now our line numbers aren't equidistant anymore! let's clean them up => nil > squish > list 1 puts "what is your name?" 2 name = gets.chomp 3 for _ in 0..5 4 puts "nice to meet you, #{name}!" 5 end > # or alternatively... => nil > widen > list 10 puts "what is your name?" 20 name = gets.chomp 30 for _ in 0..5 40 puts "nice to meet you, #{name}!" 50 end > # alright, let's save our program => nil > save greeter.rub > # or if you just want a normal ruby file => nil > export greeter.rb > # with load/import you can also do the opposite (load an existing rub or rb file) => nil
This, in my opinion, is a much nicer way to write scripts in the REPL. Aside from normal BASIC operations, I've also added squish
and widen
commands. squish
will change the line numbers so they go 1, 2, 3, etc., and widen
will change the line numbers so that they go 10, 20, 30, etc. I think widen
especially is quite useful, as it's easy to run out of line numbers when making changes otherwise! I'm not sure if any BASIC REPLs implement an equivalent of these commands. I didn't check, so it's probably not a new idea, but I don't think it's a super common one either.
Also worth pointing out is save
/ load
and import
/ export
. The former works with Rubic files (which are Ruby files with the line numbers) and the latter works with normal Ruby files. This allows you to write a full Ruby program in Rubic, and then save it to a file. It also allows you to take an existing Ruby file and iterate on it in Rubic.
Rubic also supports a bunch of creature comforts that you would expect (primarily through use of IRB under the hood). You can write code without line numbers to have it run immediately, and it supports syntax highlighting and autocomplete as well.
One thing I'm quite proud of is that Rubic's syntax highlighting is context-aware! For example, imagine this is what your current program looks like:
10 for i in 0..10
If you try to do 5 end
, that end
doesn't match up with anything, so it turns red. But if you do 20 end
, the end
matches the for
and so it turns green!
Motivation
I should probably talk a bit about why I built this. I find when writing scripts, I'd like a slightly faster development loop than "edit file" -> "run script" -> "repeat". I think being able to just change lines and type "run" in the same context should be a little less annoying.
Another thing specific to Ruby is that large-ish projects have fairly awful startup times. A mid-sized Rails project might take 30 seconds just to start the environment. Having to do that every time you rerun a script can be a bit dreadful. Rails does include a reload!
method which helps, but it still takes a few seconds, and it's another thing to have to type every iteration. I'm hoping that Rubic delivers real value for me, using it to iterate on scripts, but I'm also hoping it serves as a PoC for writing more modern REPLs that use the same concepts.
Disclaimer
I should probably mention that Rubic is not exactly stellar in terms of code quality. The way it works under-the-hood is pretty inefficient (though it shouldn't matter unless you have many thousands of lines of code). It's also a bit hacky (for example, using a private IRB method in once place). It does its job well, but improvements certainly wouldn't hurt.