This was inspired by #8511, where I commented what I could see as a potential solution to the same problem. I figured I could outline it here and see what others from the community think about this idea.
The problem
Crystal has a pretty strict type system, which is awesome. Unfortunately, the stricter things are the harder it can be to write more generic code. For this reason modules are a godsend. Using a module you can pretty simply define a basic interface including a couple abstract types and include it in any class or struct you want. For example:
module Numeric
abstract def to_i : Int32
end
The problem is that you then have to monkeypatch every type that fits this interface that you may want:
class String
include Numeric
end
abstract struct Number
include Numeric
end
# ...
The problem then becomes keeping track of every type that fits the interface, and requires monkeypatching which is generally an anti-pattern. This is also unfeasible, especially for use cases such as a logger.
The Solution
What I’m proposing is a new interface type to cover these use cases. It would work much like a module, but be completely abstract and not require you to implement it in each type you want to fit the interface. As an example:
interface Numeric
abstract def to_i : Int32
end
class Foo
def initialize(@bar : Numeric)
# this should work
puts @bar.to_i
# this should either not work, or be dependent on the types being passed in.
puts @bar.to_s
end
end
The idea is to provide a generic interface for cases in which you only intend to use a limited subset of methods from a number of different types. Normally this would be handled using a messy alias type, or a module as described above. The problem is that the likelyhood of an alias/module covering all possible types is very low, and the alias/module becomes a maintenance burden.
Now if I want to instantiate the above Foo
class, I can do this:
require "big"
foo1 = Foo.new(10000)
foo2 = Foo.new("100")
foo4 = Foo.new(3.14)
foo3 = Foo.new(BigInt.new("123456779984923492348234324234")
foo4 = Foo.new(MyCustomType.new) # Provided MyCustomType has a `to_i` method.
Obviously this example is contrived and there are workarounds for it. For example:
class Foo
@bar : Int32
def initialize(bar)
@bar = bar.to_i
end
end
But as I see it, even this has some issues.
First of all it’s not typed, and instead relies on the compiler to do almost exactly what I’m proposing and decided if the type being passed to Foo.new
has the method #to_i
. This means that documentation either has to be explicitly written to say what the type of bar
is, or that type just won’t be documented.
This also may or may not put extra strain on the compiler. I don’t know, but I do feel like specifying ahead of time what methods the compiler should expect to find on a type should make things easier for it.
Conclusion
This is something I myself have needed many times, and I’m sure others have to. I don’t know that this is the right direction to go over what was proposed in #8511, but I do know that Crystal needs something like this. Strong typing in a language is already somewhat of a barrier to adoption, especially to our userbase which is mainly Ruby developers that are frustrated with how slow and error prone it is. Mostly though, I’m putting this here because @asterite has a tendency to make awesome PRs based on proposals like this and I’m really hoping he decides to make a PoC for this