Feature #9428

Inline argument expressions and re-assignment

Added by Tom Wardrop 3 months ago. Updated 3 months ago.

[ruby-core:59867]
Status:Rejected
Priority:Normal
Assignee:-
Category:core
Target version:-

Description

Just a random idea. Currently, Ruby allows you to use any arbitrary expression for setting default values for arguments, which can be really convenient and makes for clear code, especially handy for documentation, etc. For example:

def fetch(id, cache = config[:cache])
  # bleh
end

In the same vein, as well as setting a default value using an arbitrary expression, it's not uncommon to post-process an argument, some common examples include:

arg = arg.upcase
arg = arg.to_sym
arg = arg.dup

It would be rather nice in my opinion to be able to do this inline when defining the argument:

def fetch(id.to_i, cache = config[:cache])
  # bleh
end

This works well where the argument is the receiver of the method call, but what if you wanted to do Integer(id) in the above example instead of using String#to_i? There are two options. One could either fallback to processing the argument within the method/block body, or, you could make the implementation a little bit clever by using inferencing.

Ruby could auto-assign the passed argument to the first variable encountered in the expression. So in the following example, as soon as the virtual machine encounters id, it recognises it as a variable and assigns the argument value before continuing. When encountering subsequent variables, Ruby would take the usual action and look for a corresponding method in self before throwing an error. You can always disambiguate by qualifying the receiver, e.g. self.id

def fetch(Integer(id), cache = config[:cache])
  # bleh
end

Whatever the result of the expression, it's assigned as the final argument value. So in the case of id.to_i, the argument name of id is inferred. id is set to the supplied argument for the duration of the expression. The result of the expression is then re-assigned as the value of id. This technically allows expressions of arbitrary complexity, but like all things in Ruby, with great power comes great responsibility. One must use common sense when deciding whether to manipulate the argument inline, or within the method body. As long as the expression is of reasonable length and complexity, readability remains perfectly reasonable.

Interested to get some thoughts and opinions on this one. I sense the potential for controversy :)

History

#1 Updated by Nobuyoshi Nakada 3 months ago

It looks just complicated and confusing to me.

#2 Updated by Tom Wardrop 3 months ago

Haha. I don't think any programming language exists that does anything even similar to this. Whether it's a good idea or not, it's going to provoke all the feelings that come with unfamiliarity. Everything is confusing until you learn it and get use to it. Plenty of things in Ruby confused the hell out of me as there were many new ideas and concepts; those things normally turn out to be the best features mind you.

The first thing to keep in mind that the behaviour is very well defined, and the logic itself is simple. I can't think of any edge cases except if no variable is used in the expression, but this can be picked up by the compiler which could throw an error like: "Expecting local variable in argument expression at position 1".

Really, it's a question of "do we want this in Ruby". I don't think there's any denying the practicality, so really it's only a matter of aesthetics and readability. Keep in mind that just everyone uses code highlighting, so any semi-decent editor would pick up the first local variable in the expression at highlight it some how (make it bold, underline it, etc). If the expression is simple enough like in the examples I've provided, it's very readable in my opinion.

Aesthetically, we must compare the current situation to the proposed. Here's some code I wrote today. Pretty common scenario:

ordered_values.map { |v|
  v = v.dup
  [v.delete(:media_type), v]
}.to_h

Rewritten using the proposal, we get it onto one line:

ordered_values.map { |v.dup| [v.delete(:media_type), v] }.to_h

You tell me which you prefer? Of course, like most features in Ruby, it can be abused, so looking at all the wrong you can do with it isn't relevant. Another use case would be to provide logical defaults. At the moment, default values for arguments are only applicable when that argument isn't supplied at all, but what if we want to set a default if the value is nil or false. Here's a comparison:

# Currently
def article(title, body)
  title = title || 'Unnamed'
  # bleh
end

# With proposal
def article(title || 'Unnamed', body)
  # bleh
end

I find all these examples readable and aesthetically pleasing. Longer expressions with conditions are best avoided in favour of simply doing it within the body of the method or block, but shorter conditions work quite well:

def article('Unnamed' if title.empty?, body)
  # bleh  
end

Like in my previous example, this comes in most handy not when defining methods, but when defining proc's where this can in my opinion greatly improve readability.

#3 Updated by Nobuyoshi Nakada 3 months ago

I meant, by "confusing", what's the argument name is not obvious.

#4 Updated by Tom Wardrop 3 months ago

Without syntax highlighting, it isn't super obvious, but in simple cases (which are the main use case), like fetch(id.to_i), it is obvious enough without the aid of syntax highlighting.

Remember though, while there are aspects to this that are potentially unobvious, there are other aspects which it make things more obvious, such as in the case of auto-documentation. If the transformation of #toi or #tosym is embedded within the method signature, it makes it obvious that the given argument must respond to that method (#toi or #tosym). If this logic is hidden away in the method/block body, the author must either document it explicitly, or the user must troll through the method body unless they prefer to find out the hard way at runtime.

There are certainly benefits to be had here beyond merely reducing verbosity. The trade-offs are merely readability related, but as I've said, keeping the expressions simple and good code highlighting pretty much completely mitigate that problem.

If we can't get over the argument name inferencing, then perhaps someone can suggest a syntax for defining the argument name explicitly, e.g.

def fetch(id.to_i as id)
end

But I don't really find that example any more readable. The implicit assignment in the original proposal makes it easy to distinguish expression arguments from arguments with default values.

I certainly don't expect anyone to see this proposal and instantly fall in love. It's a pretty radical idea.

#5 Updated by Matthew Kerwin 3 months ago

I'm -1 for this.

  1. aesthetically: it puts some of the function's code outside the
    function's body, which makes it harder to follow a function's execution
    when reading code, and it makes the signature unnecessarily messy.

  2. syntactically: none of the proposals you've given make enough sense, at
    least for me personally to understand what they mean:

    def foo( arg.toi )
    def foo( arg.to
    i as arg )

    Is the left-most 'arg' a local variable, or referring to self#arg, or
    something else..?

    Ruby could auto-assign the passed argument to the first variable
    encountered in the expression.

    According to my understanding of the parser, any heretofore unseen
    "bareword" tokens are interpreted as function calls, so there is no "first
    variable encountered." It works for optional positional parameters because
    they have an equals sign in (and 'bareword = expression' is universally
    lvar creation/assignment, in Ruby).

  3. debugability: the 'def' line is a single line, however there's no real
    limit to the number of parameters you can include in that line. If each of
    those parameters can include arbitrary expressions, well, I'd hate to have
    to debug a "NoMethodError: undefined method ... for nil:NilClass" on that
    line. And if the answer to that is to split the 'def' line over multiple
    lines, then why not just put the expressions on those multiple lines anyway?

  4. orthogonality: what about non-optional keyword arguments?

    For what it's worth, I'm not entirely for allowing arbitrary expressions
    in optional parameters either, but in that case I can't think of a better
    representation. But if I ever seen anything more than a #[] call in a
    default value I consider it Bad Form™.

    Matthew Kerwin
    http://matthew.kerwin.net.au/

#6 Updated by Tom Wardrop 3 months ago

  1. I know you said you're not a fan of allowing expression when assigning default values to optional parameters, but the point about aesthetics applies equally to them also.

  2. The rule is relatively simple. The first identifier (lvar/method) encountered is automatically assigned the value of the argument passed to the method or proc. That's the rule, the first identifier (valid variable name) is assigned the argument value. If you want to refer to self.id, you must use self.id to disambiguate as you would have to in many other scenario's in Ruby. In the example you highlighted def foo( arg.to_i ), the identifier arg is encountered and automatically assigned the argument value before the expression continues execution. The result of the expression is then assigned back to arg.

  3. The same problem exists for expressions used as default values for optional arguments. Debugging is the same for each. If it's not clear where the error occurred, one could always temporarily break the argument definitions over multiple lines while debugging. I don't think debugging would be any worse than debugging a long method chain like Hash[var.select { |v| #bleh }.map { |v| # blah }]. The problem is universal. I don't think debugability can be used against this proposal.

  4. Technically, for optional arguments, you can have an expression for when an argument is given, and an expression for when an argument is optional. It remains consistent in this respect.

    def foo(id.to_i = config[:default_id])

#7 Updated by Yukihiro Matsumoto 3 months ago

  • Status changed from Open to Rejected

As Nobu pointed out, it's too difficult (or impossible) to parse argument list.
You should propose other way. For example, some languages provide argument decoration like:

  def foo(a:b)
  end

where implicitly calls a = b(a). Note that : is not usable for Ruby. It conflicts with keyword arguments.

Matz.

#8 Updated by Matthew Kerwin 3 months ago

On 20 January 2014 10:41, tom@tomwardrop.com wrote:

Issue #9428 has been updated by Tom Wardrop.

  1. I know you said you're not a fan of allowing expression when assigning default values to optional parameters, but the point about aesthetics applies equally to them also.

That's partly why I'm not a fan. If I could think of a valid, useful
alternative I would strongly suggest it. I know it wouldn't be adopted
(backwards compatibility, if nothing else) but I'd propose it anyway. The
best I can come up with is another special method, along the lines of
method_given?, perhaps:

def foo bar, baz=? # no idea what syntax to propose here
baz = 42 unless argument_given? :baz
end

It's not great, obviously, but it removes arbitrary code from the 'def'
line.

  1. The rule is relatively simple. The first identifier (lvar/method) encountered is automatically assigned the value of the argument passed to the method or proc. That's the rule, the first identifier (valid variable name) is assigned the argument value. If you want to refer to self.id, you must use self.id to disambiguate as you would have to in many other scenario's in Ruby. In the example you highlighted def foo( arg.to_i ), the identifier arg is encountered and automatically assigned the argument value before the expression continues execution.

"First encountered" in regular left-to-right parsing order?

def foo( a[b] )
#=>
def foo a
a = a[b]
end

?

  1. The same problem exists for expressions used as default values for optional arguments. Debugging is the same for each. If it's not clear where the error occurred, one could always temporarily break the argument definitions over multiple lines while debugging. I don't think debugging would be any worse than debugging a long method chain like Hash[var.select { |v| #bleh }.map { |v| # blah }]. I therefore don't think debugability can be used against this proposal.

I agree that existing long/complex lines are hard to debug. But why add
the opportunity for more such lines? Especially in a place that is
traditionally free from such concerns? With my background as a C
programmer I instinctively see the 'def' line as free from execution; it's
a definition, something that informs the interpreter and the human reader
about the nature of the program/data/etc. I would be surprised if I
started seeing runtime exceptions raised from these traditionally
compile-time-only lines.

Again, I know it's already possible to achieve these errors using optional
args, but I concede that as a necessary evil in the absence of an
alternative. And, since we're stuck with them, I prefer a culture of
promoting the least amount of executable code possible in that line; thus
some of my opposition to this proposal.

  1. Technically, for optional arguments, you can have an expression for
    when an argument is given, and an expression for when an argument is
    optional. It remains consistent in this respect.

    def foo(id.to_i = config[:default_id])

This introduces some amount of confusion. Which of the following is
equivalent?

id = id.toi // id = config[:defaultid]

or:

id = id.toi // id = config[:defaultid].to_i

Either way, this is very confusing when, anywhere else in a Ruby script, it
would mean:

id.toi=( config[:defaultid] )

--
Matthew Kerwin
http://matthew.kerwin.net.au/

#9 Updated by Tom Wardrop 3 months ago

"First encountered" in regular left-to-right parsing order?

It would be first identifier encountered as per the order of execution. In the following example, the variable in the if statement would be the name of the argument.

def foo(id.to_i if String === bob )

You could rewrite this as...

def foo(bob)
  bob = id.to_i if String === bob
end

A contrived and fairly non-sensical example, but it demonstrates that the variable furthest to the left isn't necessarily the argument name.

This introduces some amount of confusion. Which of the following is
equivalent?

The first case id = id.to_i // id = config[:default_id]. If an argument is given id = id.to_i, if the argument is omitted, the argument name would be inferred from the expression on the left (id.to_i) and the result of config[:default_id] would be assigned to it as in id = config[:default_id]. I'm not suggesting anyone would want to do this, but it's possible.

As Matz has indicated though, it would be very difficult to parse. Fun to discuss though.

#10 Updated by Alexey Muranov 3 months ago

Tom Wardrop wrote:

It would be rather nice in my opinion to be able to do this inline when defining the argument:

def fetch(id.to_i, cache = config[:cache])
  # bleh
end

-1: I do not see why the person or machine that reads the first line of method's definition (its interface) needs to know how the arguments will be post-processed before being further post-processed. Maybe some idempotent operations, like upcase, would make more sense in this context, but looks too complicated to me.

#11 Updated by Fuad Saud 3 months ago

I am very sorry, I replied the wrong email thread.
-- 
Fuad Saud
Sent with Airmail

#12 Updated by Tom Wardrop 3 months ago

I do not see why the person or machine that reads the first line of method's definition needs to know how the arguments will be post-processed before being further post-processed

Semantically, it just seems more appropriate to define the argument transformation as part of the method definition. It's common to have the first line or two of a method be argument transformation, the point of which is to check and coerce the arguments into their expected form. The actual logic of the method normally just uses that argument variable without ever reassigning it. It wouldn't likely be "further processed" (i.e reassigned) within the method body as you suggest. Where the argument is only used once within the method, you can do the transformation inline; it'd be unnecessary to do it in the method signature unless you wanted to for documentation reasons.

In some respects, you could consider it type hinting for a dynamic language. While statically typed languages would have a type hint., in dynamically typed languages, it's not uncommon for one to check and coerce the argument into something expected.

#13 Updated by Alexey Muranov 3 months ago

Tom Wardrop wrote:

In some respects, you could consider it type hinting for a dynamic language. While statically typed languages would have a type hint., in dynamically typed languages, it's not uncommon for one to check and coerce the argument into something expected.

It seems this is what i meant by an idempotent operation: something like #to_s, #to_i, #upcase. (A function f is idempotent if f(f(x)) = f(x).) I can't think of any syntax though, or how and if to restrict it to only idempotent operations.

#14 Updated by Tom Wardrop 3 months ago

Indeed limiting it to method calls on the argument object (e.g arg.to_i) would make it much easier to parse and more readable, but greatly limits the potential application. It seems the inferencing in the initial proposal is the cause of all the readability and parsing difficulties. The as syntax is certainly workable though. Perhaps <lvar> as <expression> would be better than <expression> as <lvar>. Makes it more natural as an assignment operation, and names the argument before using it in the expression.

The problem will come back to what to do with optional arguments and keyword arguments? Ambiguity goes through the roof while readability goes through the floor. I suppose method and block definitions are perhaps at the limit of the express-ability, with features like the grenade operator *args, hash grenade **hash, block capturing &block, keyword arguments, optional arguments with default values, etc. It's best to just accept that there's pretty much no room left for adding new functionality to method definitions whilst maintaing parsability and readability.

Also available in: Atom PDF