You’re focusing too much on the specific example, which isn’t relevant to the proposal at hand. The point is, there are plenty of mutator methods that don’t return None; if you’re ignoring the return value, it doesn’t matter what it is. You could discard three elements off a list by calling stuff.pop() three times, but unless they JUST HAPPEN to be None, you wouldn’t be able to chain those calls together.
IMO any proposal along these lines should be agnostic as to the return value. I’m +0.1 on any such proposal in general, and they have a lot of problems to solve before they can be viable, but adding requirements like this isn’t making it any more useful.
Okay, cool. I thought that might have been your stance based on the conclusion, but there was too much time spent on specifics that I felt the need to clarify
:= + or actually works, but the danger is ending up reciting a spell and summoning some demon
The use of the token proposed in this discussion would be much more readable and intuitive.
You’re back to square one.
In this specific case of the CRC (a class I control), I was able to resolve it by improving the API with argument packing (specifically because __init__ does the same thing as update()), but the situation changes when you don’t have control and you are forced to create custom utilities, wrappers, or subclasses.
I don’t think I’ve ever seen an &. operator in any language before - it could almost be a cursed hybrid of C++'s pass-by-reference (&) and attribute access. Or a confused misspelling of *. to dereference the method and access an attribute (by a C newbie)?
So maybe it’s just me, but this doesn’t feel very intuitive.
Nor does it feel that readable - especially when you’re trying to figure out where the value comes from, searching through the line of code, figuring out whether it was returned by a function, or comes from a stray & somewhere.
Not against the idea in general, but I think you’d need a lot more evidence to say that it’s “readable and intuitive”
Look, it’s much easier to just explain that & is an operator that returns the evaluated object before the getter than to explain how (_ := CRC32(b'foo')).update(b'bar') or _.digest works.
To me, it seems clear that it’s returning the evaluated object before obtaining the attribute.
>>> @dataclass
... class Point:
... x: int
... y: int
....
>>> point = Point(1, 2)
>>> point
Point(x=1, y=2)
>>> point.x
1
>>> point.y
2
>>> point.&x
Point(x=1, y=2)
>>> point.&y
Point(x=1, y=2)
But I would like to reinforce what I said in the considerations:
No, builder pattern was created because of lack of keyword arguments (**kwargs) in languages like Java. Like most patterns, it indicates language doesn’t support the thing speaker want or need.
To be clear, I think that proposing grammatical change just to bypass std API design decision isn’t constructive.
Don’t see this feature as a reason for people to neglect good API choices; see its use as a signal that you should rethink your API, and if there’s no other way, it’s okay to use it as a last resort.
I left the advantages listed in the gist above, but I’ll mention them here:
Suggesting improvements for APIs often takes a long time to be accepted, and even then there’s a chance they’ll be rejected, making the process even more time-consuming;
You are no longer required to create temporary variables in order to avoid losing the reference;
Avoid creating custom utilities or subclasses to work around the problem.
It can serve as a metric to indicate a poor API;
You are not dependent on API maintainers because it works with any method; ~ @jsbueno
It is moderately readable and intuitive, much more so than solutions with := + or, and much more concise.
Since the API remains unchanged, it does not break compatibility with previous versions; ~ @jsbueno
More incentive for creating methods that modify only the class state, so they can focus solely on functionality; ~ @jsbueno
In this case, I believe the Python interpreter could compare the data types and the memory address. This way, it can determine if it’s a kind of copy and issue a warning, similar to what happens when you forget to use await in a coroutine.
Something like:
>>> # both are types
>>> are_types = type(x) == type and type(y) == type
>>>
>>> # are not types AND same type AND different memory address
>>> if not are_types and type(x) == type(y) and x is not y:
>>> warnings.warn('Another instance belonging to the same type was lost when using the cascade operator (.&), this may have been a mistake.')
bool are_types = PyType_Check(x) && PyType_Check(y);
if (!are_types && Py_TYPE(x) == Py_TYPE(y) && !Py_Is(x, y)){
PyExc_WarnEx(
PyExc_RuntimeWarning,
"Another instance belonging to the same type was lost when using the cascade operator (.&), this may have been a mistake",
1
);
}
This follows a standard object-oriented pattern used in Python and many other languages: calling a class creates an instance, on which methods can then be called.
What’s the benefit of doing this all in one line?
One-liners are fine for simple calls, but splitting steps improves clarity, debugging, and future changes:
crc = CRC16(b'foo')
crc.update(b'bar')
crc.update(b'baz')
result = crc.digest()
There’s no doubt about that, but you shouldn’t be forced to put the instance in a variable and in many cases you could simply do: CRC16(b'foo').digest().
This topic addresses cases where you are required to create a temporary variable to avoid losing the reference prior to the method call, which in many cases only makes your code too verbose. This will serve as syntactic sugar for the same result, but in a more practical, elegant, and concise way.
Are you suggesting that the one-liner is better than the current pattern enforced by the API choices?
result = CRC16(b'foo').&update(b'bar').&update(b'baz').digest()
crc = CRC16(b'foo')
crc.update(b'bar')
crc.update(b'baz')
result = crc.digest()
How would you read the one-liner? Something like, ’ result is equal to the digest of a CRC16 object initialized with b'foo', after updating it first with b'bar' and then with b'baz'.'? Of course not. What about the .& operator?
How should it be read?
result = CRC16(b'foo').&update(b'bar').&update(b'baz').digest()
More precisely, this topic is about adding a language feature to allow users to override the design decisions of library authors regarding the design of their library[1]. That’s not inherently a bad thing, but in general I’d regard “using a library in a way that wasn’t intended” as a code smell.
There are already plenty of ways of modifying the API of a library, including writing wrapper code and monkeypatching the library.
Given that questions of whether code is “verbose”, or “clear”, or “elegant” are inherently subjective, it seems very unlikely to me that this thread is going to come up with a sufficiently compelling argument for a language change unless someone comes up with a benefit of the new syntax that isn’t about allowing users to override the design of a library API - and so far, I’ve seen no such examples.
If the user wrote the library code in question, they can simply change the called code, and this feature isn’t needed. ↩︎
I view it a bit differently. There is (at least some) desire in the ecosystem to support method chaining but so far the common practice is usually to not return self if a call has side effects. Brett made a great comment about that in another thread: Support method chaining in pathlib - #2 by brettcannon
Adding a dedicated operator would allow the API design to continue as is while simultaneously giving user the option to use chaining explicitly. This could ultimately lead to better APIs as devs might not feel pressured anymore to support chaining even if it doesn’t make sense for their project / API.
–
Not sure I’m convinced yet .& is the right syntax. Why not use .. for it?
The formatting is probably something that should be considered here as well. Using newlines like above does make sense IMO but can get verbose fairly quickly.
Another issue to consider might be async function calls. Not sure how to handle these best.