On Tue, 21 Dec 2021 at 18:35, Steven Troxler <steven.trox...@gmail.com> wrote:
>
> I've been thinking about readability hard because I share many of your 
> concerns about readability.

Before I comment on syntax, I'd like to question the example:

> An example
> =========
>
> The function
> ----------------
>
> To get at what I mean, here's a nice simple function:
> ```
> def zip(f, g):
>     def zipped(x: int, y: str):
>         return f(x), g(y)
>     return zipped
> ```

So my first question is why a generic function like this would limit
the parameters x and y to int and str. Surely this is an entirely
workable function for *any* arguments with the appropriate "shape"? So
how is this a good example of somewhere you'd use types like
Callable[[int], bool] (by the way, where did that "bool" appear from?)
in a signature?

My immediate thought when seeing that zip function is that its type is
"obvious"¹, (takes two functions, returns a function which takes 2
arguments matching the args of the 2 functions and returns a tuple
matching the results of the 2 functions). That type is almost
impossible to express clearly, because it needs pretty complex
generics. But the more important point is that I'd never, **ever**,
want to write that type out longhand. I'd expect a type system to
either infer the type, and it would be anonymous¹, or to give up and
treat it as untyped. One thing I absolutely would not want to do is
over-constrain any of the arguments just to make it possible to write
the type down.

The real issue with this function, in my view, is not expressing
callables, but rather generic type variables ("return a function whose
first argument has the same type as the single argument of the
function which is the first argument of this function..." !!!)

Can you suggest a more "real world" function as an example, which
focuses on the callable syntax and doesn't use things like arguments
called f and g? Maybe a GUI callback with a function argument like
on_click?

¹ By which I mean intuitive, not easy to express in words!!! :-)
² Some languages may allow syntax like `typeof(zip)` to refer to that
anonymous type, but that's a separate point.

> Here's it's type using typing.Callable:
> ```
> typing.Callable[
>     [typing.Callable[[int], bool], typing.Callable[[str], float]],
>     typing.Callable[[int, str], tuple[bool, float]
> ]
> ```
> which seems ugly. It's actually not bad compared to a lot of production code, 
> but this is the kind of thing that led to PEP 677.

It's massively over-constrained. I assume that's because you're trying
to make a point about Callable[] rather than about generics, but can
you give a realistic example that *doesn't* involve over-constraining
higher order functions?

On a side note, why not name at least some of those function types?
And why not use "from typing import Callable"? It feels like you're
not making enough effort to make your example readable, which
undermines your point as a result.

> ((int) -> float, (str) -> bool) -> (int, str) -> tuple[float, bool]

To your credit, you've made this pretty unreadable, which gives some
balance here :-) Seriously, making it a one-liner with all those ->
arrows is a disaster. Changing the location of the parentheses doesn't
alter that at all.

Rewriting as a multi-line expression:

(
    (int) -> float,
    (str) -> bool
) -> (int, str) -> tuple[float, bool]

helps quite a bit, but returning a function looks bad here. We're not
writing Haskell, you know ;-)

I'd prefer a "mixed" notation here:

(
    (int) -> float,
    (str) -> bool
) -> Callable[(int, str), tuple[float, bool]]

I don't honestly think there's a readable "function returning a
function" form here - the chained -> tokens is just awkward. Although
that's clearly a matter of preference, there's never going to be an
objective answer here.

> Here’s the type if we change the syntax to put the right parenthesis after 
> the return type:
> ```
> ((int -> float), (str -> bool) -> (int, str -> tuple[float, bool])
> ```
>
> To my eyes, most of the pain points are now eliminated.
> - there’s never a double-arrow due to callable in return position
> - even for argument types, to my eyes it’s now easier to read

To my mind, the eye is still drawn to the arrows, and the readability
is no better. And the parentheses give me a lisp vibe, for reasons I
can't really pin down but which makes this version *less* readable.

> An added bonus is we no longer have to think about double-arrows when a 
> callable type is in the return position of a function, e.g.:
> ```
> def f() -> (int, str -> bool): ...
> ```

Still looks like double arrows to me, I'm afraid. The parentheses
don't group strongly enough to override the "chain of arrows"
impression.

> And it solves another major usability problem we found - writing optional 
> callables - because it’s now no problem at all to write
> ```
> (int, str -> bool) | None
> ```
> as the type of an optional callable argument.

I guess, but it feels like punctuation soup to me, I'm afraid.
Optional[Callable[[int, str], bool]] is more obvious to me (the
ugliness in that version comes from the square brackets and the
capitalisation, which are present for different reasons, not the use
of words rather than symbols).

Paul
_______________________________________________
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/5SUC22FESWTEWCXP3K7TR5GSH3RJ5DJK/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to