Project

General

Profile

Feature #16264

Real "callable instance method" object.

Added by zverok (Victor Shepelev) 8 months ago. Updated 5 months ago.

Status:
Closed
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:<unknown>]

Description

It is a part of thinking about the "argument-less call style" I already made several proposals about.

Preface

Argument-less call style is what I now call things like map(&:foo) and each(&Notifications.:send) approaches, and I believe that naming the concept (even if my initial name is clamsy) will help to think about it. After using it a lot on a large production codebase (not only symbols, but method references too, which seem to be less widespread technique), I have a strong opinion that it not just "helps to save the keypresses" (which is less important), but also helps to clearer separate the concepts on a micro-level of the code. E.g. if you feel that each(&Notifications.:send) is "more right" than select { |el| Notifications.send(el, something, something) }, it makes you think about Notifications.send design in a way that allows to pass there exactly that combination of arguments so it would be easily callable that way, clarifying modules responsibilities.

(And I believe that "nameless block parameters", while helping to shorter the code, lack this important characteristic of clarification.)

The problem

One of the problems of "argument-less calling" is passing additional arguments, things like those aren't easy to shorten:

ary1.zip(ary2, ary3).map { |lines| lines.join("\n") }
#                                             ^^^^
construct_url.then(&HTTP.:get).body.then { |text| JSON.parse(text, symbolize_names: true) }
#                                                                  ^^^^^^^^^^^^^^^^^^^^^^

(BTW, here is a blog post where I show recently found technique for solving this, pretty nice and always existing in Ruby, if slightly esotheric.)

There's a lot of proposals for "partial applications" which would be more expressive than .curry (guilty myself), but the problematic part in all of this proposals is:

The most widespread "shortening" is &:symbol, and Symbol itself is NOT a functional object, and it is wrong to extend it with functional abilities.

One of consequences of the above is, for example, that you can't use 2.6's proc combination with symbols, like File.:read >> :strip >> :reverse. You want, but you can't.

Here (while discussing aforementioned blog posts), I stumbled upon an idea of how to solve this dilemma.

The proposal

I propose to have a syntax for creating a functional object that when being called, sends the specified method to its first argument. Basically, what Symbol#to_proc does, but without "hack" of "we allow our symbols to be convertible to functional objects". Proposed syntax:

[1, 2, 3].map(&.:to_s)

Justification of the syntax:

  • It is like Foo.:method (producing functional object that calls method)
  • Orphan .:method isn't allowed currently (you need to say self.:method to refer to "current selfs method"), and Matz's justification was "it would be too confusable with :method, small typo will change the result" -- which in PROPOSED case is not as bad, as :foo and .:foo both meaning the same thing;
  • It looks kinda nice, similar to (proposed and rejected) map { .to_s } → with my proposal, it is map(&.:to_s), implying somehow applying .to_s to the previous values in the chain.

The behavior: .:foo produces object of class, say, MethodOfArgument (class name is subject to discuss) — which makes differences of "Proc created from Symbol" (existing internally, but almost invisible) obvious and hackable.

Potential gains

  • New object could be used in proc composition: File.:read >> .:strip >> JSON.:parse >> .:compact
  • When both "method" and "method of argument" are proper functional objects, a new partial application syntax can be discussed, common for them both. For example (but not necessary this method name!)
paragraph_hashes.map(&.:merge.with(author: current_author))
filenames.map(&File.:read.with(mode: 'rb'))
  • (I believe at this point we'll be able to finally switch from discussing "show we extend Symbol with more callable-alike functionality" to just method's name and exact behavior)
  • I am not an expert, but probably some optimizations could be applied, too
  • Currently, :sym.to_proc is internally different from other proc, but this can't be introspected:
:read.to_proc.inspect # => "#<Proc:0x0000556216192198(&:read)>"
                                                     # ^^^^^
  • Probably, exposure of this fact could lead to some new interesting metaprogrammin/optimization techniques.

Transition

:foo and .:foo could work similarly for some upcoming versions (or indefinitely), with .:foo being more powerful alternative, allowing features like groups_of_lines.map(&.:join.partial_apply(' ')) or something.

It would be like "real" and "imitated" keyword arguments. "Last hash without braces" was good at the beginning of the language lifecycle, but then it turned out that real ones provide a lot of benefits. Same thing here: &:symbol is super-nice, but, honestly, it is semantically questionable, so may be slow switch to a "real thing" would be gainful for everybody?..

Also available in: Atom PDF