Reputation: 481
You'll have to forgive me as I haven't worked with a statically typed language in 15 years and I'm sure the answer to this is incredibly simple but I've been banging my head against it for days with no luck.
In crystal, instance and class variables don't get the magic inferred type features as much as local variables. I understand why. But it's killing me. I first ran into this trying to take something the yaml parser parsed and save it to a class variable so that the rest of my program could access this information (configuration) from anywhere. First I didn't define a type at all, and I got the typical "can't infer the type" from the compiler. So I added the type. But then you have to immediately assign it to something of that type because it can't be nil. If my type is YAML::Any
then I can't just do @@class_var = YAML::Any.new
because YAML::Any.new
expects a bunch of parameters. So instead I assume I'm supposed to make it YAML::Any?
so that it can also be nil
, but then everywhere I try to actually do anything with the variable says "no, you can't do that, because it could be nil." I've also tried literally every "this is how you work with class/instance variables in Crystal" thing I can find: if var.try
, if var.nil?
, if var.is_a
, if var.responds_to?
and every single one of them gives me the exact same "nope, this could be nil."
Example (contrived) example:
class ThingOne
property thing2
def initialize
puts "ThingOne #{self}"
end
end
class ThingTwo
property thing1
def initialize
puts "ThingTwo #{self}"
end
def something
puts "something!"
end
end
one = ThingOne.new
two = ThingTwo.new
one.thing2 = two
two.thing1 = one
one.thing2.something
This won't compile with the typical "can't figure out the type." So I change the property
lines to add : ThingOne
and : ThingTwo
but then they have to be initialized because they can't be nil. In this case I could do that, but in some cases (like the one I want to solve) I can't do that, so I'm going to pretend I have to do : ThingOne?
and : ThingTwo?
. Now the type error goes away and I get to the "can't do this because it could be Nil" issue. Wrapping the function call in try
or is_a
or responds_to?
or any of those others gives me the same issue.
What is the magic incantation I am missing?
Edit: I was using the try
block wrong, it should be:
one.thing2.try { |t2| t2.something }
Which does work. Is this the accepted practice? I still feel like I'm missing something.
Upvotes: 1
Views: 514
Reputation: 5661
There are several ways to approach nilable properties in Crystal.
A good solution is obviously to avoid nilable types when possible, for example require a value be provided to the initializer. In your example, this can't work for both of your values because they are interdependent. But it could be an option for the second one.
When you have a nilable type in an instance variable, you can handle it in different ways:
try
to be working. That's a valid approach. Note you can use short block syntax: one.thing2.try &.something
.is_a?
and responds_to
, you need to be aware that they only work as type restriction on local variables, not on instance/class variables or calls (such as getters of those variables). See documentation on if var.is_a?
and if var.responds_to?
(more generic: if var
)..not_nil!
on the value if you know it can't be Nil
. That resolves the nilable type. It's considered a code smell and should only be used when no other solution is possible to convince the compiler that there must be an actual value at this point.property!
macro. It's similar to the property
macro you're already using in the example, except that the getter implicitly removes the Nil
type. This is recommended for instance variables that can be Nil
(for example because they may initially be ommited and assigned after the constructor) but expected to be not nil
when used.Upvotes: 2