Feature #16746
closedEndless method definition
Description
Ruby syntax is full of "end"s. I'm paranoid that the ends end Ruby. I hope Ruby is endless.
So, I'd like to propose a new method definition syntax.
def: value(args) = expression
As you see, there is no "end".
Examples.
def: hello(name) =
puts("Hello, #{ name }")
hello("endless Ruby") #=> Hello, endless Ruby
def: inc(x) = x + 1
p inc(42) #=> 43
x = Object.new
def: x.foo = "FOO"
p x.foo #=> "FOO"
def: fib(x) =
x < 2 ? x : fib(x-1) + fib(x-2)
p fib(10) #=> 55
Limitations.
-
def: foo x = x
is invalid; the parentheses for formal arguments are mandatory. -
private def: foo = x
is invalid; this method definition cannot be an method argument.
A patch is attached. No conflicts.
Files
Updated by shevegen (Robert A. Heiler) over 4 years ago
I'm paranoid that the ends end Ruby. I hope Ruby is endless.
With so many cancellations of events this year, we may not want to
want to jinx ruby too much for the 3.0 release this year because
then 3.0 itself may become the endless version! (Or we could skip
it, like PHP skipped a version ... I was briefly tempted to propose
this on this special day today, but I really don't want to jinx
it. I do, however had, remember matz joking last year about unforeseen
events ... now I hope I just did not jinx it!)
Updated by shyouhei (Shyouhei Urabe) over 4 years ago
- Related to Feature #5054: Compress a sequence of ends added
Updated by shyouhei (Shyouhei Urabe) over 4 years ago
- Related to Feature #5065: Allow "}" as an alternative to "end" added
Updated by shyouhei (Shyouhei Urabe) over 4 years ago
- Related to Feature #12241: super end added
Updated by mame (Yusuke Endoh) over 4 years ago
- File deleted (
endless-method-definition.patch)
Updated by mame (Yusuke Endoh) over 4 years ago
Updated by matz (Yukihiro Matsumoto) over 4 years ago
- Tags deleted (
joke)
Updated by mame (Yusuke Endoh) over 4 years ago
- Assignee set to nobu (Nobuyoshi Nakada)
- Status changed from Open to Assigned
Okay seriously. You should have written many "simple" method definitions that have only one expression:
# three-line style
def value
@val
end
# one-line style
def value; @val; end
Surprisingly, according to the following rough estimate of ruby/ruby code base, this kind of simple method definitions account for 24% of the entire method definitions.
# three-line style: 8570
p Dir.glob("**/*.rb").map {|f| File.binread(f).scan(/^(\s*)def.*\n.*\n\1end$/).size }.sum
# one-line style: 949
p Dir.glob("**/*.rb").map {|f| File.binread(f).scan(/^\s*def.*;[^;]*;\s*end$/).size }.sum
# all method definitions: 39502
p Dir.glob("**/*.rb").map {|f| File.binread(f).scan(/\bdef\b/).size }.sum
# propotion = (8570 + 949) / 39502 = 24%
I agree that def foo(a) = expression
is best, but I cannot implement it. Pass it to @nobu (Nobuyoshi Nakada) .
Updated by ka8725 (Andrey Koleshko) over 4 years ago
Updated by ruurd (Ruurd Pels) over 4 years ago
shevegen (Robert A. Heiler) wrote in #note-1:
I'm paranoid that the ends end Ruby. I hope Ruby is endless.
Like - basically - a snake biting its own tail amirite ;-)
Updated by nobu (Nobuyoshi Nakada) over 4 years ago
Updated by ioquatix (Samuel Williams) over 4 years ago
Have you considered adopting Python's whitespace sensitive indentation?
def hello(name):
puts("Hello, #{ name }")
hello("endless Ruby") #=> Hello, endless Ruby
Updated by mame (Yusuke Endoh) over 4 years ago
@nobu (Nobuyoshi Nakada) implemented def foo(arg) = expression
in one night. Also, it allows private def: foo = 42
. Perfect.
https://github.com/ruby/ruby/pull/2996
@matz (Yukihiro Matsumoto) We are ready to perform the experiment.
Updated by retro (Josef Šimánek) over 4 years ago
To be honest, I'm a little confused if this is serious proposal now, since it probably started as a joke and got some attention already at GitHub pull request (https://github.com/ruby/ruby/pull/2996) with mixed attitude.
Anyway for one-line method definitions I like part of https://bugs.ruby-lang.org/issues/5065 (originally at https://bugs.ruby-lang.org/issues/729, that was rejected because of invalid reason for the change request AFAIK). It looks "nature" to me since similar syntax is already available for blocks and often is used for one-liners. Ideally scope this syntax only for one-liners.
# original
def value; @val; end
# proposed - is that conflicting one?
def value { @val }
Alternatively, since the only use-case I have found in this issue is getter having different method and instance variable names, what about to support this out of attr_reader
(and all attr_*
friends)?
# few ideas
attr_reader :value, for: :val # reads as -> attribute reader "value" for instance variable "val", probably impossible for multiple definitions on one line
attr_reader value: :val # this would support multiple definitions on one line
Updated by osyo (manga osyo) over 4 years ago
Using the assignment operator like def value =
expects the following behavior.
value = 42
# Refer to variables outside scope
def hoge = value
hoge # => 42
However, currently it is error: undefined local variable or method
.
In the Endless method definition, the scope seems ambiguous, so I need to discuss whether to capture variables.
Updated by matz (Yukihiro Matsumoto) over 4 years ago
I'd like to experiment with this new syntax. We may find drawbacks in the future, but to find them, we need to experiment first.
Matz.
Updated by nobu (Nobuyoshi Nakada) over 4 years ago
- Status changed from Assigned to Closed
Applied in changeset git|e8f53692ca7d65c92bde6d5bc6a1aea492915d9f.
Endless method definition [Feature #16746]
Updated by retro (Josef Šimánek) over 4 years ago
I'd like to experiment with this new syntax. We may find drawbacks in the future, but to find them, we need to experiment first.
If this was merged as an experiment, shouldn't it raise warning on usage (same as pattern matching does)?
Updated by nobu (Nobuyoshi Nakada) over 4 years ago
It is not easy to control parsing time warnings, and bothers tests.
Updated by Eregon (Benoit Daloze) over 4 years ago
nobu (Nobuyoshi Nakada) wrote in #note-19:
It is not easy to control parsing time warnings, and bothers tests.
The exact same applies to pattern matching and yet we added a warning there.
Testing/suppressing the warning is an eval
away.
I think it's important for new experimental syntax to warn about it, especially when the right semantics are unclear.
My opinion is it should not capture, def
/class
/module
never captured outside scopes and that shouldn't change.
Updated by Eregon (Benoit Daloze) over 4 years ago
Is it intended to allow multi-line definitions in this style?
I think we should not, and only single-line expressions should be allowed.
I think that's not the original purpose of this ticket and rather an incorrect usage of this new syntax,
e.g., https://twitter.com/devoncestes/status/1256222228431228933
Updated by marcandre (Marc-Andre Lafortune) over 4 years ago
tldnr; I am against this proposal, it decreases readability, increases complexity and doesn't bring anything to the table (in that order)
I read the thread carefully and couldn't find a statement of what is the intended gain.
@matz (Yukihiro Matsumoto), could you please explain what you see are the benefits of this new syntax (seriously)?
We may find drawbacks in the future, but to find them, we need to experiment first.
I see many drawbacks already. Also, it is already possible to experiment with tools like https://github.com/ruby-next/ruby-next.
Here's a list of drawbacks to start:
- Higher cognitive load.
Ruby is already quite complex, with many nuances. We can already define methods with def
, define_method
, define_singleton_method
.
-
Requiring all text editors, IDE, parsers, linters, code highlighters to support this new syntax
-
Encouraging less readable code
With this endless method definition, if someone want to know what a method does, one has to mentally parse the name and arguments, and find the =
to see where the code actually starts. My feeling is that the argument signature is not nearly as important as the actual code of the method definition.
def header_convert(name = nil, &converter) = header_fields_converter.add_converter(name, &converter)
# ^^^^^^^^^^^^^^^^^^^^^^^^^^ all this needs to be scanned by the eye to get to the definition
# ^ ^^^ the first `=` needs to be ignored, one has to grep for ") ="
That method definition, even if very simple, deserves it's own line start, which makes it easy to locate.
If Rubyists used this new form to write methods in two lines, with a return after the =
, it is still harder to parse as someone has to get to the end of the line to locate that =
. After a def
the eye could be looking for an end
statement.
def header_convert(name = nil, &converter) =
header_fields_converter.add_converter(name, &converter)
# <<< hold on, is there a missing `end`? or is the indent wrong? Oh, no, the line ended with `=`
def more_complex_method(...)
# ....
end
I believe it is impossible to improve the readability of a one-line method as written currently. This new form can only make it harder to understand, not easier.
def header_convert(name = nil, &converter)
header_fields_converter.add_converter(name, &converter)
end
def more_complex_method(...)
# ...
end
If header_convert
ever need an extra line of code, there won't be a need to reformat the code either.
“Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. ...[Therefore,] making it easy to read makes it easier to write.”
― Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship
For these reason I am against this proposal and my hope is that is reverted and that we concentrate on more meaningful improvements.
Updated by Eregon (Benoit Daloze) over 4 years ago
Noteworthy is the current syntax in trunk is def name(*args) = expr
(and not def: name
), so there is no visual cue that this is a endless method definition except the =
which comes very late.
I agree with @marcandre (Marc-Andre Lafortune), I think it makes code just less readable and harder to maintain.
Updated by marcandre (Marc-Andre Lafortune) over 4 years ago
Eregon (Benoit Daloze) wrote in #note-23:
Noteworthy is the current syntax in trunk is
def name(*args) = expr
(and notdef: name
), so there is no visual cue that this is a endless method definition except the=
which comes very late.
Oh, my mistake. That syntax makes it even less readable! There are potentially =
already in the method definition...
I'm even more against it 😅
I edited my post.
Updated by zverok (Victor Shepelev) over 4 years ago
To add an "opinion perspective"... I, for one, even if not fascinated but at least intrigued by new syntax.
My reasoning is as follows:
- The most precious Ruby's quality for me is "expressiveness at the level of the single 'phrase' (expression)", and it is not "code golf"-expressiveness, but rather structuring language's consistency around the idea of building phrases with all related senses packed, while staying lucid about the intention (I believe that, as our "TIMTOWTDI" is opposite to Python's "there should be one obvious way...", this quality is also opposite to Pythons "sparse is better than dense")
- (I am fully aware that nowadays I am in a minority with this viewpoint: whenever the similar discussions are raised, I see a huge difference between "it can be stated in one phrase" vs "it can not", while most of my correspondents find it negligible)
- I believe that the difference of "how you write it when there is one statement" vs "...more than one statement" is intentional, and fruitful: "just add one more line to 10-line method" typically small addition, but "just add one more line to 1-line method" frequently makes one think: may be that's not what you really need to do, maybe data flow becomes unclear? One existing example:
collection.map { |x| do_something_with(x) } # one line! cool!
# ...what if I want to calculate several other values on the way? I NEED to restructure it:
collection.map do |x|
log_processing(x)
total += x
do_something_with(x)
end
# ...which MIGHT imply that "I am doing something wrong", and maybe what would clearer express intent
# is separating calculation of total, and move `log_processing` inside `do_something`
- So, it was actually always felt for me as "a bit too wordy" for trivial/short methods to write it as
def some_method(foo)
foo.bar(baz).then { |result| result + 3 }
end
- ...alongside with "nicer formatting" of empty lines between methods, and probably requirement to document every method, and whatnot, it means that extraction of trivial utility method feels like "making MORE code than necessary", so I'd really try how it would feel with
def just_utility(foo)= foo.bar(baz).then { |result| result + 3 }
def other_utility(foo)= "<#{self}(#{foo})>"
def third_utility(foo)= log.print(nicely_formatted(foo))
- ...and I believe that "what if you need to add second statement, you will reformat the code completely?" is a good thing, not bad: you'll think twice before adding to "simple utility one-statement" method something that really, actually, maybe not belongs there.
Updated by mame (Yusuke Endoh) over 4 years ago
In the previous dev-meeting, matz said that it should be prohibited to define setter method with endless definition.
# prohibited
def foo=(x) = @x = x
There are two reasons:
- This code is very confusing and it is not supposed that a destructive operation is defined in this style.
- We want to allow
def foo=42
asdef foo() = 42
in future. It is difficult to implement it in short term, though. Ifdef foo=(x) = @x = x
is allowed once, it will become more difficult.
I will create a PR soon.
Updated by mame (Yusuke Endoh) over 4 years ago
Updated by pankajdoharey (Pankaj Doharey) about 4 years ago
Why do we even need a def
. ? Why not just do it like Haskell?
triple(x) = 3*x
Since this is an assignment if triple hasn't already been assigned it should create a function otherwise it should pose a syntax error. Function redefinition error or something.
or Perhaps a let
keyword ?
let double x = x * 2
Updated by etienne (Étienne Barrié) almost 4 years ago
I haven't seen it mentioned in the comments so I just wanted to point out that the regular method-definition syntax doesn't require semicolons, and is very close to this experiment, given the parentheses are mandatory:
def fib(x) x < 2 ? x : fib(x-1) + fib(x-2) end
# one-line style
def value() @value end
# one-line style 3.0
def value() = @value
# one-line style setter
def value=(value) @value = value end
# not supported in 3.0
def value=(value) = @value = value
It doesn't remove the end
which is the purpose of this feature sorry, but I like it because it keeps the consistency of having end
, while not using more punctuation like ;
or =
.
Edit: Also when we use endless methods, we need parentheses for calls made in the method, whereas the regular syntax does not
# one-line style
def value() process @value end
# endless method needs parentheses
def value() = process(@value)