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/