I have defined two simple methods, one that takes an array of arrays of whatever and another that takes an array of whatever.
The compiler complains that when given an array of Int32 the Array(T) implementation does not match. Shouldn’t it?
def foo(arr : Array(Array(T)))
p "This was an array of arrays"
end
def foo(arr : Array(T))
p "This was just an array"
end
If I call foo([0,1,2,3]) I would expect the output to be "This was just an array" rather than the error above. I suspect I have missed something integral to generic type implementation but can’t figure it out from the docs.
module Foo(T)
extend self
def foo(arr : Array(Array(T)))
p "This was an array of arrays"
end
def foo(arr : Array(T))
p "This was just an array"
end
end
Foo.foo([1,2,3])
error in line 13
Error: no overload matches 'Foo(T).foo' with type Array(Int32)
Overloads are:
- Foo(T)#foo(arr : Array(Array(T)))
- Foo(T)#foo(arr : Array(T))
module Foo
extend self
def foo(arr : Array(Array(T))) forall T
p "This was an array of arrays"
end
def foo(arr : Array(T)) forall T
p "This was just an array"
end
end
p Foo.foo([1,2,[3,4]])
returns the error:
Error: no overload matches 'Foo.foo' with type Array(Array(Int32) | Int32)
Overloads are:
- Foo#foo(arr : Array(Array(T)))
- Foo#foo(arr : Array(T))
I really would like to accomplish understanding how generics work in Crystal. The problem is that Arrays are not strongly typed, so I don’t know how to reason about them when some aspects of the language are strongly typed. Because you can have an Array of various types ([1, "a", [2,"b"]], for example) making a method for Array(T) is very difficult for me to figure out. What I really want is to call one method when T is U and another when T is [U]. How does one do that?
The thing is, when you have Array(Int32) | Array(Array(Int32)), which is something that happens in your code, it doesn’t match T because there’s no way to find a single T for that.
Instead, make the code simpler:
def foo(a : Array)
a.each do |a|
foo(a)
end
end
def foo(a)
p a
end
foo([1, [2, [[3]], [4, [[5]]], 6, 7], 8])
Or even:
def foo(a)
if a.is_a?(Array)
a.each do |a|
foo(a)
end
else
p a
end
end
foo([1, [2, [[3]], [4, [[5]]], 6, 7], 8])
In most cases T, forall T, etc., are very advanced features and not needed.
Is it fair to say then that runtime type checks are preferred over compile time checks in Crystal?
And I still don’t understand the output from the example in my last post. Why does it not recurse into the [[3]] structure when it does for all the other nested arrays?
Compile-time checks are preferred. You can consider the behaviour in your snippet a bug or a limitation. Sorry, I don’t have more time to explain it but maybe someone else can.
I appreciate the time you’ve taken. I changed the snippet slightly so that the output reflects what I am confused about more clearly:
def foo(a : Array(T)) forall T
a.each_with_index do |a, i|
p "first call for #{a.class}"
foo(a)
end
end
def foo(a : T) forall T
p "second call for class #{a.class}"
p a
end
foo([1, [2, [[3]], [4, [[5]]], 6, 7], 8])
returns
“first call for Int32”
“second call for class Int32”
1
“first call for Array(Array(Array(Array(Int32)) | Int32) | Array(Array(Int32)) | Int32)”
“first call for Int32”
“second call for class Int32”
2
“first call for Array(Array(Int32))”
“second call for class Array(Array(Int32))”
[[3]]
“first call for Array(Array(Array(Int32)) | Int32)”
“first call for Int32”
“second call for class Int32”
4
“first call for Array(Array(Int32))”
“first call for Array(Int32)”
“first call for Int32”
“second call for class Int32”
5
“first call for Int32”
“second call for class Int32”
6
“first call for Int32”
“second call for class Int32”
7
“first call for Int32”
“second call for class Int32”
8
Clearly the expected recursion is happening for [[5]] in a way it is not for [[3]], which is very confusing to me.
I think this is going to come down to an interesting problem of mathematical sets.
Define N as whole numbers.
Define A as groups of N.
Define B as groups of N | A.
Define C as groups of N | B | C.
With that recursive type C the “unit type” is going to be (N | B) even though the unit type of B is N. Any recursive type should trigger this “bug”, which might not really be a bug so much as unintuitive behavior.
That’s why [1, [2, [[3]], [4, [[5]]]]] triggers the bug but [1, [2, [[[3]], 4, [[5]]]]] does not. In the first [2, [[3]], [4, [[5]]]] is a recursive type. If we define
Z = Array(Array(Int32)) | Int32 | Z
then [2, [[3]], [4, [[5]]] is type Z but [2, [[[3]], 4, [[5]]]] is not. It is type
Y = X | Int32 where X = Array(Array(Int32)) | Int32
Because this is not a recursive type there is no bug.