Proc and return type restrictions

Hi,

GTK4 has a lot of GObject interfaces that are mapped to Crystal modules in the bindings, but it also has a lot of callbacks that receives that interfaces as parameters or return types, so I create aliases for these callbacks like:

alias TreeListModelCreateModelFunc = Proc(GObject::Object, Gio::ListModel?)

Where Gio::ListModel is a interface (i.e. a Crystal module).

The problem rises when I try to use functions that receive these callbacks as parameters… summarizing the problem in plain Crystal code:

module Interface
end

class Implementation
  include Interface
end

alias Callback = Proc(Interface?)

def callback_impl : Interface?
  # If I comment the line above it also doesn't works.
  Implementation.new if rand < 0.5
end

def callback_caller(callback : Callback) : Interface?
  callback.call
end

callback_caller(->callback_impl)

This error out with:

expected argument #1 to 'callback_caller' to be Proc((Interface | Nil)), not Proc((Implementation | Nil))
Overloads are:
 - callback_caller(callback : Callback)

I remember that return type restrictions as the name says are just restrictions… basically used only to generate error messages, not to define the return type.

So the only way to fix that is rewriting the callback with a ugly cast:

def callback_impl : Interface?
  # If I comment the line above it also doesn't works.
  Implementation.new.as(Interface) if rand < 0.5
end

Now the question :sweat_smile:

What was the reason to keep return type restriction just a… restriction, not the real method return type, and what would be the consequences of changing this (besides API breakage).

You should be able to use callback_caller(-> : Interface? { callback_impl })

1 Like

I think you need an explicit upcast in the return expression of the callback implementation.

This is similar to what we have in crystal-db. Refactor connection factory by bcardiff · Pull Request #181 · crystal-lang/crystal-db · GitHub should offer another alternative that may apply in your use case.

Coming back to this question, I think the main reason is that when the compiler knows the actual type is Implementation?, it does not need to worry about generating code for other implementations of Interface. This results in smaller executable size and better performance and usually is completely fine.

1 Like