crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.43k stars 1.62k forks source link

Methods with a return type of `self` automatically return `self` #14907

Open jwoertink opened 2 months ago

jwoertink commented 2 months ago

This would be sort of a quality of life improvement. When creating an API that uses some sort of builder pattern where you want to chain multiple methods, you will generally return self on all of these methods. However, this means that the last thing in every one of these methods has to be the self keyword...

class ThingBuilder
  def with_name(name : String) : self
    add_field("name", name)
    self
  end

  def with_type(type : String) : self
    add_field("type", type)
    self
  end
end

builder = ThingBuilder.new
builder.with_name("Thing")
  .with_type("Type")
  # and possibly a lot more...

But what if you didn't have to add the self in the method?

class ThingBuilder
  def with_name(name : String) : self
    add_field("name", name)
  end

  def with_type(type : String) : self
    add_field("type", type)
  end
end

This pattern sort of already exists in Crystal when you set the return type to Nil. We don't have to add nil as the last thing in a method, Crystal already knows to just return nil.

# returns nil not 123
def number : Nil
  123
end

And of course, since you can't set the return type of self in a global scope, you'd still get the "there's no self in this scope" error anyway.

I have no idea if this would even be possible, so if not, or if it would be more effort than worth, then feel free to close.

Thanks :raised_hands:

straight-shoota commented 2 months ago

I recognize the appeal of this, but unfortunately, it's not an easy feat because semantics are in the way.

self in the type grammar is a convenient reference to the name of the type that the current scope is in. But it does not necessarily indicate the same instance as self means in an instance scope.

That's obvious from usage of self as type restrictions of parameters (#==(other : self) : Bool) or return type of class methods (.new : self).

For example, Slice#sort has a return type restriction of self, but returns a different instance:

def sort : self
  dup.sort!
end

To be fair, in some places the return type restriction appears to be used to indicate the same instance where methods returning a different instance (of the same type) would have the type name spelled out.

For example String#+:

def +(other : self) : String
  # ...
end

Here self is used as parameter type, but the return type is String. Maybe it's just a coincidence, but it could also be intentional to show that the returned value is a different instance 🤷

jwoertink commented 2 months ago

ah! Yeah, that's a good point. I didn't consider that you could have the same type but different instance, so this would end up being a potentially huge breaking change :grimacing: I do like the idea of using the type's actual name to indicate it's the same type but a different instance

stakach commented 2 months ago

maybe a new helper type def chainable : instance to differentiate from self