Overloadable implicit type casts

I feel an excellent feature to Crystal would be the ability to create overloadable implicit type casts for any class. This would reduce the amount of boilerplate overloaded methods with slightly different parameter types simply to support extra types.

Consider this example:

class Foo
    def initialize(@bar : Bar)
    end

    def implicit : Bar
        @bar
    end
end

class Bar
    def initialize(@message : String)
    end

    def say
        puts @message
    end
end

def baz(bar : Bar)
    puts bar.say
end

bar = Bar.new "Hello, world"
baz(bar) #=> Hello, world
# Passing an object of type `Bar` to `baz` yields expected functionality

foo = Foo.new bar
baz(foo) #=> Hello, world
# Passing an object of type `Foo` (which shares no common inherited ancestor
# with `Bar`) internally calls the `#implicit` method which determines how
# the type should mutate with the expected type.

# Thus, `baz(foo)` is identical to `baz(foo.implicit)`

In this example, there exists two types, Foo and Bar. The top-level method baz is responsible for invoking Bar#say. It is clear that baz will not accept Foo any time, as Foo does not share any inherited ancestors with Bar, and no overloads exist for baz that accept a Foo directly.

However, with the proposition of overloadable implicit type casts, Foo can define a sort of “implicit” method that explains to the compiler how the object should act as if it were used as another type. In this case, Foo has an instance variable, bar, that defines a clear instance of type Bar. Additionally, Foo defines a type-annotated implicit method that returns its instance variable of bar, so that when it is used in an implicit type cast, it pulls this value from its defined implicit type cast method.

This proposition would reduce the amount of redundant checks to responds_to?, and reduce the amount of overloaded methods with slightly different parameter types.

This feature is found in several other languages with well-defined approaches to OOP. For example, C# allows you to overload implicit type casts as an operator:

public static implicit operator Bar(Foo foo) {
    return foo.Bar;
}

Additionally, C# also allows you to specify explicit over implicit to provide behavior when explicitly casting. (Bar) foo would yield this functionality.

Thanks for this suggestion. It sounds interesting, but I’m not sure we should add something like this to Crystal. Until now we have almost no implicit type conversions at all in the language.
I guess such custom implicit conversions might be surprising, and I don’t entirely understand what they’re good for.
Could you show some real-world use cases for this? And which languages have such a feature? I could not find any other than C#.

I have created a repo of a few use cases I initially thought of when writing this post. I wrote a lot of C# prior to discovering Crystal (I actually had never written a line of Ruby prior), so I used this feature a lot. It’s something that requires a slightly different mindset but personally helps clean up the look of code a lot.

Included in this repo are a few examples for Crystal (I will add more as time goes), an example from C#, and an example from C++, which also supports overloadable implicit type casts, much like C#.

My experience with implicit conversions is that it makes it really hard to understand what’s going on. You pass a type somewhere, suddenly a method that doesn’t accept that type ends up being invoked and you are like “wat?”. Then you have to remember there might be implicit conversions, you have to go look that up, etc. It’s all very confusing. In Crystal we pretty much prefer explicit over implicit everywhere. The few cases where we do implicit conversions are hardocded into the language and they are kind of natural (integer literals are automatically casted to floats, or symbols can be used instead of the more verbose enum full name).

I really don’t think we’ll end up doing something like this.

1 Like

I fail to see a reasonable benefit provided by these implicit conversions as opposed to being explicit. Explicit is always more flexible and easier to comprehend. And it doesn’t even add a lot of verbosity at the call site or by providing method overloads.

I think the multi-dispatch and implicit type cast might be challenging to coexist with no surprised.

I even have some itches with the current implicit type casts. Although the non-ambiguity check helps.

My past needs of implicit type casts were replaced by factory methods, dsl, or explicit converters.

Currently, to implement the monetary sample I would assume there is a #to_money method for the values that you want to act as monetary. You will not be able to use type restrictions on those methods, but if you already want to be called by any type that would’ve an implicit conversion you will be in the same situation.

I completely understand your reasoning here. However, I am curious, could you provide an example of an experience you’ve had with implicit conversions that blatantly confused you? I used to work with C# a lot, and as such, used them somewhat often. I found them nothing but beneficial to my code and to help people using my code clean up theirs from unnecessary calls to converting methods.

I can agree here. It makes sense that explicit methods are more readable and logical. The only reason I have against them is just that it adds an extra method call that could be considered unnecessary. Perhaps instead of naming general explicit conversion methods to_X, some actual enforcement on how they must be written could be implemented. I don’t like how loose it is to define an explicit conversion method as it is, as I could call it whatever and its functionality may not behave exactly as expected.

I feel that implicit conversions would effectively reduce the amount of multi-dispatch definitions necessary, as literally implicitly defines more multi-dispatch definitions by transforming an argument prior to calling the method.

I am curious about your usage of factory methods and DSL for replacing your needs of implicit type casts, for my own benefit. I tend to think in a mindset where I require implicit type casts too often and am reminded that Crystal doesn’t have such a feature.

Instead of using numbers and strings to build objects in specs, some nice dsl like GitHub - thoughtbot/factory_bot: A library for setting up Ruby objects as test data. ended up working pretty well for me. But I am comparing my experience in C# 4.0 when I was involved for example in Moq vs my current practice and experience as as a Ruby on Rails developer. So, it’s not only a library/language feature, but a delta of some years :stuck_out_tongue:

Regarding multi-dispatch vs implicit casts, how would you resolve if a type A can be converted to B and C, and both foo(x : B) and foo(x : C) are defined? Currently with auto casts that won’t compile and the user need to disambiguate by doing something that will not perform a cast. The similar to implicit cast is explicit cast, which well… make things explicit. Since the information of whether A is convetible to B or C relies in A and not in the call foo(a), I found it harder to follow.

1 Like

If there was overloadable implicit type casting, https://github.com/BrucePerens/recursive_generic might have been very much easier to code because much of the wrapping and unwrapping might have been replaced with two methods in ValueWrapper. Also, the number-size promotion (int32 to int64, etc.) proposed a while back might have been written in the stdlib rather than the compiler.

In a case such as:

class A
  def implicit : B
    # return a B
  end

  def implicit : C
    # return a C
  end
end

def some_method(o : B)

end

def some_method(o : C)

end

test = A.new
some_method test

how will the compiler decide what to implicitly cast A to? Other than this edge case I see a use case in personal code, but it would be really confusing if it were in a shard or library of some sort and stuff was implicitly cast to other stuff without my knowledge. Would auto-docs include a section like “implicitly cast to:…” or something? I know this is kind of old but I’m interested in this feature.