[Python-ideas] Dart-like method cascading operator in Python
Steven D'Aprano
steve at pearwood.info
Fri Nov 22 03:16:39 CET 2013
On Thu, Nov 21, 2013 at 11:55:46AM +0100, Perešíni Peter wrote:
> Recently I fell in love with Dart method cascading operator .. to the
> degree that I find it really convenient and I am missing it in the Python.
> The cascade operator
>
> myObj
> ..setX(5)
> ..y=6
>
> is the syntactic sugar equivalent of the following:
>
> tmp = myObj
> tmp.setX(5)
> tmp.y=6
>
> This can be used to greatly simplify instantiating objects and can also
> enable creating domain specific languages in Python.
Please give examples of how this might do such a thing, rather than just
claim it will. "This syntax will end world hunger and bring peace to the
Middle East..." I don't see how this syntax can simplify instantiating
objects:
obj = MyClass("spam", "eggs")
is already pretty simple. Perhaps you're referring only to a small
subset of (in my opinion) *poorly designed* if not outright buggy
objects which aren't instantiated completely on creation and need to be
tweaked by hand before being ready to use. Or those with excessively
complicated APIs that could really do with a few helper functions.
In my opinion, we shouldn't encourage classes that require manual
instantiation like this:
obj = MyClass()
obj.foo = "spam"
obj.setbar("eggs")
obj.make_it_work()
# now obj is fully instantiated and ready to be used...
not even with your suggested syntax:
obj = MyClass()
..foo = "spam"
..setbar("eggs")
..make_it_work()
Helping people do the wrong thing is not, in my opinion, an advantage.
The use-case of DSLs is perhaps more interesting, but a simple example
would go a long way to support the idea.
> In particular, we can
> make this much more powerful in Python (as opposed to Dart) because Python
> recognizes scope by indentation and therefore it would be possible to do
> something like
>
> gnuplot.newPlot()
> ..set("xrange [0:5]")
> ..set("yrange [0:20]")
> ..newPlot()
> ..addSeries("Linear", [1,2,3])
> ..addSeries("Quadratic", [1,4,6])
> ..run()
I had to read that multiple times before I was able to interpret what
this is supposed to mean. It doesn't help that I'm not familiar enough
with the gnuplot API to tell exactly what you're doing. I *think* that
it would be the equivalent of this:
p = gnuplot.newPlot()
p.set("xrange [0:5]")
p.set("yrange [0:20]")
q = p.newPlot()
q.addSeries("Linear", [1,2,3])
q.addSeries("Quadratic", [1,4,6])
p.run()
If I'm wrong, I think that suggests that your extension to the syntax
isn't as clear as you hoped. If I'm right, I wonder what the point of
the inner sub-block is, since you don't appear to do anything with the q
plot. What have I missed?
[...]
> Note that this is strictly better than method chaining, e.g.
>
> template = MyObject().setX(1).setY(2)
>
> because
> a) method chaining is hard to write on multiple lines, the only two options
> are
[snip explicit line continuation]
> (template = MyObject()
> .setX(1)
> .setY(2))
>
> which looks weird.
I suggest it only looks weird because you've put the opening bracket in
the wrong place. I'd write it like this:
template = (MyObject()
.setX(1)
.setY(2)
)
a a variation of such, which apart from an extra set of parentheses is
virtually exactly the same as your suggestion:
template = MyObject()
..setX(1)
..setY(2)
without the addition of a new double-dot syntax.
To the extent that this suggested cascading dot operator is just used
for method chaining, I'm against it because we can already easily chain
methods with no new syntax provided the object supports chaining.
To the extent that it adds chaining to those objects which don't
support it, for example those with mutator methods that return None,
that's a more interesting proposal:
mylist = list(some_function(a, b, c))
..sort()
..reverse()
..pop() # output of pop is discarded
..sort() # still operating on the list
But I'm going to suggest that this is still not a good idea, since it
goes against the Zen "Explicit is better than implicit". As I understand
it, this cascade relies on there being an implicit "last object used",
and that can be a bit problematic.
For starters, you should explain precisely how the compiler will
determine what is the "last object used" in practice. Your examples
so far suggest it is based on the last line of source code, but
that's tricky. For example:
x = spam(); y = eggs()
..method()
I would *assume* that this implicitly calls y.method rather than
x.method, but that's not a given. How about this?
y = spam()
x = eggs()
del y
..method()
Does that call x.method()? And what do this do?
myClass()
for i in range(3):
..method()
Is it a syntax error? Or does it call i.method()? Or perhaps even call
method on the range object? Or does the myClass object still count as
the implicit "last object used"?
You're going to need to specify exactly where and under what
circumstances the implicit object is set, and where you can use this
implicit object cascading operator.
> b) method chaining cannot take advantage of multiple scoping, e.g. the only
> way to write Gnuplot example is
Since I'm not sure I understand your Gnuplot example above, I'm not
going to further comment on it here.
> c) method chaining needs specialized API, e.g. each method needs to return
> self
This is a good argument in support of your proposal.
> d) method chaining cannot be used with attributes, e.g. there is no
> equivalent to
>
> obj = MyObject()
> ..x=1
> ..y=2
That's a good argument in support of your proposal, but not a terribly
powerful argument, since the above is trivially written as:
obj = MyObject()
obj.x = 1
obj.y = 2
and I for one prefer to see the object being operated on explicitly
rather than implicitly.
--
Steven
More information about the Python-ideas
mailing list