Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Allow generic instantiations with empty type vars everywhere #10204

Closed
HertzDevil opened this issue Jan 6, 2021 · 4 comments · Fixed by #11906
Closed

[RFC] Allow generic instantiations with empty type vars everywhere #10204

HertzDevil opened this issue Jan 6, 2021 · 4 comments · Fixed by #11906

Comments

@HertzDevil
Copy link
Contributor

It should be possible to instantiate generic types with 0 type vars everywhere. Currently, this is only doable in contexts where typeof is allowed:

Tuple(*typeof(Tuple.new)) # => Tuple()

Below are some contexts where the same splat trick isn't doable:

class C(*T); end
module M(*T); end

class Foo < C(*typeof(Tuple.new))        # not allowed
  include M(*typeof(Tuple.new))          # not allowed

  @x : C(*typeof(Tuple.new))             # not allowed

  def bar(x : Tuple(*typeof(Tuple.new))) # not allowed
  end
end

I propose that we allow a pair of empty parentheses to represent a generic instantiation with 0 arguments: (note that inheritance and module inclusion further require #3649, and instance variables probably need #8520)

Tuple().new            # => {}

class Foo < C()        # okay
  include M()          # okay

  @x : C()             # okay

  def bar(x : Tuple()) # okay
  end
end

I can't think of any syntactic ambiguities that would arise from this. Semantic-wise, T and T(...) already mean different things when T is generic, e.g. Tuple alone in a def type restriction matches all tuples regardless of their type vars, so allowing ... to be empty shouldn't be a huge impact.

This does not mean the same as allowing generic definitions to have 0 type vars. The following are still illegal:

class C(); end  # Error: must specify at least one type var
module M(); end # Error: must specify at least one type var

Thus generic instantiation with 0 arguments is only possible if the generic has a single splat parameter and no other non-splat parameters. (Not using splats doesn't help because instantiations must still refer to the empty Tuple type in that case, like C(typeof(Tuple.new)), just without a splat operator.)

@Daniel-Worrall
Copy link
Contributor

Can you demonstrate a practical use-case?

@HertzDevil
Copy link
Contributor Author

The most common scenario of empty splats in generic type arguments is the argument types of Proc, so all types that behave similarly to Proc will be affected by this. For example if I have a functor-like object where certain type arguments of the Proc are predetermined (e.g. the return type is ignored), it's natural to write code like below

class Window; end

class Callback(*Args)
  @fn : Proc(Window, *Args, Nil) # requires #8520
  # ...
  
  def initialize(@parent : Window, fn : Proc(Window, *Args, _))
    @fn = fn
  end

  def run(*args : *Args)
    @fn.call(@parent, *args)
  end

  def disconnect
    # @parent would be needed here
  end
end

Then it's impossible to use argless functors in some places:

class Window
  getter on_resize = [] of Callback(Int32, Int32)      # okay
  getter on_close = [] of Callback()                   # Error: unexpected token: )
  getter on_close = [] of Callback(*typeof(Tuple.new)) # Error: can't infer the type of instance variable '@on_close' of Window

  def resize(x : Int32, y : Int32)
    # do something
    on_activate.each &.run x, y
  end

  def close
    # do something
    on_close.each &.run # okay
  end
end

wnd = Window.new
cb = Callback.new(wnd, ->(w : Window) { puts "closing" })
# Crystal is able to deduce the generic type arguments for `cb`,
# which are the empty splat, so having `Callback()` is entirely valid here
typeof(cb) # => Callback()
wnd.on_close << cb # okay

Not supporting empty type splats would make generics much harder to work with; generics with 0 type arguments are just a natural consequence of the type splat being the only type var of a generic type. Allowing empty parentheses fills in this syntactic gap.

@Sija
Copy link
Contributor

Sija commented Jan 6, 2021

And using just Callback wouldn't work?

@HertzDevil
Copy link
Contributor Author

HertzDevil commented Jan 7, 2021

Yes, because omitting the type vars isn't the same as providing 0 type vars. To illustrate more clearly:

x = [] of Callback                     # Error: can't use Callback(*Args) as generic type argument yet, use a more specific type
x = [] of Callback()                   # should be allowed
x = [] of Callback(*typeof(Tuple.new)) # okay

class Window
  getter on_close = [] of Callback     # Error: can't infer the type of instance variable '@on_close' of Window
  getter on_close = [] of Callback()   # should be allowed

  # not allowed
  getter on_close = [] of Callback(*typeof(Tuple.new))
end

Part of the confusion between T and T(...) is eliminated in #10206.

There is little that can be done on an [] of Callback anyway because every element would possibly take different argument types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants