KCE
KCE

Reputation: 1209

How to force return value to be same type as argument value in Crystal?

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?

Upvotes: 1

Views: 86

Answers (2)

Samual
Samual

Reputation: 188

Here is something that works:

{% for sub in Barlike.subclasses %}
  struct {{sub}}
    def foo() : {{sub}}
      {{sub}}.new(@bar+1)
    end
  end
{% end %}

full example

But it feels like this is trying to solve the wrong problem. It uses the #subclasses macro to generate a foo for all of the child structs.

You could also declare these as self methods inside the abstract class: example.

Upvotes: 1

Johannes M&#252;ller
Johannes M&#252;ller

Reputation: 5661

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:

  1. Use a macro to validate the argument type:
def foo(arg : T) : T forall T
  {% raise "arg must implement Barlike" unless T < Barlike %}
  arg
end
  1. Delegate to another method with a type restriction:
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.

Upvotes: 3

Related Questions