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
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-le...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at 
https://mail.python.org/archives/list/python-dev@python.org/message/RLJOIZS4WGYFSMHAIOEYITUPP7N5DBWE/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to