rakaur
rakaur

Reputation: 481

instance and class variables in crystal seem impossible to use

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

Answers (1)

Johannes Müller
Johannes Müller

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:

  • You already found try to be working. That's a valid approach. Note you can use short block syntax: one.thing2.try &.something.
  • With 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).
  • Alternatively, you could just call .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.
  • Crystal's standard library has a useful tool for nilable instance variable: The 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

Related Questions