On Mon, 20 Jun 2022 at 03:06, David Mertz, Ph.D. <david.mertz@gmail.com> wrote:
I guess '>=' also looks "confusable", but it's far less common in signatures, and the meaning is further away.
It's no less valid than your other examples, nor less common (why would you have "==" in a function signature, for instance?).
I guess I probably use `==` more often in function calls and signatures, on reflection. In call, I use `==` quite often to pass some boolean switch value, and `>=` much less often. Obviously, I am aware that `>=` also produces a boolean result, and YMMV on how often comparing for equality and inequality expresses the flag you want.
In signature, I'd really only use it, I reckon, as a "default default." E.g.
def frobnicate(data, verbose=os.environ.get('LEVEL')==loglevel.DEBUG): ...
This supposes I have an environmental setting for verbosity that I usually want to use, but might override that on a particular call.
Okay. When it comes to finding causes for the difficulty of reading your function signatures, I think this is a far greater one *in a vacuum* than having support for late-bound defaults. Maybe it's not a problem *for you* because you're accustomed to reading that as a single token, but that can be true of any sequence. Is there any value in not putting that into a global constant?
I think the cognitive complexity of a line with sigils is somewhere around quadratic or cubic on the number of distinct sigils. But when several look similar, it quickly tends toward the higher end. And when several have related meanings, it's harder still to read.
It shouldn't need to be. Once you know how expressions are built up, it should give linear complexity.
I'm not talking about the big-O running time of some particular engine that parses a BNF grammar here. I'm talking about actual human brains, which work differently.
Yes, and human brains are fine with adding more options, as long as they work the same way that other options do.
I don't have data for my quadratic and cubic guesses. Just 40 years of my own programming experience, and about the same amount of time watching other programmers.
It would be relatively easy to measure if one wanted to. But it's a cognitive psychology experiment. You need to get a bunch of people in rooms, and show them lots of code lines. Then measure error rates and response times in their answers. That sort of thing. The protocol for this experiment would need to be specified more carefully, of course. But it *is* the kind of thing that can be measured in human beings.
So my (very strong) belief is that a human being parsing a line with 5 sigils in it will require MUCH MORE than 25% more effort than parsing a line with 4 sigils in it. As in, going from 4 to 5 distinct sigils in the same line roughly DOUBLES cognitive load. Here the distinctness is important; it's not at all hard to read:
a + b + c + d + e + f + g + h + i
Okay. You run a test, and let's see how it goes. At the moment, all you have is a single data point: yourself. Specifically, yourself with all your current knowledge. I think it's a little biased. :)
Did the introduction of the @ (matrix multiplication) operator to Python increase the language's complexity multiplicatively, or additively? Be honest now: are your programs more confusing to read because @ could have been *, because @ could have been +, because @ could have been ==, etc etc etc, or is it only that @ is one new operator, additively with the other operators?
I'm not sure how much you know about the background of this in the NumPy world. While other libraries have also now used that operator, NumPy was the driving force.
In the old days, if I wanted to do a matrix multiply, I would either do:
A_matrix = np.matrix(A) B_matrix = np.matrix(B)
result = A_matrix * B_matrix
Or alternately:
result = np.dot(A, B)
Neither of those approaches are terrible, but in more complex expressions where the dot product is only part of the expression, indeed `A @ B` reads better.
Regardless, the @ operator is now available *everywhere* in Python. Does it quadratically increase cognitive load?
When I write an expression like 'a - b * c / d**e + f' that also has a bunch of symbols. But they are symbols that:
- look strongly distinct - have meanings familiar from childhood - have strongly different meanings (albeit all related to arithmetic)
The double asterisk wasn't one that I used in my childhood, yet in programming, I simply learned it and started using it. What happens is that known concepts are made use of to teach others.
I didn't learn the double asterisk in school either. That I had to learn in programming languages. I actually prefer those programming languages that use `^` for exponentiation (in that one aspect, not overall more than Python), because it's more reminiscent of superscript.
In other words: you had to learn it. Just like everything else. So "have meanings familiar from childhood" only takes you so far; we have to learn everything.
Is that simply because you already are familiar with those operators, or is there something inherently different about them? Would it really be any different?
It's a mixture of familiarity and actual visual distinctness. `/` and `+` really do just *look different*. In contrast `:=` and `=` just really look similar.
That's because they ARE similar. Assignment is assignment. It's the same as how "x *= 4" looks like a combination of multiplication and assignment, because it is. Cognitive load is linear when the brain can find patterns.
This actually circles back to why I would greatly prefer `def myfunc(a=later some_expression())` as a way to express late binding of a default argument. Even though you don't like a more generalized deferred computation, and a version of PEP 671 that used a soft keyword would not automatically create such broader use, in my mind the option of later more general use is left open by that approach.
So, you prefer a spelling that makes it less likely that people will use it. In other words, you hate the idea, and are asking me to worsen my own idea to make it less useful. Is that right? Then, no. I will continue to promote the "=>" form, since it doesn't give the false impression that "later some_expression()" could stand on its own.
- it makes the function header carry information that actually isn't part of the function signature (that the object is also in the surrounding context as "_sentinel" - the function's caller can't use that information), and doesn't have any real advantages over just putting it a line above.
I do think some of this comes down to something I find somewhat mythical. 99%+ of the time that I want to use a sentinel, `None` is a great one. Yes I understand that a different one is required occasionally. But basically, `arg=None` means "late binding" in almost all cases. So that information is ALREADY in the header of almost all the functions I deal with.
As with the loglevel example above, you're accustomed to seeing "arg=None" as "late binding". But: 1) You don't get the information about WHAT it late-binds to 2) This is only true in your own code, and other code might actually use None in a completely different way In a function signature "def bisect(stuff, lo=0, hi=None)", you don't know what the hi value actually defaults to. Even if it's obvious that it is late-bound, that is actually the least relevant piece of information that could be given! Instead of saying what the default is, you simply say "oh and hey, this has a default that I'm not telling you about". It would be far more useful to have the default there, but then have some extra tag somewhere else that says whether it's late-bound or early-bound, than to have it this way around. ChrisA