Crystal compiler does not detect that object is not nil - crystal-lang

I have the following class :
class X
property son, val
def initialize(#val : Int32)
#son = nil.as X?
end
def add(other : X?)
unless other.nil?
if #son.nil?
#son = other
else
#son.add(other)
end
end
end
end
x = X.new 5
x.add(nil)
x.add(X.new 3)
But when I try to build I get
Showing last frame. Use --error-trace for full trace.
In nil-test.cr:12:22
12 | #son.add(other)
^------
Error: undefined method 'include' for Nil (compile-time type is (X | Nil))
According to the manual, this is exactly the kind of situation where the compiler should recognize that #son cannot be nil in the else branch, yet it apparently fails to do so.
What am I doing wrong ?
Note : using #son.not_nil!.add(other) works, I'm just asking why the compiler can't do without.

That only works for local variables, not instance variables - since the instance variables may be mutated by another fiber between the condition and you accessing the variable. See this section (under "Limitations") in the Crystal docs.
You can do it like this, assigning the instance variable to a local variable that will not change out from under you:
def add(other : X?)
unless other.nil?
if s = #son
s.add(other)
else
#son = other
end
end
end

Related

variable type tagged additionally with Nil in ensure clause

I wonder why the type of the variable is (String | Nil) and not just String? Is there a way one can make it just String?
def main
text = "hello"
ensure
puts typeof(text) # => (String | Nil)
end
main
https://carc.in/#/r/2w3a
ensure runs after the main body in any case, even if there was an exception raised. Because this could have happend anywhere, it has to be considered that the body of the method hasn't been executed at all if it failed at the first instruction.
Therefore, in the ensure block, all variables are known but it must be assumed that their value can be nil.
If you're sure that text is always set, you don't need to protect that assignment in a rescue/ensure clause.
def main
text = "hello"
begin
# here is the code that might fail
ensure
puts typeof(text) # => String
end
end

why is the assignment of instance variables to local variables is need in crystal?

In Crystal compiler source code I've seen such code
def dirname
filename = #filename
if filename.is_a?(String)
File.dirname(filename)
else
nil
end
end
def original_filename
case filename = #filename
when String
filename
when VirtualFile
filename.expanded_location.try &.original_filename
else
nil
end
end
def <=>(other)
self_file = #filename
other_file = other.filename
if self_file.is_a?(String) && other_file.is_a?(String) && self_file == other_file
{#line_number, #column_number} <=> {other.line_number, other.column_number}
else
nil
end
end
So, what the reason to assign an instance variable to a local variable instead of using instance variable directly?
Because #filename may be changed concurrently between the time we check if it's not nil (if #filename) and the time we access it. Crystal being a compiled program, would #filename not be the type it's expected to be, then the program would crash with a segfault.
By assigning to a local variable, we make sure the variable does exists.

RSpec it block with variable name

I have a function get_type that returns a string given an int:
def get_type(integer)
types = [...]
return types[integer]
end
When testing with RSpec, I tried doing the following:
describe 'function' do
context 'on valid input'
let(:input){ 2 }
let(:type){ 'large' }
let(:result){ get_type input }
it{ expect(result).to eq(type) }
end
end
However, this gives the message:
function on valid input should eq "large"
without any mention to the input, thus sounding like the function should always return "large".
How should this message be changed to say something like:
function on valid input should eq type
or another meaningful message? I could name the it block:
it 'should have the correct type' do
expect(result).to eq(type)
end
but is there a nicer way to do this without essentially typing out the test twice?
I think the unhelpful message should be considered a smell - you're headed down a road where every test is just expect(result).to eq(expected) with a wall of let. To my mind this is overuse of let - I don't think you gain anything over
describe 'function' do
context 'on valid input' do
it{ expect(get_type(2)).to eq('large') }
end
end
Which would produce a more helpful failure message. I would keep let for when the expressions are more complex or when I can give them a better name (eg a hash of attributes called valid_attributes)

Use rspec to test class methods are calling scopes

I have created rspec tests for my scopes (scope1, scope2 and scope3) and they pass as expected but I would also like to add some tests for a class method that I have which is what is actually called from my controller (the controller calls the scopes indirectly via this class method):
def self.my_class_method(arg1, arg2)
scoped = self.all
if arg1.present?
scoped = scoped.scope1(arg1)
end
if arg2.present?
scoped = scoped.scope2(arg2)
elsif arg1.present?
scoped = scoped.scope3(arg1)
end
scoped
end
It seems a bit redundant to run the same scope tests for each scenario in this class method when I know they already pass so I assume I really only need to ensure that different scopes are called/applied dependant on the args being passed into this class method.
Can someone advise on what this rspec test would look like.
I thought it might be something along the lines of
expect_any_instance_of(MyModel.my_class_method(arg1, nil)).to receive(:scope1).with(arg1, nil)
but that doesn't work.
I would also appreciate confirmation that this is all that's necessary to test in this situation when I've already tested the scopes anyway would be reassurring.
The Rspec code you wrote is really testing the internal implementation of your method. You should test that the method returns what you want it to return given the arguments, not that it does it in a certain way. That way, your tests will be less brittle. For example if you change what scope1 is called, you won't have to rewrite your my_class_method tests.
I would do that by creating a number of instances of the class and then call the method with various arguments and check that the results are what you expect.
I don't know what scope1 and scope2 do, so I made an example where the arguments are a name attribute for you model and the scope methods simply retrieve all models except those with that name. Obviously, whatever your real arguments and scope methods do you should put that in your tests, and you should modify the expected results accordingly.
I used the to_ary method for the expected results since the self.all call actually returns an ActiveRecord association and therefore wouldn't otherwise match the expected array. You could probably use includes and does_not_includes instead of eq, but perhaps you care about the order or something.
describe MyModel do
describe ".my_class_method" do
# Could be helpful to use FactoryGirl here
# Also note the bang (!) version of let
let!(:my_model_1) { MyModel.create(name: "alex") }
let!(:my_model_2) { MyModel.create(name: "bob") }
let!(:my_model_3) { MyModel.create(name: "chris") }
context "with nil arguments" do
let(:arg1) { nil }
let(:arg2) { nil }
it "returns all" do
expected = [my_model_1, my_model_2, my_model_3]
expect_my_class_method_to_return expected
end
end
context "with a first argument equal to a model's name" do
let(:arg1) { my_model_1.name }
let(:arg2) { nil }
it "returns all except models with name matching the argument" do
expected = [my_model_2, my_model_3]
expect_my_class_method_to_return expected
end
context "with a second argument equal to another model's name" do
let(:arg1) { my_model_1.name }
let(:arg2) { my_model_2.name }
it "returns all except models with name matching either argument" do
expected = [my_model_3]
expect_my_class_method_to_return expected
end
end
end
end
private
def expect_my_class_method_to_return(expected)
actual = described_class.my_class_method(arg1, arg2).to_ary
expect(actual).to eq expected
end
end

scope not working on Mongoid (undefined method `to_criteria')

I invoke ReleaseSchedule.next_release in other controller
and got the following error
NoMethodError (undefined method `to_criteria' for #<ReleaseSchedule:0x007f9cfafbfe70>):
app/controllers/weekly_query_controller.rb:15:in `next_release'
releae_schedule.rb
class ReleaseSchedule
scope :next_release, ->(){ ReleaseSchedule.where(:release_date.gte => Time.now).without(:_id, :created_at, :updated_at).first }
end
That's not really a scope at all, that's just a class method wrapped up to look like a scope. There are two problems:
You're saying ReleaseSchedule.where(...) so you can't chain the "scope" (i.e. ReleaseSchedule.where(...).next_release won't do what it is supposed to do).
Your "scope" ends in first so it won't return a query, it just returns a single instance.
2 is probably where your NoMethodError comes from.
If you really want it to be a scope for some reason then you'd say:
# No `first` or explicit class reference in here.
scope :next_release, -> { where(:release_date.gte => Time.now).without(:_id, :created_at, :updated_at) }
and use it as:
# The `first` goes here instead.
r = ReleaseSchedule.next_release.first
But really, you just want a class method:
def self.next_release
where(:release_date.gte => Time.now).without(:_id, :created_at, :updated_at).first
end
The scope macro is, after all, just a fancy way to build class methods. The only reason we have scope is to express an intent (i.e. to build queries piece by piece) and what you're doing doesn't match that intent.