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.
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.
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.
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