I am getting compiler errors when trying to determine the type of a class. In my code I am using
somevar.class.to_s in a case statement. The compiler complains that
a class DrawElement cannot be cast to DrawPath.
Example
module XYZ
class DrawElement
...
end
class DrawPath < DrawElement
...
end
def f(element : DrawElement)
case element.class.to_s # <<<<<<<<<<<<< compiler error
when "XYZ::DrawPath"
else
end
end
end
The construct somevar.class.to_s felt strange but gives me the correct result in the following example.
Example code
module XYZ
class A
property x : Int32 = 0
end
class B < A
property y : Int32 = 1
end
class C < B
property z : Int32 = 2
end
def self.print1(x : A)
case x
when XYZ::A
t = x.as(A)
puts "x = #{t.x}"
when XYZ::B
t = x.as(B)
puts "y = #{t.y}"
when XYZ::C
t = x.as(C)
puts "z = #{t.z}"
else
end
end
def self.print2(x : A)
case x.class.to_s
when "XYZ::A"
t = x.as(A)
puts "x = #{t.x}"
when "XYZ::B"
t = x.as(B)
puts "y = #{t.y}"
when "XYZ::C"
t = x.as(C)
puts "z = #{t.z}"
else
end
end
end
x = XYZ::C.new
# expect z = 2
::XYZ.print1(x)
# expect z = 2
::XYZ.print2(x)
The reason print1 prints 0 and not 2, is the order of the case whens. Since C inherits from B and B inherits from A, C is essentially A, so the first when is truthy, and prints 0. If you put C first, it would work the way you want.
Although, wouldn’t it be better to use overloads? I.e.
def print(t : A)
puts t.x
end
def print(t : B)
puts t.y
end
def print(t : C)
puts t.z
end
OK, good to know but that is not the problem. I think I am not explaining this very well.
In my code I am using the construct “somevar.class.to_s” almost everywhere without thinking about it.
But now the compiler is telling me it cannot make the cast from “XYZ::DrawElement” to “XYZ::DrawPath”
Question : what is the correct way to determine the type of class ?
I wrote a graphical library with all kinds of draw elements and during the output phase a big case switch is used to determine what to draw based on the given draw element.
The class hierarchy is something like
module XYZ
class DrawElement
...
end
class DrawSuperPenPath < DrawElement
...
end
class DrawShade < DrawElement
...
end
... etc ..
end
Can you share some code that reproduces this issue? Both of these examples compile fine.
You would be better off using Overloading - Crystal. I.e. you call the same method for every type of element, and the compiler just knows which method to use based on the type restriction of that method (or lack of).
Showing last frame. Use --error-trace for full trace.
In drawelement.cr:26:11
26 | x = object.as(XYZ::DrawPath)
^
Error: can't cast XYZ::DrawElement to XYZ::DrawPath
Sample code
def DrawElement_to_vminstance(object : XYZ::DrawElement) : VmInstance
vminstance = create_DrawElement_object()
case object.class.to_s
when "XYZ::DrawPath"
x = object.as(XYZ::DrawPath) # <<<<<<<<<<<<<<<< compiler error >>>>>>>>>>>>>>>>>>>
vm = XYZ_DrawPath_to_vminstance(x)
return vm
when "XYZ::DrawFill"
x = object.as(XYZ::DrawFill)
vm = XYZ_DrawFill_to_vminstance(x)
return vm
when "XYZ::DrawClipBegin"
x = object.as(XYZ::DrawClipBegin)
vm = XYZ_DrawClipBegin_to_vminstance(x)
return vm
when "XYZ::DrawClipEnd"
x = object.as(XYZ::DrawClipEnd)
vm = XYZ_DrawClipEnd_to_vminstance()
return vm
when "XYZ::DrawImage"
x = object.as(XYZ::DrawImage)
vm = XYZ_DrawImage_to_vminstance(x)
return vm
when "XYZ::DrawNoPaletteImage"
x = object.as(XYZ::DrawNoPaletteImage)
vm = XYZ_DrawNoPaletteImage_to_vminstance(x)
return vm
when "XYZ::DrawPaletteImage"
x = object.as(XYZ::DrawPaletteImage)
vm = XYZ_DrawPaletteImage_to_vminstance(x)
return vm
when "XYZ::DrawLayer"
x = object.as(XYZ::DrawLayer)
vm = XYZ_DrawLayer_to_vminstance(x)
return vm
when "XYZ::DrawShade"
x = object.as(XYZ::DrawShade)
vm = XYZ_DrawShade_to_vminstance(x)
return vm
when "XYZ::DrawTensorShade"
x = object.as(XYZ::DrawTensorShade)
vm = XYZ_DrawTensorShade_to_vminstance(x)
return vm
when "XYZ::DrawTensorShade1"
x = object.as(XYZ::DrawTensorShade1)
vm = XYZ_DrawTensorShade1_to_vminstance(x)
return vm
when "XYZ::DrawGouraudShade"
x = object.as(XYZ::DrawGouraudShade)
vm = XYZ_DrawGouraudShade_to_vminstance(x)
return vm
when "XYZ::DrawLatticeShade"
x = object.as(XYZ::DrawLatticeShade)
vm = XYZ_DrawLatticeShade_to_vminstance(x)
return vm
when "XYZ::DrawAxialShade"
x = object.as(XYZ::DrawAxialShade)
vm = XYZ_DrawAxialShade_to_vminstance(x)
return vm
when "XYZ::DrawRadialShade"
x = object.as(XYZ::DrawRadialShade)
vm = XYZ_DrawRadialShade_to_vminstance(x)
return vm
when "XYZ::DrawLabel"
x = object.as(XYZ::DrawLabel)
vm = XYZ_DrawLabel_to_vminstance(x)
return vm
when "XYZ::DrawBar"
x = object.as(XYZ::DrawBar)
vm = XYZ_DrawBar_to_vminstance(x)
return vm
when "XYZ::DrawBars"
x = object.as(XYZ::DrawBars)
vm = XYZ_DrawBars_to_vminstance(x)
return vm
when "XYZ::DrawFunctionImage"
x = object.as(XYZ::DrawFunctionImage)
vm = XYZ_DrawFunctionImage_to_vminstance(x)
return vm
when "XYZ::DrawFunctionShade"
x = object.as(XYZ::DrawFunctionShade)
vm = XYZ_DrawFunctionShade_to_vminstance(x)
return vm
else
# Crystal 0.34
end # case
vminstance.fields["id"] = Env::Value.new(object.id)
return vminstance
end
```
Let me explain what I am doing so you have more information. I wrote a graphical vector library a year ago, and now I am working on my interpreter to interface with this library (both written in Crystal).
So in my interpreter I am creating objects in memory to mirror the Crystal objects. The sample code I showed is generated. I have two FFI generator versions, an older versions which uses the construct somevar.class.to_s and a newer version which extended the older versions with some class initializer.
When diffing the code both versions produce, there are small differences like extra spaces in the code or an extra comment but for the rest the code is the same. Now however version 2 gives me compiler errors (the sample code I showed which is identical to the code of version 1).
That prompted me to investigate the “somevar.class.to_s” case switch. I thought as the compiler gives me an error, I should change it, so I did and the compiler now compiles the code.
# not compiling
case element.class.to_s
when ...
....
end
to code which does compile
# compiling
case element
when ...
end
The problem now is that, as you explained, the logic is changed due to the overloading of classes and the order in which they appear in the case switch.
Right, your previous version (was kinda a hacks/smell TBH), didn’t have this problem as it was essentially just string comparisons. However, now that you’re actually comparing on type, the order/setup you have is trying to “change” the type of an object using .as, which is not its purpose. You can easily reproduce this error doing something like:
class A; end
class B < A; end
class C < B; end
B.new.as(C) # => Error: can't cast B to C
I still think you would be better off in the long run to not use this giant case and switch to something where the language does the heavy lifting for you, like overloads. For example, the majority of cases are like:
x = object.as(XYZ::DrawFill)
vm = XYZ_DrawFill_to_vminstance(x)
return vm
which uses the passed in object to create a vm and returns it. Why is the case needed in the first place when you could just do obj.vm which each type implements as like
class XYZ::DrawFill < XYZ::DrawElement
...
def vm : VmInstance
XYZ_DrawFill_to_vminstance self
end
end
end
You could define define a default implementation on the parent abstract type to handle the logic at the end of the case, for when its an element that wasn’t found. Granted I’m not familiar with your implementation or what its doing, this is all just based on the examples you provided.
Thanks for your clear explanation, the example you gave feels more natural but more importantly it works. So I have a lot of work to do yet (thought I was almost done).
Out of interest, despite being a hack (somevar.class.to_s), the compiler did not object until now, but why ?
Because it was just doing string comparisons and not working with the actual types. I.e. given the setup of like:
class A; end
class B < A; end
class C < B; end
The case was expanding to like, assuming we’re using C again like your first example:
tmp = obj.class.to_s # => "C"
if tmp == "A"
puts 0
elsif tmp == "B"
puts 1
elsif tmp == "C"
puts 2
end
In this case, even if C is a child of A and B, it wasn’t matching, as again its just string comparisons, so it doesn’t match anything until it gets to "C", and prints 2. However when you switched to the actual typed version, the semantics of the resulting if statement changed to something like:
tmp = obj
if tmp.is_a? A
puts 0
elsif tmp.is_a? B
puts 1
elsif tmp.is_a? C
puts 2
end