Determine type of class

Hi

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

and per element a different draw routine is used.

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).

This is the compiler error

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

Notice == is now is_a? which takes into consideration that C is a child of A and B, which results in a 0 being printed. This is because of crystal/src/class.cr at eb46097440bf20d22eff6c38fc82732927bb193e · crystal-lang/crystal · GitHub, since #=== is what is used for case equality.