An introduction to Gloss pt. II: Our first app

johansenja
7 min readMar 28, 2021

--

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!

Crystal plus Ruby equalsĀ ?

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 šŸ„³

Six green specs all passing
Is there a better feeling than an all-green test suite?

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 šŸ˜ šŸš€

--

--