Why following flatten_type method work?

I saw some code in this post, but i don’t know why this code work as expected.(flatten the type)

def flatten_type(object)
  if object.is_a?(Array)
    flatten_type(object[0])
  else
    object
  end
end

puts typeof(flatten_type(1))                          #=> Int32
puts typeof(flatten_type([1, [2]]))                   #=> Int32
puts typeof(flatten_type([1, [2, ['a', 'b']]]))       #=> Int32 | Char

Any idea? Thanks.

What part of it is suprising do you? What do you not understand?

Let us use following code as a example:

def flatten_type(object)
  if object.is_a?(Array)
    flatten_type(object[0])
  else
    object
  end
end

x = [1, [2, ['a', 'b']]]
puts x[0].is_a? Array                           # => false
puts typeof(x[0])                               # => (Array(Array(Char) | Int32) | Int32)
puts typeof(flatten_type([1, [2, ['a', 'b']]])) # => Int32 | Char

I can’t understand how flatten_type can flatten the nested type.

x = [1, [2, [‘a’, ‘b’]]]; the object[0] is x[0], it is 1, right? the type of 1 is (Array(Array(Char) | Int32) | Int32), how it change to Int32 | Char ?

Thanks

Hi! You can read about it here: if var.is_a?(...) - Crystal

Let us know if it’s still not clear.

Yes, still not clear, I think I lack imagination in some way.

let me try guess this code step by step.(crystal i not work for me for now because missing LLVM 15 support)

  • x is [1, [2, [‘a’, ‘b’]]]
  • Try run flatten_type(x)
  • x.is_a? Array => Yes
  • try run flatten_type(x[0]), because x[0] is 1, so, should be flatten_type(1).
  • 1.is_a? Array => No
  • So, flatten_type finally return object, it is 1,
  • the compile-time type of 1 should be (Array(Array(Char) | Int32) | Int32)

Why the result is Int32 | Char ?

This is the part that’s actually slightly different.

The compiler doesn’t know what’s in the first position of the array. You know x[0] is an int, but for the compiler that could be an int or the other array.

It’s easier if you also print the types as the compiler figures things out:

def flatten_type(object : T) forall T
  {% puts "typeof(object) is #{T}" %}
  if object.is_a?(Array)
    show_typeof_object0(object[0])
    flatten_type(object[0])
  else
    show_typeof_object(object)
    object
  end
end

def show_typeof_object0(x : T) forall T
  {% puts "typeof(object[0]) is #{T}" %}
end

def show_typeof_object(x : T) forall T
  {% puts "typeof(object) is #{T}" %}
end

flatten_type([1, [2, ['a', 'b']]])

The output is:

typeof(object) is Array(Array(Array(Char) | Int32) | Int32)
typeof(object[0]) is (Array(Array(Char) | Int32) | Int32)
typeof(object) is (Array(Array(Char) | Int32) | Int32)
typeof(object[0]) is (Array(Char) | Int32)
typeof(object) is (Array(Char) | Int32)
typeof(object[0]) is Char
typeof(object) is Char
typeof(object) is Char
typeof(object) is Int32

Another way to understand this:

a = [1, 'a']
puts typeof(a[0]) # => Char | Int32

That is, typeof(a[0]) is not Int32, it’s the union. The compiler can’t know (in general) what’s in each position of the array.

2 Likes

I have been trying to understand this code intermittently these days, but I still don’t understand how above code is running.

Following is a improved version for above code.

def flatten_type(object : T) forall T
  {% puts "typeof(object111) is #{T}" %}
  p! object.is_a?(Array)
  if object.is_a?(Array)
    show_typeof_object0(object[0])
    flatten_type(object[0])
  else
    show_typeof_object(object)
    object
  end
end

def show_typeof_object0(x : T) forall T
  {% puts "typeof(object[0]) is #{T}" %}
end

def show_typeof_object(x : T) forall T
  {% puts "typeof(object222) is #{T}" %}
end

# ic(1.7.2):001> typeof([1, [2, ['a', 'b']]])
# => Array(Array(Array(Char) | Int32) | Int32)

flatten_type([1, [2, ['a', 'b']]])

The output is:

 ╰─ $ cr run 1.cr 
typeof(object111) is Array(Array(Array(Char) | Int32) | Int32)
typeof(object[0]) is (Array(Array(Char) | Int32) | Int32)
typeof(object111) is (Array(Array(Char) | Int32) | Int32)
typeof(object[0]) is (Array(Char) | Int32)
typeof(object111) is (Array(Char) | Int32)
typeof(object[0]) is Char
typeof(object111) is Char
typeof(object222) is Char
typeof(object222) is Int32
object.is_a?(Array) # => true
object.is_a?(Array) # => false

I still confusing on why the code if object.is_a?(Array) run only twice? if the latter is return false, why above output in macro is not output as expected? i consider some magic happen on compile-time before the if condition is run?

Because the first time the method is called, the input is an array. Then the first element of that array, an integer, is passed to the method.

The method isn’t called for every element in the array (this is runtime execution.)

BUT: the method is analyzed for every possible element type in the array (this is compile-time analysis.)

2 Likes

I guess I might understand, thanks.

But I have to say that some knowledge that is never mentioned in the official documents seems to be used here? It’s hard to figure out for a programmer who’s used to Ruby.