Recently, Ruby introduces a new method definition syntax called endless method definition (ticket and pull request):
def inc(x) = x + 1
def square(x) = x * x
p square(inc(1)) # => 4
It seems good to me. So, I'd propose to add this feature to Crystal.
Thank you.
I don't see the benefit, compared to:
def inc(x); x + 1 end
def square(x); x * x end
Saving 1/2 lines for another, rare syntax to learn, which is more limited than the exisiing one? Don't worth it, and even harmful.
That's another example of Ruby "syntax multiplication", both syntax will end up being mixed to do the same thing:
def inc(x) =
x + 1
def inc(x)
x + 1
end
However it is clearer it returns a value.
In general, I don't think this is worth it, mostly because of what @j8r pointed out: trends of "python-style" method definitions and managing consistency. Mixing the two doesn't seem nice.
But, I actually wouldn't mind this for delegations (without using delegate
macro)
def foo(x) = @inner.foo(x)
def bar(y) = @inner.bar(y)
I can't see myself using it outside of this though. Perhaps it would be nice for mathematical related code that often uses small methods to give names to expressions.
I think it's nice for things like:
def default_value = nil
def default_count = 0
stuff like that. That said, it's just a new style and then there will be comments in PRs and reviews saying that some stuff should be in one line, not with end
, etc., so I'm not sure it's a good idea to add more styles into the mix.
I like the aesthetics.
Still no word whether it will be adopted by Ruby
Matz proposed experimenting with it
https://bugs.ruby-lang.org/issues/16746#note-16
to see if fits.
The percentage of one liners methods is probably higher in Ruby,
is there an easy way in Crystal to get statistics on average method size of a given code base?
This format was adopted in Scala over a decade ago, and I think Ruby's PR was inspired by it.
https://docs.scala-lang.org/style/declarations.html
def add(x: Int, y: Int): Int = x + y
It is often the case that the logic itself only needs one line, such as specifying a default value or defining a simple function. But now it's four lines due to a boilerplate called def
, end
and line break.
https://github.com/crystal-lang/crystal/blob/master/src/array.cr#L2066-L2072
private def check_needs_resize
double_capacity if @size == @capacity
end
private def double_capacity
resize_to_capacity(@capacity == 0 ? 3 : (@capacity * 2))
end
private def check_needs_resize = double_capacity if @size == @capacity
private def double_capacity = resize_to_capacity(@capacity == 0 ? 3 : (@capacity * 2))
I agree with the introduction because I feel that having this format increases the amount of information on one screen and makes it more readable.
Furthermore, I hope that this format will be a catalyst for the further evolution of future Crystal, such as the First class and Higher order Functions, Currying and Partial Functions.
@maiha I disagree on your examples being an improvement. To me this seems much less readable when you have lots of things packed into a single line.
I can see the appeal for really short method bodies consisting only of a simple expression. But as soon as conditionals come into play, I don't see no benefit.
Just as an example, I need to scroll to the right to see the full definition under "RFC", but in the previous code snippet everything is clear at a glance.
I'm ambivalent. I probably wouldn't use this myself, I find the four lines (including spacing) which a small method takes up perfectly fine.
I'm worried about this introducing another "style" variable though. Now you have two ways of writing trivial methods. And style problems can get quite contentions and cause unnecessary friction, as we've seen recently.
I don't want to shoot this down, but we don't need to introduce every ruby feature either.
I feel like this would fall slightly in to the category of an "alias" since it doesn't change any sort of functionality, and Crystal tries to avoid having aliases. One thing that's nice in crystal is by having 1 way to do things, all code starts to look similar. As a teacher that teaches brand new people how to code, trying to explain multiple different syntaxes to do the exact same thing to students is really difficult.
Having multiple developers working on the same code base over the course of years could lead to code mismatched, and even having the formatter to support would just mean that you couldn't auto fix to one way or the other.
I also like what @RX14
we don't need to introduce every ruby feature
It's great that we're "inspired" by Ruby, but we don't need to just be another Ruby.
Yes, I wrote bad example.
For example, when empty?
has already been defined, I don't want to use 4 lines.
def any?
!empty?
end
def any? = !empty?
I think it's more of a syntax sugar category than an alias category.
For example, we should avoid having size
and count
that work the same way. Yep, I agree it!
But I think it makes sense that there's another way to write something simple in a concise way.
def name
@name
end
getter name
Should we remove the getter
? No. Because it's simple and intuitive.
Like this, simple logic wants to be expressed in simple terms.
Ok, I can see that. That makes sense. What about setter methods though?
class Person
@name : String?
def name = @name
def name=(@name)
end
Where the first one is the getter, and the second one is the setter... These two are super close and could lead to beginning programmers getting lost and confused.
The latter would correctly look like this.
def name=(@name)
end
# or RFC style (This case gets complicated in reverse.)
def name=(v) = @name = v
It can't be helped that the endless and the equal method are similar.
I ran this Ruby script under Crystal repo and result is commented:
files = Dir["src/**/*.cr"].map { |path| File.read(path) }
simple_def_count = files
.map { |content| content.scan(/^(\s*)def.*\n.*\n\1end$/).size }
.sum
p simple_def_count # => 4287
all_def_count = files
.map { |content| content.scan(/^(\s*)def.*\n(.|\n)*?\n\1end$/).size }
.sum
p all_def_count # => 9618
4287/9618 = 0.45. In short, most half (45%) methods are potentially simplified by this RFC.
And, by @mame's research (here), Ruby's one is 24%.
That's pretty neat to see those stats! That's also a testament to the language and all the hard work everyone has put in to see that we can write half the language in such a simple way.
In Lucky, we also wanted a way to clean up some of the small methods, so we have a macro to handle doing that.
Maybe we could add that macro to the language instead? That would solve the issue of shrinking up the codebase, give us syntactic sugar for small definitions, and not impact the compiler parsing at all.
I ran this Ruby script under Crystal repo
Heresy!
(just kidding)
Thank you for the stats!
I don't think the stats are correct. For example here are some results that that script considers "one liners":
def total
utime + stime + cutime + cstime
end
def to_s(io : IO) : Nil
io.printf " %.6f %.6f %.6f ( %.6f)", utime, stime, total, real
end
def human_mean
mean.humanize(precision: 2, significant: false, prefixes: Number::SI_PREFIXES_PADDED).rjust(7)
end
def step(*, to = nil, by = 1)
StepIterator.new(self + (by - by), to, by)
end
def sign
self < 0 ? -1 : (self == 0 ? 0 : 1)
end
def clone
Range.new(@begin.clone, @end.clone, @exclusive)
end
def number?
ascii? ? ascii_number? : Unicode.number?(self)
end
def initialize(@target : String, tolerance : Int? = nil)
@tolerance = tolerance || (target.size / 5.0).ceil.to_i
end
def read_abbreviations(io)
@abbreviations = Abbrev.read(io, debug_abbrev_offset)
end
def inspect(io : IO) : Nil
io << "#{self.class.name}(type=#{type}, name=#{name.inspect}, sect=#{sect}, desc=#{desc}, value=#{value})"
end
def values_at(*columns : Int)
columns.map { |column| row_internal[column] }
end
def self.each_row(string_or_io : String | IO, separator : Char = DEFAULT_SEPARATOR, quote_char : Char = DEFAULT_QUOTE_CHAR)
Parser.new(string_or_io, separator, quote_char).each_row
end
Now written with the endless method syntax:
def total = utime + stime + cutime + cstime
def to_s(io : IO) : Nil = io.printf " %.6f %.6f %.6f ( %.6f)", utime, stime, total, real
def human_mean = mean.humanize(precision: 2, significant: false, prefixes: Number::SI_PREFIXES_PADDED).rjust(7)
def step(*, to = nil, by = 1) = StepIterator.new(self + (by - by), to, by)
def sign = self < 0 ? -1 : (self == 0 ? 0 : 1)
def clone = Range.new(@begin.clone, @end.clone, @exclusive)
def number? = ascii? ? ascii_number? : Unicode.number?(self)
def initialize(@target : String, tolerance : Int? = nil) = @tolerance = tolerance || (target.size / 5.0).ceil.to_i
def read_abbreviations(io) = @abbreviations = Abbrev.read(io, debug_abbrev_offset)
def inspect(io : IO) : Nil = io << "#{self.class.name}(type=#{type}, name=#{name.inspect}, sect=#{sect}, desc=#{desc}, value=#{value})"
def values_at(*columns : Int) = columns.map { |column| row_internal[column] }
def self.each_row(string_or_io : String | IO, separator : Char = DEFAULT_SEPARATOR, quote_char : Char = DEFAULT_QUOTE_CHAR) = Parser.new(string_or_io, separator, quote_char).each_row
Now, I'm not saying adding endless method is a bad idea, it's just that the places where it will actually be applied are not that many. I can imagine for integer literals, nil
, maybe simple sums or computations, or delegation.
You can append newline after =
. Please keep this in mind.
I ran this imperfect command: grep -A2 'def ' $(find src -name "*.cr") | grep -c '\.cr-.*end'
. It returns 5983
.
As @asterite , it may be more.
I find personally the syntax less readable, imagine:
def [x, y]= = @hash[x] = y
An option is to use a a block:
def [x, y]= { @hash[x] = y }
It is closer with the Proc
syntax. I may be a good thing or it may add confusion. This might also mean that def [x, y]= do @hash[x] = y end
is also valid (?!), which reminds Elixir.
You can append newline after =. Please keep this in mind.
You are right.
To tell you the truth, I've been applying this to some of the existing codebase and it does look less noisy, specially if you can put the expression on a separate line.
In the end, less code and less lines to read, if it not makes things more cryptic (and this is not one of those cases if used well) is good.
So ๐ from me on this feature.
This is pretty subjective, but I like how it looks when it's a simple method like:
def inc(x) = x + 1
But I don't like theses cases:
def inc(x) =
x + 1
def foo=(x) = @foo = x
def foo(x) = @foo = x
def foo(x) = puts("Hi!"); x + 1
In other words, if this syntax is adopted I think I would restrict it to:
=
end
specially if you can put the expression on a separate line
We have a conflict of opinions ๐
We have a conflict of opinions ๐
True :-)
I was just looking at char.cr
:
# Returns `true` if this char is a number according to unicode.
#
# ```
# '1'.number? # => true
# 'a'.number? # => false
# ```
def number?
ascii? ? ascii_number? : Unicode.number?(self)
end
# Returns `true` if this char is a lowercase ASCII letter.
#
# ```
# 'c'.ascii_lowercase? # => true
# 'รง'.lowercase? # => true
# 'G'.ascii_lowercase? # => false
# '.'.ascii_lowercase? # => false
# ```
def ascii_lowercase?
'a' <= self <= 'z'
end
# Returns `true` if this char is a lowercase letter.
#
# ```
# 'c'.lowercase? # => true
# 'รง'.lowercase? # => true
# 'G'.lowercase? # => false
# '.'.lowercase? # => false
# ```
def lowercase?
ascii? ? ascii_lowercase? : Unicode.lowercase?(self)
end
With this change, not allowing newlines it could be changed to:
# Returns `true` if this char is a number according to unicode.
#
# ```
# '1'.number? # => true
# 'a'.number? # => false
# ```
def number? = ascii? ? ascii_number? : Unicode.number?(self)
# Returns `true` if this char is a lowercase ASCII letter.
#
# ```
# 'c'.ascii_lowercase? # => true
# 'รง'.lowercase? # => true
# 'G'.ascii_lowercase? # => false
# '.'.ascii_lowercase? # => false
# ```
def ascii_lowercase? ='a' <= self <= 'z'
# Returns `true` if this char is a lowercase letter.
#
# ```
# 'c'.lowercase? # => true
# 'รง'.lowercase? # => true
# 'G'.lowercase? # => false
# '.'.lowercase? # => false
# ```
def lowercase? = ascii? ? ascii_lowercase? : Unicode.lowercase?(self)
but I feel the lines become too long. The method bodies are pretty simple so if we write them like this:
# Returns `true` if this char is a number according to unicode.
#
# ```
# '1'.number? # => true
# 'a'.number? # => false
# ```
def number? =
ascii? ? ascii_number? : Unicode.number?(self)
# Returns `true` if this char is a lowercase ASCII letter.
#
# ```
# 'c'.ascii_lowercase? # => true
# 'รง'.lowercase? # => true
# 'G'.ascii_lowercase? # => false
# '.'.ascii_lowercase? # => false
# ```
def ascii_lowercase? =
'a' <= self <= 'z'
# Returns `true` if this char is a lowercase letter.
#
# ```
# 'c'.lowercase? # => true
# 'รง'.lowercase? # => true
# 'G'.lowercase? # => false
# '.'.lowercase? # => false
# ```
def lowercase? =
ascii? ? ascii_lowercase? : Unicode.lowercase?(self)
it looks a bit better, and because there's no end
the reader can feel (or at least I can feel) that the methods are simple. And in the end you end up with less lines and less noise (the =
doesn't add noise because it's obvious after def
comes the method definition).
The relation between this syntax and plain-old def
looks very similar to the relation between ternary operator and if
expression.
The first one is not necessary in the language absolutely, because all cases are covered by the latter. However ternary operator dramatically improves codes sometimes.
@asterite maybe we should have inspired on Python syntax then ๐คฃ
The problem with Python is that you can't write a formatter for it because you are forced to keep the indentation.
Another thought that came to my mind: I guess this can't be applied to macros.
If we're going to put restrictions on what the right hand side expression can be, isn't that just admitting this is too easy to abuse?
If that's the case, I'd rather not have it. Nobody has complained about the current syntax until now! If we're not sure it's a good idea, better to not have it.
I feel this is going to be an endless discussion...
@RX14 well, nobody complained about Crystal not existing, and here we are... ๐
Don't worry, we're not going to carelessly add more syntax. I agree that having too many restrictions that look arbitrary sounds like a red flag.
This is just syntax sugar that other languages already have, and maybe we'd like to adopt something similar.
Let's just get rid of end
all, shall we?
class Nop < ASTNode
end
# The nil literal.
class NilLiteral < ASTNode
end
# A bool literal.
class BoolLiteral < ASTNode
end
class Nop < ASTNode
class NilLiteral < ASTNode # The nil literal.
class BoolLiteral < ASTNode # A bool literal.
Oh, I feel so refreshed. ๐
well, nobody complained about Crystal not existing, and here we are...
Well I'm super glad we are here!
well, nobody complained about Crystal not existing, and here we are...
Well I'm super glad we are here!
Yeah, I didn't know Crystal was the missing language for me before trying it...
At least the Crystal creators did complain that the language didn't exist โ otherwise why not staying with the status-quo on Ruby? :)
I like this approach. I admit I did not read all of it, but we have a number of places in Lucky where this kind of thing would be used all the time. We even have a macro called quick_def
because we have certain patterns where this would be helpful. The biggest is when defining a page_title
on HTML pages:
class MyPage < MainLayout
def page_title = "my title" # This would be awesome
quick_def :page_title, "my title" # The macro Lucky has now
end
Also great for delegation, aliasing, simple string return like this. I think it is nice :) ๐
I also personally feel this is pretty intuitive to read, but that is just my opinion
And before anyone suggests Lucky does something different like a CONSTANT or something...we've considered a lot and instance methods are kinda the only thing that works. This methods allow access to instance vars like
"#{@current_user.name} - Profile"
. Constants can't do that and neither cangetter
(I tried but they did not work with the way we did abstract classes
It may be a bit uglier, but using valid, standard Crystal is shorter than quick_def
:
quick_def :page_title, "my title"
def page_tile; "mytitle"; end
The point is: we don't like semicolon (ok, fair, Crystal is not C-like) and end
(hey, we are on a Ruby-inspired language!).
So the only problem here is semicolons?
Pretty much that that looks bad and is harder to read in my opinion
So yes it is mostly aesthetic, but I think that is important ๐คทโโ๏ธI also find def page_tile; "mytitle"; end
hard to parse and just doesn't look right, but once again that could just be me.
I would love to at least have the option to do the "endless method defitinion"
I think the difference is just how much noise there is between the two styles. I think the = alternative has a bit less noise.
I'm not sure why we need this as a language feature. Wouldn't this just be like this macro:
macro endless(assign)
def {{assign.target}}
{{assign.value}}
end
end
class Foo
def initialize(@target : String)
end
endless hello_world = "Hello #{@target}"
end
I suppose the only reason for being a language feature is overloading def
?
Does that work with methods with arguments?
@straight-shoota I'd prefer a language feature because it is more flexible (args as asterite mentioned)
But I do like that idea in the meantime. I may use that instead of quick_def since the syntax is nicer. Maybe I'll call it qdef
:D
No it doesn't accept arguments. But I'm not sure whether that's actually necessary/useful. Might be a feature to force keeping it simple, not a bug =)
The latest examples didn't use any. And from the examples with arguments I didn't find any that I would've particularly liked/considered useful.
That's a good point. Probably not that necessary to accept args
what about introducing a block like syntax?
def default_value { 10 }
def foo(x) { x + 100 }
I count number of one-line body methods and such methods having arguments in Crystal repo.
files = Dir["src/**/*.cr"].map { |path| File.read(path) }
simple_def_count = files
.map { |content| content.scan(/^(\s*)def.*\n.*\n\1end$/).size }
.sum
p simple_def_count # => 4301
simple_arg_def_count = files
.map { |content| content.scan(/^(\s*)def.*\(.*\).*\n.*\n\1end$/).size }
.sum
p simple_arg_def_count # => 2502
2502/4301 = 0.58. 58% one-line body methods have arguments. For good examples of such methods:
src/nil.cr
:
# Returns `true`: `Nil` has only one singleton value: `nil`.
def ==(other : Nil)
true
end
# Returns `true`: `Nil` has only one singleton value: `nil`.
def same?(other : Nil)
true
end
# Returns `false`.
def same?(other : Reference)
false
end
# See `Object#hash(hasher)`
def hash(hasher)
hasher.nil
end
We can rewrite the above by using this RFC:
# Returns `true`: `Nil` has only one singleton value: `nil`.
def ==(other : Nil) = true
# Returns `true`: `Nil` has only one singleton value: `nil`.
def same?(other : Nil) = true
# Returns `false`.
def same?(other : Reference) = false
# See `Object#hash(hasher)`
def hash(hasher) = hasher.nil
@wontruefree This means being more C-like, nothing will stop to do:
def foo(x) {
x + 100
}
Got this code:
class Object
macro cdef(name, *args, &block)
def {{name.id}}({{args.splat.id}})
{{block.body}}
end
end
end
cdef(hey) { "Hey" }
cdef mul, x : Int32, y : Int32 { x * y }
puts hey #=> Hey
puts mul(2, 3) #=> 6
With annotations, API docs arguments types are properly present.
Remains to add some logic for the method return type.
I am not that happy with this implementation, since it supports C-like definitions.
On the other hand, it is a simple macro.
I collect my thoughts and previous discussions for now as RFC again.
Please read it.
If you think this is too long, please read the below "Conclusion" section at least.
This proposal introduces a new syntax named "endless method definition".
An "endless method definition" is just synax sugar for one-line body method.
For example, we have the following inc
method definition:
def inc(x)
x + 1
end
Then, this proposal allows to rewrite it to
def inc(x) = x + 1
All method definition options (scope specifier, arguments, return type restriction and annotation) are also allowed by endless method definition.
When these options are consumed, and =
is appeared, then this method definition is parsed as endless method definition. The following expression to =
and whitespaces (containing newlines) becomes body of this.
For example, this is full example of endless method definition:
@[AlwaysInline]
protected def self.ascii_char?(c : Char) : Bool =
0 <= c.ord <= 0xFE
We can write a macro for short definition easily.
But such macro is not clearer than this syntax.
Indeed 45% of Crystal methods body are one-line.
However all of these doesn't accept this syntax.
For example, we shouldn't apply this syntax to the following example:
class Nil
def to_s(io)
io << "nil"
end
end
Because this method is operative.
On the other hand, we should apply this syntax to the following example:
class Nil
def same?(x : Nil)
true
end
end
Because it returns a value and, this body expression is one-line and deadly simple.
Then, we can rewite it using this RFC:
class Nil
def same?(x : Nil) = true
end
Good!
However, we can think it is ambiguous that there are two ways for doing one thing.
IMHO, the last example is clearer than before, and this syntax is more descriptive that it returns a value.
We can distinguish between operative one-line method and just a value method by this syntax. It is benefit.
We now consider uses =
as delimiter of endless method definition.
However other delimiters can be considerable.
For example:
# Hash syntax analogy
def inc(x) => x + 1
def inc(x): x + 1
# keyword version
def inc(x) return x + 1
I think =
is the best, but such discussion is missing for now.
This syntax conflicts setter method naming in our mind. (The parser can distinguish them of course.)
# `=` is appeared twice
def foo=(x) = x
# more bad example
def foo= = x
I think such examples are too arbitrary, and special rule makes us confusing.
However we can consider an option disallowing such naming in this synax.
I don't explain what expression is allowed as right-hand side of endless method, and I think this is largest discussion point of this RFC.
IMHO, we should not allow suffix keywords in this expression.
It may confuse us easily, and its behavior is unclear if suffix condition is not satisfied (if
/unless
case).
# This
def first? = self[0] unless self.empty?
# ... is consfusing which is parsed as:
(def first? = self[0]) unless self.empty?
# or as:
def first? = (self[0] unless self.empty?)
Moreover I think parsing from ternary expression level is better (it is a bit technical, sorry). This has some benefits:
For example, the following examples are INVALID if the above is accepted:
# Error: unexpected keyword 'unless'
def first? = self[0] unless self.empty?
# Error: unexpected token '='
# (We should show more detailed error message like "please wrap (...)"?)
def foo=(x) = @foo = x
On the other hand, the following examples are VALID:
def first? = (self[0] unless self.empty?)
def foo=(x) = (@foo = x)
Note that p (self[0] unless self.empty?)
is already valid expression, please.
We needs to get agreements with others that:
We needs to decide how do about:
=
as delimiter, or use other delimiter."setter=
name in this syntax."When the above TODO lists are all satisfied, I will create a new Pull Request. (cc @asterite)
Thank you!
@MakeNowJust Fantastic and very clear writeup:
=
setter
in most cases anywadef/end
def test = (
a = 0
a += 1
a += 2
)
Should be forbidden, it defeats the purpose of having small methods.
If this is accepted, then what stops to have C-style curl-braces functions... The syntax is very close.
@j8r I see no problem with that.
To reply to @MakeNowJust points:
@MakeNowJust Fantastic and very clear writeup:
quick_def
or qdef
. If people want a macro like that, and think that's fine, then using def
is better because def
is already a thing.=
is best.def foo= = 1
makes no sense because methods that end with =
require an argument, so you'll have def foo=(@foo) = ...
which isn't confusing. I know you can define a method named foo=
without arguments, it's just a bit useless in my opinion.if
and unless
just fine because methods can't be conditionally defined like that in Crystal. No ambiguity at all.@asterite However argument default value expression does not accept trailing if
and unless
(e.g. def foo(x = 1 if true); x end
invokes syntax error expecting token ')', not 'if'
). Why?
So, I'd suggest another idea: parsing right-hand side expression of endless method is same as right-hand side expression of assignment (and it is same as argument default value expression.)
I think sharing same rule on parsing right-hand side between x = exp
and def x = exp
is intuitive enough.
One good thing about being as close to another language as Crystal is to Ruby, is that in some cases it may be perfectly fine to let them implement stuff, and then we can look at adoption and see if
a: the feature is used
b: we like the overall effect of the feature and how it is used.
So I'd propose to wait and see the fallout in Ruby as I don't feel the problem it solves is big enough to rush it.
In fact Ruby's one was alternated yesterday. It is good to wait until Ruby's specification is fixed.
I like those ruby changes minus the one about requiring parens even if there are no args. Looks weird and very unruby like. Thanks for linking
Yeah. I think that def count = 0
or def empty? = true
was going to be most used use-case, but now I don't know how will use that... or, well, I guess people will use it with parens, but that adds a lot of noise.
Then again, we don't have to do what Ruby does.
Yes so let's not ๐
Let's not make def
the only difference between variable assignment and defining a function
could we use another keyword like fn
.
I was initially resistant to this idea but after doing some more documentation of code I think this could be a nice feature.
I think "don't have to do what Ruby does" does not mean "discard this RFC".
We should accept Ruby's good things and remove bad things.
One reason this makes more sense to me than ruby is that we have type overloads in crystal. A an overloaded constructor often looks like this:
class Foo
def initialize(@s : String)
end
def initialize(s : Int32)
new(s.to_s)
end
# or, with RFC
def initialize(s : Int32) = new(s.to_s)
end
That makes a lot of overoladed initializers easier to write
@robacarp As a general note, constructor overloads should better be defined on self.new
, not initialize
.
And I have some doubts regarding the practical usefulness of your argument. Considering realistic use cases, you'll often find: more than a single argument, expressive argument names, longer type paths (maybe also union types), default values and longer conversion methods. Thus method signatures in the real world are usually considerably longer than your artificial example.
It may look nice, but the actual benefit is neglible. And it would be a pity if the effect is to try to fit as much in a single endless method definition that the argument definitions are less expressive and line lenght increase dramatically.
Overloading needs a bit complex method header (signature) in fact. However this syntax works for a simple method body (not for header), and I think overloading method body is simple in many case, so the actual benefit is nice too.
was there any resolution on whether this is happening? i kind of agree with @robacarp but for exactly what @straight-shoota mentions - initialize
overloads are better done with self.new
, which, for a lot of what i've been doing lately, relegates initialize
to a zero line method definition
e.g.
class CustomThing
getter str : String
getter num : Int32
getter char : Char
# this has always looked weird to me, but is usually the right answer
def initialize(@str, @num, @char)
end
end
where i would greatly prefer
class CustomThing
getter str : String
getter num : Int32
getter char : Char
# no `=` necessary.
def initialize(@str, @num, @char)
end
this use-case does seem like it's easily solved with a macro, unlike some of the other examples in this thread, but that feels even weirder than a zero line method definition.
...plus it just seems like something a curious reader would have to look up only to be disappointed when they peek under the hood ๐คฃ
@MakeNowJust can you elaborate on why we wouldn't use the endless method syntax for the following?
class Nil
# original method:
def to_s(io)
io << "nil"
end
# endless method:
def to_s(io) = io << "nil"
end
Because this method is operative.
As in it performs an operation instead of returning a value? Why should that be excluded from this syntax?
I have seen an interesting syntax from Elixir, which also takes inspirations from Ruby:
defmodule Math do
def sum(a, b) do
a + b
end
end
This syntax, do...end
, is the block syntax in Crystal. What if we consider that method declaration is related to them, with the do
omited? Then, as proposed by @wontruefree, it would be logical to have:
def sum(a, b) { a + b }
# Maybe even
class Error < Exception { }
We can say curly braces look ugly, but they are already used for block syntax. The good point is it won't really be an entirely new syntax.
Here is another example:
class Klass
# Already valid, memoization
getter name : String { "klass" }
def mul : Int32 { @num * 2 }
def initialize(@num : Int32) { }
end
Finally, compared to the other main proposal def mul = @num * 2
, no surprises/ambiguations with methods like mul=
or []=
or with anything inside the method โ the usual rules for blocks with curly braces applies.
Note: I think this whole proposal โ single line methods โ is more a quality of life than a necessity, we can take time to discuss. This shouldn't of course impact Crystal 1.0. We have lived and can live fine without ๐
Most helpful comment
I feel this is going to be an endless discussion...