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)