Question about typeof

while reading typeof - Crystal

class Array
  def self.elem_type(typ)
    if typ.is_a?(Array)
      puts "typ arr go #{typ}"
      elem_type(typ.first) #2
    else
      puts "typ go #{typ}"
      typ #1
    end
  end
end

nest = [1, ["b", [:c, ['d']]]]
#p Array.elem_type(nest)
p typeof(Array.elem_type(nest)) 

it outputs (Char | Int32 | String | Symbol),
I’m wrapping my head over it why it can flatten the type in nested array.

so is compile time compiler first checks:
(nest) # => Array(Int32 | Array(String | Array(Symbol | Array(Char)))) ,

take #1 will have Int32
take #2 will have Array(String | Array(Symbol | Array(Char))

so it will becomes Int32 | String | Array(Symbol | Array(Char)),

then another round compiler goes to Int32 | String | Symbol | Array(Char) like that,

then another round compiler goes to Int32 | String | Symbol | Char ?

This smells like tail-call optimization to me, flattening out all the calls into a single call. You get a very different outcome if you handle the different types in different methods:

class Array
  def self.elem_type(typ : Array)
    puts "typ arr go #{typ}"
    elem_type(typ.first) # 2
  end

  def self.elem_type(typ)
    puts "typ go #{typ}"
    typ # 1
  end
end

nest = [1, ["b", [:c, ['d']]]]
p typeof(Array.elem_type(nest))
# (Array(Array(Array(Char) | Symbol) | String) | Array(Array(Char) | Symbol) | Array(Char) | Char | Int32 | String | Symbol)

TCO is a runtime technique. The evaluation of typeof happens entirely at compile time. You’ll notice that the puts calls don’t actually print anything, because the method never executes. It’s just used for typing.

Also, the recursive call isn’t even the last expression in the method.

The behaviour you see with your edited example is a bit confusing. But there’s a difference between overloading and branching on is_a?.
I’m not sure what exactly the overloading is doing here and my expectation would be that the result should be the same as before.

It works if you add type restrictions to the second method (self.elem_type(typ : Char | Int32 | String | Symbol)). :person_shrugging:

Not exclusively. LLVM does tail call optimization during code generation on 4 different architectures. The LLVM Target-Independent Code Generator — LLVM 18.0.0git documentation

Okay, let’s call it runtime and codegen technique. typeof evaluation never reaches either of them as it’s entirely at the semantic level.

1 Like