An introduction to Gloss pt. II: Our first app
Welcome to the second part of this introductory series on Gloss. If you havenāt already, I recommend checking out part I, where I give more of an overview of what Gloss is, and a bit of my motivation behind it.
To cut a relatively short story even shorter, Gloss is just like Ruby, but with a bit more!
The task at hand
If you came here hoping for a To-do list ā I am sorry to disappoint. Though hopefully after this you will have the know-how to use Gloss to create one, if you were so inclined. Instead, I am going to be creating a simple Roman numeral parser, in the form of a CLIā here are some basic specs I made for myself:
- I can type in some Roman numerals, and get a decimal response back
- I can do this as many times as I like
And some test cases:
It parses
- CLI as 151
- III as 3
- MCMXIV as 1914
- LIP as error
- IIIII as 5 (Poor style but OK)
- IIX as 8 (Poor style but OK)
Getting set up
Firstly, letās make a new directory (I am calling mine onefiveone) and cd
into it. Weāll need a Gemfile
, so we can list our dependencies (in this case just a single one š)
# Gemfile
source "https://rubygems.org/"
gem "gloss"
Now we can run bundle install
to install Gloss, followed by gloss init
to set up this directory as a gloss project. Youāll notice that the latter creates a .gloss.yml
file in the directory, which lists the projectās preferences. Here, I have added a bit of explanation on each one:
# string literals mentioned in each file will be frozen by default (same as prepending "# frozen_string_literal: true" in ruby)
frozen_string_literals: yes# the directory containing your project's Gloss files. By default, this is "src", to allow for distinction and separation between Gloss and Ruby files, if desired. But this could just be ".", for current directory
src_dir: src# the filepath of the gloss file which you call to run you application
entrypoint: src/app.gl# should all "require" and "require_relative" paths must have type definitions associated with them? If working with external libraries or native extensions, and type definitions are not available, then this adds convenience
strict_require: yes# Options: lenient (recommended, for now), default, strict
type_checking_strictness: default
Letās set these to something we are happy with, and hopefully that should give us a bit of structure to work with:
frozen_string_literals: yes
src_dir: src
entrypoint: src/app.gl
strict_require: yes
type_checking_strictness: lenient
So we have said we will be working from the src
directory, with app.gl
as our main entrypoint, so we can go ahead and create those.
Writing our code
This also gives us a chance to think about the components weāll need, so we can break things down a bit more. Firstly, weāll need a means of interacting with the user, via some sort of interface. Weāll also want a piece which handles to logic of actually calculating the result, and then also some means of repeating this an arbitrary number of times, so that the user can keep inputting numerals. On top of that, we will probably need some sort of āglueā to bind all of our components together.
Why not start with our interface for interacting with the user, because this can be a fairly simple wrapper around a few core methods:
# src/interface.gl
class BasicInterface
def input(prompt : String) : String
output prompt
input = gets
input ||= ""
input.chomp
end def output(msg : String) : String
puts msg
msg
end
end
As you can probably tell, this is pretty close to Ruby! The main differences here being that we are specifying types for the parameters and return types of the input
and output
methods. Of course, when the behaviour of the two methods is as simple as this, it is hardly much better than just calling gets
and puts
on there own ā but where would the fun be in that?
Next, we could probably start thinking about the actual logic we want to use to evaluate our numerals. Weāll want a distinct component for this, which accepts a numeral in the form of a string, and can output the value as an integer. Weāll also need a way of storing the values of each character. Letās look at some code, and then discuss whatās going on:
# src/ofo.gl
require_relative "errors"class OneFiveOne
VALUES = {
"I" => 1,
"V" => 5,
"X" => 10,
"L" => 50,
"C" => 100,
"D" => 500,
"M" => 1000
} def initialize(@numeral : String); end def generate : Integer
total = 0
atomic_subgroup : Array[Integer] = Array.new
(0...@numeral.length).each do |index|
current_char = char_by index
current_char_val = value_by current_char
if atomic_subgroup.length.zero?
atomic_subgroup << current_char_val
else
last_val = atomic_subgroup[-1]
if last_val > current_char_val
subgroup_total = calculate_subgroup_total atomic_subgroup
total += subgroup_total
atomic_subgroup = [ current_char_val ]
else
atomic_subgroup << current_char_val
end
end
end
total + calculate_subgroup_total(atomic_subgroup)
end private def calculate_subgroup_total(subgroup : Array[Integer])
case subgroup.length
when 0
0
when 1
subgroup.first
else
if subgroup.all? { |number| number == subgroup.first }
subgroup.sum
else
subgroup.reverse.inject :-
end
end
end private def char_by(index : Integer)
char = @numeral[index]
char ? char.upcase : nil
end private def value_by(char : String | NilClass) : Integer
VALUES.fetch(char) { raise UnexpectedCharacter, char }
end
end
The two main parts worth discussing here are generate
and calculate_subgroup_total
; the way I decided I wanted the logic to work was to look through each of the numeralās characters, and if the next value is lower than the current numeralās value, then the next value is added to the current numeral, to give a total. If, on the other hand, the next numeral was higher, the logic would act in reverse, and the current numeral would have to be subtracted from the next numeral. I kept track of cases like this by having an array called an atomic subgroup, so that successive numerals could be accounted for.
Consider the example of XI
vs IX
; in the first case, I
is smaller than X
, so it gets added to X
to give 11. In the second case, X
is bigger than I
, so I
gets subtracted from X
to give 9. In the context of the code above, I would be keeping track of a āsubgroupā of [ "I", "X" ]
, for which the usual logic of successive addition would be reversed. So generate
, therefore, contains the master loop and tracks the total of the characters as it goes, and calculate_subgroup_total
handles the logic of any āreverseā sequences.
Is this a perfect algorithm? Not exactly! There are some ways it could trip up ā plus it allows for potentially ugly combinations such as IIIIIX
ā but it satisfies my fairly lenient specs, and itās good enough for the purposes of this exercise!
You may also notice the require_relative "errors"
at the top of the file; so letās add that file, so we can define some custom error classes which we can raise
and handle:
# src/errors.gl
class UnexpectedCharacter < StandardError
def initialize(character)
super "Unexpected character '#{character}'. Accepted values: #{OneFiveOne::VALUES.keys.join(', ')}"
end
end
Nothing too fancy here, but this just means that we can have a set format of error message every time we want to use it, and it also means we can easliy categorise errors in our error handling.
This brings us neatly onto the final part of our app: the main glue, in our entrypoint src/app.gl
. The main requirements here are that we have a means of allowing the program to repeat itself, so that the user can use it many times over; in addition, we can also specify some behaviour in the case of hitting an unexpected character, or when the user interrupts execution with ctrl-c
.
# src/app.glrequire_relative "interface"
require_relative "ofo"interface = BasicInterface.newloop do
input = interface.input "Enter a Roman numeral:"
output = OneFiveOne.new(input).generate
interface.output "Answer: #{output}\n\n"
rescue UnexpectedCharacter => e
interface.output e.message
retry
rescue Interrupt
interface.output "Thanks for using CLI!"
exit 0
end
Thatās it for the code! Now that everything is written, we can run gloss build
to compile these files and generate our ruby output. We can then run it with ruby app.rb
to see the fruits of our labour in action š¤©
$ ruby app.rb
Enter a Roman numeral:
CLI
Answer: 151
I also translated my specs and test cases into specs for rspec
, to give reliable confirmation that things were working as they should; and it is highly satisfying to see it when they do š„³
Thatās everything here for now ā thanks for bearing with me if you have made it this far! Be sure to check out the final code on GitHub below, and stay tuned for more updates, where we will use Gloss for writing and deploying our first web app š š