Franciscello

Posted on

# Thanks for the advice, Crystal compiler!

In this post, the compiler is presented as a tool to help the programmer to design a good model. Although the example is very simple and its domain limited, it is easy to extrapolate the solution to equivalent situations in more complex problems.

So, let's start presenting the problem.

# The problem

Our objective is to model a parking lot where vehicles can park. Each vehicle will have an unique license plate (for example ABC123). The license plate will be the way to identify each one of them. It is important to note that we do not care about the type of vehicle (it can be a car 🚗, motorcycle 🛵, truck 🚚, etc. 🚀). Finally, at any time we can ask the parking lot to list the license plates of the parked vehicles, and also we could asked if it's empty or not.

For example, we will want to write something like this:

``````parking_lot = ParkingLot.new
parking_lot.empty? #=> true

parking_lot.empty? #=> false
``````

For this post we are only going to implement the model of the vehicle, i.e we are only going to focus on this part:

``````Vehicle.new "ABC123"
``````

We are leaving the parking lot implementation for a next post ... maybe?

Also, we are going to use the Crystal language (luckily or the title would not make any sense 🤔) with all the advantages that Crystal gives us for being a statically typed language, and also with some advantages of its own: its type inference system at compiling time.

There are some techniques that we could use to start implementing the model (for example TDD) but in this case let's put all our attention to what the compiler is going to tell us at each step.

# First solution attempt

``````# vehicle.cr
class Vehicle
end
``````

Note: we use the macro property to define the vehicle's license plate.

Given the above model, this is how we could use it:

``````# app.cr
require "vehicle.cr"

vehicle = Vehicle.new
``````

Let's compile it with:

``````\$ crystal app.cr
``````

aaaand 🥁

⚠️ Oops! the compiler (twist &) shouts ...

``````Error: can't infer the type of instance variable '@license_plate' of Vehicle

The type of a instance variable, if not declared explicitly with
`@license_plate : Type`, is inferred from assignments to it across
the whole program.
``````

Ok, so the compiler could not infer the type of `license_plate` by the use of it in the code ... 🤔 Maybe, we could be more explicit declaring `license_plate`?

# Second attempt: explicitly declaring the type

As the compiler could not infer the type of `license_plate`, we are going to explicitly declare the variable with its type:

``````# vehicle.cr
class Vehicle
end
``````

Let's compile it again ...

``````Error: instance variable '@license_plate' of Vehicle was not
initialized directly in all of the 'initialize' methods,
rendering it nilable.
Indirect initialization is not supported.
``````

Oh no! 😱

The compiler is telling us that we had not initialized `license_plate` in any `initialize` methods so there is a moment when `license_plate` has the value `nil`. But when declaring the variable, we have indicated that is a `String`. And this is a great feature of Crystal: nil not only has its purpose: is used to represent the absence of a value, but also has its own type: `Nil`. And `Nil` is not equal to `String`.

This is very easy to test, let's ask the compiler itself!

Please Crystal, tell us what's the type of the value `nil`?

``````\$ crystal eval "puts typeof(nil)"
Nil
``````

Please Crystal, tell us if `nil` is a `String`?

``````\$ crystal eval "puts nil.is_a?(String)"
false
``````

Perfect! The value `nil` has type `Nil` and is not a `String`!

# Third attempt: Union types ... or not?

So far, in our model, a vehicle (at some point) may not have a license plate, and in that case the variable `license_plate` would have the value `nil` (which, again, its type is `Nil`).

Having this in mind we could try to fix our model using Union types, like this:

``````class Vehicle
property license_plate : String | Nil
end
``````

But wait ... before trying this, we should listen to what the compiler was saying! Let's read it again:

Error: instance variable '@license_plate' of Vehicle was not initialized directly in all of the 'initialize' methods, rendering it nilable. Indirect initialization is not supported.

It's saying more than what we think.

Let's focus on the initialization of a vehicle. In our model, is it correct to have a vehicle without a license plate? Nop, the problem says:

Each vehicle will have an unique license plate (for example ABC123). The license plate will be the way to identify each one of them.

With our model we can create vehicles (objects) that do not have a license plate (i.e. they are not complete) But, on the other hand (for the model to be correct) the `vehicle` should have a valid `license plate` since the very beginning of its existence.

# Fourth attempt: fixing the model

Let's try the following approach:

``````class Vehicle

def initialize
end
end
``````

and we could use it like this:

``````# app.cr
require "vehicle.cr"

v1 = Vehicle.new

v2 = Vehicle.new
``````

Now the vehicles are complete (they all have license plate) ... but also we have two vehicles with the same `"not valid"` license plate 🤦‍♂️

ok, this is not good at all!

Our model generates objects that are not correct! Here the compiler is not saying nothing, although maybe it's laughing 🙈

# The fifth element attempt: fixing the fix

We are going to let the user of our model to set the license plate when creating a vehicle, like this:

``````class Vehicle

end
end
``````

Let's create vehicles!

``````# app.cr
require "vehicle.cr"

v1 = Vehicle.new("ABC123")

v2 = Vehicle.new("FOO321")
``````

Great! Now, with our model, we can create vehicles defining their license plate at initialization time! Meaning that now our model generates objects that are correct and complete from the beginning!

Note: Nothing prevents us from creating two cars with the same license plate, but this should be taken care by another object that knows all the vehicles being created (or maybe the license plates available). A vehicle itself does not (and should not) know all the other vehicles.

Next, we are going to polish our vehicle model a little bit more.

# Sixth attempt: even better!

Did you see what we did here? The number 6 is even ... no? ok, bad joke 🙃

Let's continue!

First, we don't want to set a license plate once the vehicle was created. We want to set it when creating a vehicle (like we are doing now) and we only want to get the license plate at any time:

``````class Vehicle

end
end
``````

Now, if we try this:

``````# app.cr
require "vehicle.cr"

v1 = Vehicle.new("ABC123")

v2 = Vehicle.new("FOO321")

v2.license_plate = "BAR000" # this should not work!
``````

The compiler will say:

``````Error: undefined method 'license_plate=' for Vehicle
``````

Great! Exactly what we want!

# Seventh and last attempt

Last (but not least) we are going to use a shorter syntax for initializing the instance variable:

``````class Vehicle

end
end
``````

And that's it! Here is our final model!

# Farewell and see you later. Summing up

• We started with a model.
• The compiler warned us (with an error!) about situations where a variable would have an invalid value (a nil/null value situation).
• We fixed the model so that objects were complete and correct from the very beginning.
• We rewrite our model using shorter syntax!

(and it only took us seven steps!)

Hope you enjoyed it! Until the next Crystal adventure!😃

👏 Thanks, thanks and thanks!! 👏
To @bcardiff and @diegoliberman for taking the time to review this post and improve the code and text!!