Project

General

Profile

Actions

Feature #16746

closed

Endless method definition

Added by mame (Yusuke Endoh) over 4 years ago. Updated almost 4 years ago.

Status:
Closed
Target version:
-
[ruby-core:97652]

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

endless-method-definition.patch (2.47 KB) endless-method-definition.patch mame (Yusuke Endoh), 04/01/2020 03:26 AM

Related issues 3 (0 open3 closed)

Related to Ruby master - Feature #5054: Compress a sequence of endsRejectedtechnohippy (Yasushi ANDO)Actions
Related to Ruby master - Feature #5065: Allow "}" as an alternative to "end"RejectedActions
Related to Ruby master - Feature #12241: super endRejectedActions

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!)

Actions #2

Updated by shyouhei (Shyouhei Urabe) over 4 years ago

Actions #3

Updated by shyouhei (Shyouhei Urabe) over 4 years ago

  • Related to Feature #5065: Allow "}" as an alternative to "end" added
Actions #4

Updated by shyouhei (Shyouhei Urabe) over 4 years ago

Actions #5

Updated by mame (Yusuke Endoh) over 4 years ago

  • File deleted (endless-method-definition.patch)

Updated by matz (Yukihiro Matsumoto) over 4 years ago

  • Tags deleted (joke)

I totally agree with the idea seriously, (far better than #5054, #5065 and #12241) but don't like the syntax. First I thought

def foo(a) = expression

But it is a conflicting syntax.

Matz

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

matz (Yukihiro Matsumoto) wrote in #note-7:

I totally agree with the idea seriously, (far better than #5054, #5065 and #12241) but don't like the syntax. First I thought

def foo(a) = expression

But it is a conflicting syntax.

Matz

How about this:

def foo(a):
  expression

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 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.

Actions #17

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:

  1. Higher cognitive load.

Ruby is already quite complex, with many nuances. We can already define methods with def, define_method, define_singleton_method.

  1. Requiring all text editors, IDE, parsers, linters, code highlighters to support this new syntax

  2. 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 not def: 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) about 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:

  1. This code is very confusing and it is not supposed that a destructive operation is defined in this style.
  2. We want to allow def foo=42 as def foo() = 42 in future. It is difficult to implement it in short term, though. If def foo=(x) = @x = x is allowed once, it will become more difficult.

I will create a PR soon.

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)
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0