Doubt about overloading

def foo(x : String)

    puts "foo(String): #{x}."

end

def foo(x : Int32)

    puts "foo(Int32): #{x}."

end

def test(x : Object)

    p! typeof(x) # String | Int32, why not Object?

    foo(x) # compiled, dispatch in run-time?

end

x = ARGV[0]

x = x.to_i32? || x

test(x)
$ ./foo.cr sdfs

typeof(x) # => (Int32 | String)

foo(String): sdfs.


$ ./foo.cr 11

typeof(x) # => (Int32 | String)

foo(Int32): 11.

As expected, the decision is made at runtime. A union type has a memory layout consisting of an Int32 type ID and a payload area. The compiled code checks the type ID with conditional branches and calls the corresponding function.

This is because Object is just a restriction, actual types of parameters are calculated during compile time. If you add test(1.0) to your example, compiler will complain.

typeof(x) is the static type of the parameter, which in turn is the static type of the argument to the test(x) call. It comes fromx = x.to_i32? || x, which can only be an Int32 or a String. The compiler would generate an instance of test that has Int32 | String parameters; if you separately call test(11) and test("sdfs") in your source code, there would be extra instantiations for Int32 and String parameters respectively. That is to say, the arguments are never upcast to Object (which is unimplemented anyway).

If you know C++, a general rule is that something like this:

def foo(x : Int32 | String)
  # typeof(x) may be `Int32`, `String`, or `Int32 | String`
end

is analogous to:

void foo(std::convertible_to<std::variant<int, const char *>> auto x) {
  // `decltype(x)` may be `int`, `const char *`, or many others due to implicit conversion rules
}

That is interesting

# priority: Int32|String = String|Int32 > String|Int > String|Int32||Float32 > Object > T > auto

def test(x : Object)

    puts "test(Object)"

    p! typeof(x) # String | Int32

    foo(x) # compiled, dispatch in run-time

end

def test(x : Int32 | String)

    puts "test(Int32|String)"

    p! typeof(x)

    foo(x)

end

# same as test(Int32|String) and would override it

def test(x : String | Int32)

    puts "test(String|Int32)"

    p! typeof(x)

    foo(x)

end

def test(x : String | Int)

    puts "test(String|Int)"

     p! typeof(x)

     foo(x)

end

def test(x : String | Int32 | Float32)

    puts "test(String|Int32|Float32)"

    p! typeof(x)

    foo(x)

end

def test(x : T) forall T

    puts "test(T)"

    p! typeof(x)

    foo(x)

end

def test(x)

    puts "test(x)"

    p! typeof(x)

    foo(x)

end
1 Like

I mistakenly believe that crystal, like some other languages, overloading is always in compile-time.