
On Fri, Dec 24, 2021 at 06:24:03PM -0000, Jim J. Jewett wrote:
Steven D'Aprano wrote:
In comparison, Mark's version: @Callable def IntToIntFunc(a:int)->int: pass # in the type declaration func: IntToIntFunc uses 54 characters, plus spaces and newlines (including 7 punctuation characters); it takes up three extra lines, plus a blank line. As syntax goes it is double the size of Callable.
I think it takes only the characters needed to write the name IntToIntFunc.
That's only true if IntToIntFunc is a builtin, otherwise it needs to be defined somewhere. It doesn't just magically happen. If you are using the declaration many times, then I acknowledge that it may be worth the effort of pre-declaration and naming. (But see below.) Particularly if the signature is complicated, although I think that will be relatively rare. But in the worst case, you may only use it once. So the entire cognitive burden of pre-declaration (both writing it and reading it) applies to that one use.
The @callable def section is a one-time definition, and not logically part of each function definition where it is used.
The status quo is that we can use an anonymous type in the annotation without pre-defining it, using Callable. PEP 677 proposes a new, more compact syntax for the same. Any proposal for function prototypes using `def` is directly competing against Callable or arrow syntax for the common case that we want an anonymous, unnamed type written in place. Even in the case that we want to give the type a name that we plan to use repeatedly, this `def` syntax is still competing directly against what is already possible using the status quo: use Callable to create a named type alias. But with the `def` syntax, you can *only* use it as a named, pre-defined object. So half, maybe 90%, of your use-cases disappear. Any time that we have a short, simple Callable that doesn't require a name, why would we bother creating a do-nothing function just so we can use it as a prototype? I don't think many people would. I know I wouldn't. That would be the equivalent of filling your program with trivial plus_one(x) and times_two(y) functions instead of just using `x+1` and `2*y`. So the benefit of the `def` syntax comes from that relatively small subset of cases: - the callable signature is complicated; - we wish to refer it it multiple times; - giving it a name (like "FileOpener", say, not "IntToInt") aids clarity. That's not to be sneered at. But in those circumstances, we don't need the `def` syntax, because we can already use Callable and a type alias. So the `def` syntax adds nothing we don't already have, it is no easier to use, it is more verbose, not less. But if we can use an existing function as the prototype instead of having to declare the prototype, that shifts the balance. If we already have some function, then there is no extra cost in having to declare it and give it a name, it already has been declared and given a name.
I get that some people prefer an inline lambda to a named function, and others hate naming an infrastructure function, but ...
Why are you even bothering to type the callback function? If it is complicated enough to be worth explicitly typing, then it is complicated enough to chunk off with a name.
I would say the opposite: most callback or key functions have very simple signatures. If my function takes a key function, let's say: def spam(mylist:[str], a: int, b: float, c: bool|None, key: Callable[[str], str], ) -> Eggs: mylist = sorted(mylist, key=key) ... the relevant signature is (str) -> str. Do we really need to give that a predefined named prototype? def StrToStr(s: str) -> str: pass I would argue that very few people would bother. If somebody did, they probably also defined type aliases for ListOfStr and BoolOrNone, and wish they were using Java or Pascal *wink* It seems to me that most callbacks and key functions have short signatures. Certainly all the ones I have written do: they typically take a single argument, of a known type, and return a known type.
Having to switch parsing modes to understand an internal ([int, float, int] -> List[int]), and then to pop that back off the stack is much harder.
I notice that you just used something very close to PEP 677 arrow syntax totally unself-consciously, without any need to explain it. I think this is good evidence that far from being confusing, this is a completely natural syntax that we already interpret as a function prototype.
Hard enough that you really ought to help your reader out with a name,
What are you going to name it? Int_and_Float_and_Int_returns_List_of_Int_Function tells us nothing that (int, float, int) -> list[int] Callable[[int, float, int], list[int]] doesn't already say. Naming functions is hard. Naming function *prototypes* is even harder. Just duplicating the prototype in the name is noise. We don't bloat our code with say-nothing comments: mylist.sort() # sort mylist mylist.append(x) # append x to mylist or at least we hopefully don't do it beyond the initial first few months of learning to program. We let the code speak for itself. But I agree with you, if a type is complex enough that a meaningful name, or even a generic name, helps comprehension, that we should name it. We can already do that with type aliases. -- Steve