DEV Community

Franciscello
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.licenses_plates #=> []

parking_lot.receive(Vehicle.new "ABC123")
parking_lot.empty? #=> false
parking_lot.licenses_plates #=> ["ABC123"]

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

We may start with something like this:

# vehicle.cr
class Vehicle
  property license_plate
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
vehicle.license_plate = "ABC123"

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
  property license_plate : String
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
  property license_plate : String

  def initialize
    @license_plate = "not valid"
  end
end

and we could use it like this:

# app.cr
require "vehicle.cr"

v1 = Vehicle.new
puts v1.license_plate #=> "not valid"

v2 = Vehicle.new
puts v2.license_plate #=> "not valid"

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
  property license_plate : String

  def initialize(license_plate)
    @license_plate = license_plate
  end
end

Let's create vehicles!

# app.cr
require "vehicle.cr"

v1 = Vehicle.new("ABC123")
puts v1.license_plate #=> "ABC123"

v2 = Vehicle.new("FOO321")
puts v2.license_plate #=> "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
  getter license_plate : String

  def initialize(license_plate)
    @license_plate = license_plate
  end
end

Now, if we try this:

# app.cr
require "vehicle.cr"

v1 = Vehicle.new("ABC123")
puts v1.license_plate #=> "ABC123"

v2 = Vehicle.new("FOO321")
puts v2.license_plate #=> "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
  getter license_plate : String

  def initialize(@license_plate)
  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!!

About the title
The original title for this post was going to be When the compiler leads you to good design but using the words good and design in the same sentence could lead to and endless discussion 😅

Top comments (0)