If I have a function which accepts an argument of multiple types, how can I enforce that the return must match the value of the input?
This comes up particularly when I want a method to work with any children of a parent type. For demonstration, consider something that is "barlike":
abstract struct Barlike
property bar: Int32
def initialize(@bar); end
end
abstract def make_clang_sound
abstract def serve_drinks
end
Now any struct can implement those two methods, and store that value
struct Bar1 < Barlike
def make_clang_sound
puts "bing bang bong"
end
def serve_drinks
puts "your drink sir"
end
end
struct Bar2 < Barlike
def make_clang_sound
puts "kling klang"
end
def serve_drinks
puts "here are your drinks"
end
end
Now what if I have a method that wants to use the bar and return a new one with an updated value (these are structs afterall):
def foo(arg : Barlike)
new_bar = arg.bar + 2
arg.class.new(new_bar)
end
this will return a Bar1
if a Bar1
is passed in and a Bar2
if that is passed in but it's not guaranteed:
def foo(arg : Barlike)
"howdy"
end
I'm going to be putting my foo
into an abstract structure as well, so I need to guarantee that implementers of foo
return the same type of Barlike
that was given.
I tried
def foo(arg : Barlike) : arg.class
end
But that's a compile time error (arg cannot be used there like that)
I also tried
def foo(arg : Barlike) : typeof(arg)
end
which passes, but typeof here is just Barlike whereas I really need it to be only the thing that was passed in, only Bar1 or Bar2 and so on.
Can macros help?
The tool for this are free variables. That's essentially generics scoped to a single method.
# This method returns the same type as its argument
def foo(arg : T) : T forall T
arg
end
This would already solve the main part of your question.
However, it is currently not possible to apply type restrictions to free variables, for example restricting T
to Barlike
.
There are workarounds, though:
def foo(arg : T) : T forall T
{% raise "arg must implement Barlike" unless T < Barlike %}
arg
end
def foo(arg : T) : T forall T
foo_impl(arg)
end
private def foo_impl(arg : Barlike)
arg
end
Both workarounds affect the implementation of the method. There is no way to specify such a type restriction for a abstract def
. Number 2 might be feasible, if you make foo_impl
abstract and require inheriting classes to implement this one, instead of foo
.
But it's probably also fine to just go with the initial example using free variables, without the Barlike
restriction. In practice, you probably don't gain much.