
Hi, There was the discussion about vector, etc... I think I have a frustration about chaining things easily in python in the stdlib where many libs like orm do it great. Here an example : The code is useless, just to show the idea
a = [1,2,3]
a.append(4)
a.sort()
c = max(a) + 1
I would be happy to have
[1,2,3].append(4)::sort()::max() +1
It makes things very easy to read: first create list, then append 4, then sort, then get the max. To resume, the idea is to apply via a new operator (::, .., etc...) the following callable on the previous object. It's clearly for standalone object or after a method call when the return is None (there is fluent `.` when there is a return value)
object::callable() = callable(object) object(arg)::callable = callable(object(arg))
def callable(arg1,arg2): pass
object::callable(arg) == callable(object, arg)
The idea is to use quite everything as first argument of any callable. I do not know if it was already discussed, and if it would be technically doable. Nice Day Jimmy

On Wed, Feb 20, 2019 at 1:22 AM Jimmy Girardet <ijkl@netc.fr> wrote:
Hmm. The trouble is, there's no consistent "previous object" for each of your steps. You're mixing together "max()" which is a built-in, and "sort()" which is a method. Your proposed semantics (which I snipped) will happily cover built-ins, but not methods. Proposals broadly similar to these (but generally, if my memory serves, focusing on methods only) have come up periodically. You may want to briefly search the mailing list archives to find discussions on "method chaining". ChrisA

On Tue, Feb 19, 2019 at 6:23 AM Jimmy Girardet <ijkl@netc.fr> wrote:
Easy for you, but not necessarily everyone else. For instance, I find the explicit writing out of the lines easier. Also realize that design is on purpose so that mutating method calls on lists do not return themselves to get the point across that the mutation was in-place and not in fact a new list. -Brett

Brett Cannon writes:
On Tue, Feb 19, 2019 at 6:23 AM Jimmy Girardet <ijkl@netc.fr> wrote:
(Responding to the OP) In most cases, where one doesn't care about performance, one can rewrite as max(a := sorted([1, 2, 3] + [4])) + 1 This also tells you something about readability. I find the "dot-chaining" style easier to analyze than the nested function call style, because the methods are applied left-to-right and few inline operators are interpreted with right associativity -- note that this is a runtime decision! Where I don't care about analysis at this site, I define a function. If I do care about performance, writing it out line by line helps to emphasize that and to optimize well. I think given the current design with its long history, it would be more useful to identify pain points where functions like sorted() don't exist.
I have come to think that this is suboptimal. Now I would like to experiment with a Python-like language where methods *always* return self, and where returning something else makes sense, Python-oid should provide a function that does that. Of course there would be an exception for dunders. It's not irritating enough to actually do this, though. :-) Steve

Hi, thank for the replies. I searched on python-idea as Chris proposed me with "chain" and I've found 2 relevant discussions : * A proposal from Cris Angelico ;-) https://mail.python.org/pipermail/python-ideas/2014-February/026079.html """Right. That's the main point behind this: it gives the *caller* the choice of whether to chain or not. That's really the whole benefit, right there.""" ChrisA the killer answer of Nick Coghlan https://mail.python.org/pipermail/python-ideas/2014-February/026158.html explaining that the main point is about mutating and transforming : this is a part of Nick's answer :
start piece of answer Compare:
seq = get_data() seq.sort() seq = sorted(get_data()) Now, compare that with the proposed syntax as applied to the first operation: seq = []->extend(get_data())->sort() That *looks* like it should be a data transformation pipeline, but it's not - each step in the chain is mutating the original object, rather than creating a new one. That's a critical *problem* with the idea, not a desirable feature.
end of answer
It seems that in 2014 Guido was against chaining method. * Then in 2017 : https://mail.python.org/pipermail/python-ideas/2017-August/046770.html quite the same thing with a "rcompose" idea. So i understand that in this and previous discussions, the main argument against it was that mutation should looks like mutation and not transformation (which returns value). This seems like a strong thing in core-dev mind since it's about design of the language. I totally agree with this idea of "mutating does not "return value" but as a "user" of the language I easily can deal with the 2 ideas of "mutating does not return value" and "lets chain those things". I think you can do the last without breaking the first. "Although practicality beats purity" a : object::mutable1()::mutable2()::mutable3()::mutable4() b : multligne sequence mutable1(object) mutable2(object) mutable3(object) mutable4(object) Pros for a: - stands in one line without loosing clarity. - user are now used to play with "fluent" syntax in many libs or languages. Cons for a: - b was here before :-) and fits the idioms of the language. - a may look like you're using some `return Value` on mutating operation. I read somewhere that we are adult enough to be careful of what we are doing so, this cons is not obvious to me. At the very beginning of my thoughts I (maybe naively) thought that "a" could easily be converted to "b" at compile time but I'm not aware of those internal things. Le 20/02/2019 à 04:10, Stephen J. Turnbull a écrit :
[1,2,3].append(4)::List.sort()::max() +1 gives you a sequential way of reading things like you talk or speak in every day life.
Jimmy

On Tue, Feb 19, 2019 at 01:52:34PM -0800, Brett Cannon wrote:
That's possibly a matter of familiarity. I'd be very surprised if you preferred this: mystr = mystr.strip() mystr = mystr.expandtabs() mystr = mystr.lower() mystr = mystr.replace('ham', 'spam') result = function(mystr) over this fluent version: result = function(mystr.strip().expandtabs().lower().replace('ham', 'spam')) Or if you're worried about the line length: result = function(mystr.strip() .expandtabs() .lower() .replace('ham', 'spam') ) works for me. -- Steven

On 2/20/19 9:10 AM, Steven D'Aprano wrote:
Those two blocks of code don't quite do the same thing. What is the value of mystr at the end of each block? Yes, as a matter of fact, I do think that that's the important question. Consider these two similar blocks of code: f(mylist): mylist.sort() mylist.pop() mylist.reverse() g(mystr): return mystr.strip().expandtabs().replace('ham', 'spam') The difference is more than a matter of which syntax I prefer, the difference is a matter of how I build software. Python lists and dicts are classic objects. I don't interact with the underlying data, I send the object a request to operate on that data, and rest assured that the object is doing the right thing. I can create one at the top of my application, pass it around, make a series of request against it, and ask that very same object for the end result: some_list = get_some_data() f(some_list) print(some_list) # what happened to my original? Python strings, however, don't work that way. If I create one at the top of my application, I can pass it around, but my original remains as is. some_string = get_some_data() another_string = g(some_string) # why do I need a new string? print(some_string) print(another_string) It's a different way of doing things. Please don't conflate them.
I am no longer impressed that you squeezed the original into exactly 80 columns. ;-)

Here's a syntax that solves this using the new operators _:= and ,_ a = [1,2,3] ( _:= a ,_ .append(4) ,_ .sort() ) Personally, I find this a bit harder to read on one line and would break it up like this: (_:= a ,_ .append(4) ,_ . .sort() ) --- Bruce

On Wed, Feb 20, 2019 at 10:34 AM Jonathan Fine <jfine2358@gmail.com> wrote:
There's a problem with introducing ,_ as a new operator.
I should have put "new" in scare quotes to be more clear as it's not really new. As you've observed this is already implemented. For it to work as in my example you also need to use the "new" _:= operator which requires Python 3.8. --- Bruce

On Wed, Feb 20, 2019 at 11:03 AM Rhodri James <rhodri@kynesim.co.uk> wrote:
Given the responses, I think I was a bug more obtuse than necessary. My apologies. This is already in Python 3.8. I am NOT actually suggesting a change to the language. I'm showing how to "solve" the "problem" of wanting to write chained method calls on a single line. I agree the solution is unreadable and worse than the problem. Any other proposed solution is merely bikeshedding the syntax and I think suffers from the same non-readability problem. For example these are essentially the same: _:= a ,_. append(4) ,_. sort() a :: append(4) :: sort() --- Bruce

On Wed, Feb 20, 2019 at 10:24:25AM -0800, Bruce Leban wrote:
Here's a syntax that solves this using the new operators _:= and ,_
Be careful about making such dogmatic statements. Language design is not a matter of picking random syntax and claiming that it solves the problem -- especially when it doesn't solve the problem. Putting aside the aethestics of your suggestions (I think they are as ugly as sin), both of them are currently legal code, since a single underscore _ is a legal identifier. Starting in 3.8, _:= is the left hand side of an assignment expression assigning to the variable "_", so your suggestion: _:= a already has meaning in Python. That's a new feature, and you can be forgiven for perhaps not knowing about it. But the second suggestion is a very old feature, going back to Python 1 days. ,_ is part of a tuple or argument list including a variable called "_". You probably could have discovered this for yourself by trying something like this in the interactive interpreter: py> x = [] py> x,_.append(1) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'tuple' object has no attribute 'append' The actual result you would get (an exception, or a tuple) will depend on the history of your current session, but whatever result you get it will show that it is already legal syntax. That means that there's going to be an ambiguity between your hoped-for semantics and the existing semantics: _:= a ,_.b would mean a tuple with two items: _:= a _.b rather than a chained method call as you hoped.
That's currently legal code which assigns a to _ then builds a tuple (or at least attempts to build a tuple) consisting of: a None None after calling a.append and a.sort. But there's a deeper problem with this entire concept, regardless of syntax, one which to my knowledge nobody has mentioned yet: it simply isn't compatible with the way operators work in Python at the moment. More on this in another post (coming soon). -- Steven

On Thu, Feb 21, 2019 at 12:55:00PM +1100, Steven D'Aprano wrote:
Ah, I have just spotted your later explanation that you aren't referring to *new operators* spelled as _:= and ,_ (as I understood from your words) but as *existing syntax* that works today (at least in the most up-to-date version of 3.8). That pretty much makes all my previous objections to these "new operators" redundant, as they aren't operators at all. I'll point out that there's nothing special about the use of an underscore here. Instead of your snippet: _:= a ,_ .append(4) ,_ .sort() we could write: tmp := a, tmp.append(4), tmp.sort() So this is hardly method chaining or a fluent interface. (Hence my confusion -- I thought you were suggesting a way to implement a fluent interface.) If this were an Obfuscated Python competition, I'd congratulate you for the nasty trick of putting whitespace in non-standard positions to conceal what is going on. But in real code, that's only going to cause confusion. (But I think you acknowledge that this trick is not easy to read.) But it's also sub-optimal code. Why introduce a temporary variable instead of just writing this? a.append(4), a.sort() The only advantage of _ is that it is a one-character name. But there are other one character names which are less mystifying: a := some_long_identifier, a.append(4), a.sort() But why create a tuple filled mostly with None? Python is slow enough as it is without encouraging such anti-patterns. a := some_long_identifier; a.append(4); a.sort() Now no tuple is created, and doesn't need to be garbage collected. But this is still not a fluent interface. Its just cramming multiple statements into a single line. Which means you can't do this: function(arg1, mylist.append(1).sort(), arg3) -- Steven

Correcting myself twice now, that's not a good sign... :-) On Thu, Feb 21, 2019 at 12:55:00PM +1100, Steven D'Aprano wrote:
On further thought, I would like to withdraw that claim. Actual operators like + - etc of course are implemented using dunder methods, but "pseudo-operators" like the dot aren't. If we had such a fluent method chain operator, it would be more like dot than ordinary operators. -- Steven

In funcoperators, because the dot operator is just syntaxic sugar for functions getattr and setattr with a string, a.hello.world # can be implemented using infix a -o- 'hello' -o- 'world' # or using postfix a |dot('hello') |dot('world') # using from funcoperators import postfix, infix o = infix(getattr) def dot(n): return postfix(lambda x: getattr(x, n)) For methods, one could write : a.upper().replace('x', 'y') # can be implemented by a |meth('upper') |meth('replace', 'x', 'y') # using def meth(x, *a, **b): return postfix (lambda self: getattr(self, x)(*a, **b)) And one could do upper = postfix(str.upper) def replace(*a, **b): return postfix(lambda self: self.replace(*a, **b)) # to be able to do : a |upper |replace('a', 'b') And of course you can create your own functions to have the behavior you want. robertvandeneynde.be On Thu, 21 Feb 2019, 13:22 Steven D'Aprano, <steve@pearwood.info> wrote:

On 21/02/2019 15:47, Robert Vanden Eynde wrote:
(I meant bargepole, not bargepool, obviously. Sorry about the typos, I have two other projects crowding my brain at the moment and may not make a great deal of sense.) I mean it looked horrible. If someone put code like that in front of me, I would assume that the writer was being clever for the sake of being clever and rewrite it in a way that I would probably be able to read later without half an hour of puzzling first. I flat out do not see the need for fluent operators. What the heck is wrong with putting separate operations on separate lines? It's easier to read, and gives you shorter lines into the bargin (see the 80-column argument going on elsewhere). -- Rhodri James *-* Kynesim Ltd

TL;DR: When talking about all this, there are two distictions that should be considered: mutating operations vs copying operations functions vs methods. This has a lot of impact on the design, and it's very important that any final syntax makes these distinctions clear. On Wed, Feb 20, 2019 at 8:33 AM Dan Sommers < 2QdxY4RzWzUUiLuE@potatochowder.com> wrote:
Python lists and dicts are classic objects. I don't interact with the underlying data,
<snip>
It's a different way of doing things. Please don't conflate them.
however, that's kind of what this thread is all about :-) Python has two "kinds" of objects -- mutable and immutable. Immutable objects necessarily return new objects when you call methods on them (or do something else entirely) This also makes them naturally easy to chain operations, as per the string processing example(s). However, mutating operations are not so easy to chain -- you can't write: a_list.append(item).sort()[:10] To, say, add a value to a list and get th first ten items in sorted order I think the solution to THAT is to do as someone suggested, and to identify the missing non-mutating operations. after all, we can, with lists, do: sorted(a_list + [item])[:10] but there are multiple issues with that: 1) a mixture of function calling and operators 2) that fact that "append an item and make a new list" isn't supported by either a method or an operator, so we have to do a kludge to use +. And I think a big one that inspires these ideas: 3) When nesting function calling, you write (and read) the code in the reverse order that the operations take place: First you add the item to the list, then you sort it, then you slice it. 4) the "functional" form -- e.g. sorted() doesn't preserve type -- whether you sort a list or a tuple, you get a list back. So is the goal here to get "fluent" syntax for stringing mutating operations together that mutate the object? If so, then that should be clearly different than string together immutables. That is if I see: that = this.a_method().another_method() It should be really obvious whether or not "this" has been altered! Right now, if the object in question follows python conventions, then it has not been altered. Personally, I think Python has enough ways to apply an operation, but maybe in the spirit of functional programming's idea that we shouldn't be mutating collections at all, we should extend the mutable sequence ABC with methods that return new objects: list.sorted() list.appended() list.extended() ... (slicing already copies). Then you could work with mutable and immutable sequences similarly to how we do with strings: first_ten = a_list.appended(item).sorted()[:10] -CHB -- Christopher Barker, PhD Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython

On 2/20/19 7:46 PM, Christopher Barker wrote:
Absolutely.
On Wed, Feb 20, 2019 at 8:33 AM Dan Sommers <2QdxY4RzWzUUiLuE@potatochowder.com> wrote:
It's a different way of doing things. Please don't conflate them.
however, that's kind of what this thread is all about :-)
And I'm objecting to it. :-)
So is the goal here to get "fluent" syntax for stringing mutating operations together that mutate the object?
Not my goal, no.
Call me +0, or maybe +0.5 if adding such additional methods prevent adding new syntax. Dan

On 2019-02-20 10:10, Steven D'Aprano wrote:
It seems that this fluency discussion, and the vector discussion is similar; I made a toy class [1] to demonstrate. It is much like DavidMertz's vector [2], but focused on chained methods . The `vector()` method lets us enter "vector mode". There are methods that act on elements (eg. map), methods that act on the whole (eg. sort), and methods that exit vector mode (eg. list). output = vector([3, 2, 1]).append(4).sort().limit(10).list() Fluency can be had by entering vector mode on a singleton list: output = ( vector([mystr]) .strip() .expandtabs() .lower() .replace("ham", "spam") .map(function) .first() ) Given vector() is quite succinct, and Numpy and Pandas do vector operations elegantly already, I do not think there is need for vectorized operators or fluency operators in Python. [1] My toy class - https://github.com/klahnakoski/mo-vector/blob/master/mo_vector/__init__.py [2] DavidMertz vector 0- https://github.com/DavidMertz/stringpy/blob/master/vector.py

Heyy, it's funcoperators idea !
[1,2,3].append(4)::sort()::max() +1
[1, 2, 3] |append(4) |to(sorted) |to(max) |to(plus1) You just have to : pip install funcoperators from funcoperators import postfix as to plus1 = postfix(lambda x: x+1) from funcoperators import postfix def append(x): return postfix(lambda L: L + [x]) The module also propose a way to have infix operators, so if you want to have the "::" operator that have the same precedence than "/", you could call it "/ddot/" and do : [1,2,3] /ddot/ append(4) /ddot/ sort /ddot/ max +1 But for your case (creating Bash like "pipes" the above solution seems better). https://pypi.org/project/funcoperators/ PS: you could create "modify and return" version of "append" or "sort" like this : def append(x): @postfix def new(L): L.append(x) return L return new Then you'd have :

On Wed, Feb 20, 2019 at 1:22 AM Jimmy Girardet <ijkl@netc.fr> wrote:
Hmm. The trouble is, there's no consistent "previous object" for each of your steps. You're mixing together "max()" which is a built-in, and "sort()" which is a method. Your proposed semantics (which I snipped) will happily cover built-ins, but not methods. Proposals broadly similar to these (but generally, if my memory serves, focusing on methods only) have come up periodically. You may want to briefly search the mailing list archives to find discussions on "method chaining". ChrisA

On Tue, Feb 19, 2019 at 6:23 AM Jimmy Girardet <ijkl@netc.fr> wrote:
Easy for you, but not necessarily everyone else. For instance, I find the explicit writing out of the lines easier. Also realize that design is on purpose so that mutating method calls on lists do not return themselves to get the point across that the mutation was in-place and not in fact a new list. -Brett

Brett Cannon writes:
On Tue, Feb 19, 2019 at 6:23 AM Jimmy Girardet <ijkl@netc.fr> wrote:
(Responding to the OP) In most cases, where one doesn't care about performance, one can rewrite as max(a := sorted([1, 2, 3] + [4])) + 1 This also tells you something about readability. I find the "dot-chaining" style easier to analyze than the nested function call style, because the methods are applied left-to-right and few inline operators are interpreted with right associativity -- note that this is a runtime decision! Where I don't care about analysis at this site, I define a function. If I do care about performance, writing it out line by line helps to emphasize that and to optimize well. I think given the current design with its long history, it would be more useful to identify pain points where functions like sorted() don't exist.
I have come to think that this is suboptimal. Now I would like to experiment with a Python-like language where methods *always* return self, and where returning something else makes sense, Python-oid should provide a function that does that. Of course there would be an exception for dunders. It's not irritating enough to actually do this, though. :-) Steve

Hi, thank for the replies. I searched on python-idea as Chris proposed me with "chain" and I've found 2 relevant discussions : * A proposal from Cris Angelico ;-) https://mail.python.org/pipermail/python-ideas/2014-February/026079.html """Right. That's the main point behind this: it gives the *caller* the choice of whether to chain or not. That's really the whole benefit, right there.""" ChrisA the killer answer of Nick Coghlan https://mail.python.org/pipermail/python-ideas/2014-February/026158.html explaining that the main point is about mutating and transforming : this is a part of Nick's answer :
start piece of answer Compare:
seq = get_data() seq.sort() seq = sorted(get_data()) Now, compare that with the proposed syntax as applied to the first operation: seq = []->extend(get_data())->sort() That *looks* like it should be a data transformation pipeline, but it's not - each step in the chain is mutating the original object, rather than creating a new one. That's a critical *problem* with the idea, not a desirable feature.
end of answer
It seems that in 2014 Guido was against chaining method. * Then in 2017 : https://mail.python.org/pipermail/python-ideas/2017-August/046770.html quite the same thing with a "rcompose" idea. So i understand that in this and previous discussions, the main argument against it was that mutation should looks like mutation and not transformation (which returns value). This seems like a strong thing in core-dev mind since it's about design of the language. I totally agree with this idea of "mutating does not "return value" but as a "user" of the language I easily can deal with the 2 ideas of "mutating does not return value" and "lets chain those things". I think you can do the last without breaking the first. "Although practicality beats purity" a : object::mutable1()::mutable2()::mutable3()::mutable4() b : multligne sequence mutable1(object) mutable2(object) mutable3(object) mutable4(object) Pros for a: - stands in one line without loosing clarity. - user are now used to play with "fluent" syntax in many libs or languages. Cons for a: - b was here before :-) and fits the idioms of the language. - a may look like you're using some `return Value` on mutating operation. I read somewhere that we are adult enough to be careful of what we are doing so, this cons is not obvious to me. At the very beginning of my thoughts I (maybe naively) thought that "a" could easily be converted to "b" at compile time but I'm not aware of those internal things. Le 20/02/2019 à 04:10, Stephen J. Turnbull a écrit :
[1,2,3].append(4)::List.sort()::max() +1 gives you a sequential way of reading things like you talk or speak in every day life.
Jimmy

On Tue, Feb 19, 2019 at 01:52:34PM -0800, Brett Cannon wrote:
That's possibly a matter of familiarity. I'd be very surprised if you preferred this: mystr = mystr.strip() mystr = mystr.expandtabs() mystr = mystr.lower() mystr = mystr.replace('ham', 'spam') result = function(mystr) over this fluent version: result = function(mystr.strip().expandtabs().lower().replace('ham', 'spam')) Or if you're worried about the line length: result = function(mystr.strip() .expandtabs() .lower() .replace('ham', 'spam') ) works for me. -- Steven

On 2/20/19 9:10 AM, Steven D'Aprano wrote:
Those two blocks of code don't quite do the same thing. What is the value of mystr at the end of each block? Yes, as a matter of fact, I do think that that's the important question. Consider these two similar blocks of code: f(mylist): mylist.sort() mylist.pop() mylist.reverse() g(mystr): return mystr.strip().expandtabs().replace('ham', 'spam') The difference is more than a matter of which syntax I prefer, the difference is a matter of how I build software. Python lists and dicts are classic objects. I don't interact with the underlying data, I send the object a request to operate on that data, and rest assured that the object is doing the right thing. I can create one at the top of my application, pass it around, make a series of request against it, and ask that very same object for the end result: some_list = get_some_data() f(some_list) print(some_list) # what happened to my original? Python strings, however, don't work that way. If I create one at the top of my application, I can pass it around, but my original remains as is. some_string = get_some_data() another_string = g(some_string) # why do I need a new string? print(some_string) print(another_string) It's a different way of doing things. Please don't conflate them.
I am no longer impressed that you squeezed the original into exactly 80 columns. ;-)

Here's a syntax that solves this using the new operators _:= and ,_ a = [1,2,3] ( _:= a ,_ .append(4) ,_ .sort() ) Personally, I find this a bit harder to read on one line and would break it up like this: (_:= a ,_ .append(4) ,_ . .sort() ) --- Bruce

On Wed, Feb 20, 2019 at 10:34 AM Jonathan Fine <jfine2358@gmail.com> wrote:
There's a problem with introducing ,_ as a new operator.
I should have put "new" in scare quotes to be more clear as it's not really new. As you've observed this is already implemented. For it to work as in my example you also need to use the "new" _:= operator which requires Python 3.8. --- Bruce

On Wed, Feb 20, 2019 at 11:03 AM Rhodri James <rhodri@kynesim.co.uk> wrote:
Given the responses, I think I was a bug more obtuse than necessary. My apologies. This is already in Python 3.8. I am NOT actually suggesting a change to the language. I'm showing how to "solve" the "problem" of wanting to write chained method calls on a single line. I agree the solution is unreadable and worse than the problem. Any other proposed solution is merely bikeshedding the syntax and I think suffers from the same non-readability problem. For example these are essentially the same: _:= a ,_. append(4) ,_. sort() a :: append(4) :: sort() --- Bruce

On Wed, Feb 20, 2019 at 10:24:25AM -0800, Bruce Leban wrote:
Here's a syntax that solves this using the new operators _:= and ,_
Be careful about making such dogmatic statements. Language design is not a matter of picking random syntax and claiming that it solves the problem -- especially when it doesn't solve the problem. Putting aside the aethestics of your suggestions (I think they are as ugly as sin), both of them are currently legal code, since a single underscore _ is a legal identifier. Starting in 3.8, _:= is the left hand side of an assignment expression assigning to the variable "_", so your suggestion: _:= a already has meaning in Python. That's a new feature, and you can be forgiven for perhaps not knowing about it. But the second suggestion is a very old feature, going back to Python 1 days. ,_ is part of a tuple or argument list including a variable called "_". You probably could have discovered this for yourself by trying something like this in the interactive interpreter: py> x = [] py> x,_.append(1) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'tuple' object has no attribute 'append' The actual result you would get (an exception, or a tuple) will depend on the history of your current session, but whatever result you get it will show that it is already legal syntax. That means that there's going to be an ambiguity between your hoped-for semantics and the existing semantics: _:= a ,_.b would mean a tuple with two items: _:= a _.b rather than a chained method call as you hoped.
That's currently legal code which assigns a to _ then builds a tuple (or at least attempts to build a tuple) consisting of: a None None after calling a.append and a.sort. But there's a deeper problem with this entire concept, regardless of syntax, one which to my knowledge nobody has mentioned yet: it simply isn't compatible with the way operators work in Python at the moment. More on this in another post (coming soon). -- Steven

On Thu, Feb 21, 2019 at 12:55:00PM +1100, Steven D'Aprano wrote:
Ah, I have just spotted your later explanation that you aren't referring to *new operators* spelled as _:= and ,_ (as I understood from your words) but as *existing syntax* that works today (at least in the most up-to-date version of 3.8). That pretty much makes all my previous objections to these "new operators" redundant, as they aren't operators at all. I'll point out that there's nothing special about the use of an underscore here. Instead of your snippet: _:= a ,_ .append(4) ,_ .sort() we could write: tmp := a, tmp.append(4), tmp.sort() So this is hardly method chaining or a fluent interface. (Hence my confusion -- I thought you were suggesting a way to implement a fluent interface.) If this were an Obfuscated Python competition, I'd congratulate you for the nasty trick of putting whitespace in non-standard positions to conceal what is going on. But in real code, that's only going to cause confusion. (But I think you acknowledge that this trick is not easy to read.) But it's also sub-optimal code. Why introduce a temporary variable instead of just writing this? a.append(4), a.sort() The only advantage of _ is that it is a one-character name. But there are other one character names which are less mystifying: a := some_long_identifier, a.append(4), a.sort() But why create a tuple filled mostly with None? Python is slow enough as it is without encouraging such anti-patterns. a := some_long_identifier; a.append(4); a.sort() Now no tuple is created, and doesn't need to be garbage collected. But this is still not a fluent interface. Its just cramming multiple statements into a single line. Which means you can't do this: function(arg1, mylist.append(1).sort(), arg3) -- Steven

Correcting myself twice now, that's not a good sign... :-) On Thu, Feb 21, 2019 at 12:55:00PM +1100, Steven D'Aprano wrote:
On further thought, I would like to withdraw that claim. Actual operators like + - etc of course are implemented using dunder methods, but "pseudo-operators" like the dot aren't. If we had such a fluent method chain operator, it would be more like dot than ordinary operators. -- Steven

In funcoperators, because the dot operator is just syntaxic sugar for functions getattr and setattr with a string, a.hello.world # can be implemented using infix a -o- 'hello' -o- 'world' # or using postfix a |dot('hello') |dot('world') # using from funcoperators import postfix, infix o = infix(getattr) def dot(n): return postfix(lambda x: getattr(x, n)) For methods, one could write : a.upper().replace('x', 'y') # can be implemented by a |meth('upper') |meth('replace', 'x', 'y') # using def meth(x, *a, **b): return postfix (lambda self: getattr(self, x)(*a, **b)) And one could do upper = postfix(str.upper) def replace(*a, **b): return postfix(lambda self: self.replace(*a, **b)) # to be able to do : a |upper |replace('a', 'b') And of course you can create your own functions to have the behavior you want. robertvandeneynde.be On Thu, 21 Feb 2019, 13:22 Steven D'Aprano, <steve@pearwood.info> wrote:

On 21/02/2019 15:47, Robert Vanden Eynde wrote:
(I meant bargepole, not bargepool, obviously. Sorry about the typos, I have two other projects crowding my brain at the moment and may not make a great deal of sense.) I mean it looked horrible. If someone put code like that in front of me, I would assume that the writer was being clever for the sake of being clever and rewrite it in a way that I would probably be able to read later without half an hour of puzzling first. I flat out do not see the need for fluent operators. What the heck is wrong with putting separate operations on separate lines? It's easier to read, and gives you shorter lines into the bargin (see the 80-column argument going on elsewhere). -- Rhodri James *-* Kynesim Ltd

TL;DR: When talking about all this, there are two distictions that should be considered: mutating operations vs copying operations functions vs methods. This has a lot of impact on the design, and it's very important that any final syntax makes these distinctions clear. On Wed, Feb 20, 2019 at 8:33 AM Dan Sommers < 2QdxY4RzWzUUiLuE@potatochowder.com> wrote:
Python lists and dicts are classic objects. I don't interact with the underlying data,
<snip>
It's a different way of doing things. Please don't conflate them.
however, that's kind of what this thread is all about :-) Python has two "kinds" of objects -- mutable and immutable. Immutable objects necessarily return new objects when you call methods on them (or do something else entirely) This also makes them naturally easy to chain operations, as per the string processing example(s). However, mutating operations are not so easy to chain -- you can't write: a_list.append(item).sort()[:10] To, say, add a value to a list and get th first ten items in sorted order I think the solution to THAT is to do as someone suggested, and to identify the missing non-mutating operations. after all, we can, with lists, do: sorted(a_list + [item])[:10] but there are multiple issues with that: 1) a mixture of function calling and operators 2) that fact that "append an item and make a new list" isn't supported by either a method or an operator, so we have to do a kludge to use +. And I think a big one that inspires these ideas: 3) When nesting function calling, you write (and read) the code in the reverse order that the operations take place: First you add the item to the list, then you sort it, then you slice it. 4) the "functional" form -- e.g. sorted() doesn't preserve type -- whether you sort a list or a tuple, you get a list back. So is the goal here to get "fluent" syntax for stringing mutating operations together that mutate the object? If so, then that should be clearly different than string together immutables. That is if I see: that = this.a_method().another_method() It should be really obvious whether or not "this" has been altered! Right now, if the object in question follows python conventions, then it has not been altered. Personally, I think Python has enough ways to apply an operation, but maybe in the spirit of functional programming's idea that we shouldn't be mutating collections at all, we should extend the mutable sequence ABC with methods that return new objects: list.sorted() list.appended() list.extended() ... (slicing already copies). Then you could work with mutable and immutable sequences similarly to how we do with strings: first_ten = a_list.appended(item).sorted()[:10] -CHB -- Christopher Barker, PhD Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython

On 2/20/19 7:46 PM, Christopher Barker wrote:
Absolutely.
On Wed, Feb 20, 2019 at 8:33 AM Dan Sommers <2QdxY4RzWzUUiLuE@potatochowder.com> wrote:
It's a different way of doing things. Please don't conflate them.
however, that's kind of what this thread is all about :-)
And I'm objecting to it. :-)
So is the goal here to get "fluent" syntax for stringing mutating operations together that mutate the object?
Not my goal, no.
Call me +0, or maybe +0.5 if adding such additional methods prevent adding new syntax. Dan

On 2019-02-20 10:10, Steven D'Aprano wrote:
It seems that this fluency discussion, and the vector discussion is similar; I made a toy class [1] to demonstrate. It is much like DavidMertz's vector [2], but focused on chained methods . The `vector()` method lets us enter "vector mode". There are methods that act on elements (eg. map), methods that act on the whole (eg. sort), and methods that exit vector mode (eg. list). output = vector([3, 2, 1]).append(4).sort().limit(10).list() Fluency can be had by entering vector mode on a singleton list: output = ( vector([mystr]) .strip() .expandtabs() .lower() .replace("ham", "spam") .map(function) .first() ) Given vector() is quite succinct, and Numpy and Pandas do vector operations elegantly already, I do not think there is need for vectorized operators or fluency operators in Python. [1] My toy class - https://github.com/klahnakoski/mo-vector/blob/master/mo_vector/__init__.py [2] DavidMertz vector 0- https://github.com/DavidMertz/stringpy/blob/master/vector.py

Heyy, it's funcoperators idea !
[1,2,3].append(4)::sort()::max() +1
[1, 2, 3] |append(4) |to(sorted) |to(max) |to(plus1) You just have to : pip install funcoperators from funcoperators import postfix as to plus1 = postfix(lambda x: x+1) from funcoperators import postfix def append(x): return postfix(lambda L: L + [x]) The module also propose a way to have infix operators, so if you want to have the "::" operator that have the same precedence than "/", you could call it "/ddot/" and do : [1,2,3] /ddot/ append(4) /ddot/ sort /ddot/ max +1 But for your case (creating Bash like "pipes" the above solution seems better). https://pypi.org/project/funcoperators/ PS: you could create "modify and return" version of "append" or "sort" like this : def append(x): @postfix def new(L): L.append(x) return L return new Then you'd have :
participants (13)
-
Anders Hovmöller
-
Brett Cannon
-
Bruce Leban
-
Chris Angelico
-
Christopher Barker
-
Dan Sommers
-
Jimmy Girardet
-
Jonathan Fine
-
Kyle Lahnakoski
-
Rhodri James
-
Robert Vanden Eynde
-
Stephen J. Turnbull
-
Steven D'Aprano