From 5cb9636c4cb01bc42a789ecbf712b278be12794d Mon Sep 17 00:00:00 2001 From: Steven Troxler Date: Mon, 10 Jan 2022 21:46:13 -0800 Subject: [PATCH] PEP 677: Include more rejected alternatives (#2219) * Add more rejected alternatives - Rust-style syntax - Forced outer parentheses - Improving readability of existing callable type * Add discussion of alternative | precedence * Add blank lines for bullet lists * Fixes based on initial review * More tweaks from review * More updates based on review --- pep-0677.rst | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/pep-0677.rst b/pep-0677.rst index b03fc20e0..7170286c9 100644 --- a/pep-0677.rst +++ b/pep-0677.rst @@ -826,6 +826,20 @@ replacement for callable types: more like a shorter way to write them than a replacement for ``Callable``. +Hybrid keyword-arrow Syntax +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the Rust language, a keyword ``fn`` is used to indicate functions +in much the same way as Python's ``def``, and callable types are +indicated using a hybrid arrow syntax ``Fn(i64, String) -> bool``. + +We could use the ``def`` keyword in callable types for Python, for +example our two-parameter boolean function could be written as +``def(int, str) -> bool``. But we think this might confuse readers +into thinking ``def(A, B) -> C`` is a lambda, particularly because +Javascript's ``function`` keyword is used in both named and anonymous +functions. + Parenthesis-Free Syntax ~~~~~~~~~~~~~~~~~~~~~~~ @@ -839,6 +853,99 @@ existing function header syntax. Moreover, it is visually similar to lambdas, which bind names with no parentheses: ``lambda x, y: x == y``. +Requiring Outer Parentheses +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A concern with the current proposal is readability, particularly +when callable types are used in return type position which leads to +multiple top-level ``->`` tokens, for example:: + + def make_adder() -> (int) -> int: + return lambda x: x + 1 + +We considered a few ideas to prevent this by changing rules about +parentheses. One was to move the parentheses to the outside, so +that a two-argument boolean function is written ``(int, str -> bool)``. +With this change, the example above becomes:: + + def make_adder() -> (int -> int): + return lambda x: x + 1 + +This makes the nesting of many examples that are difficult to +follow clear, but we rejected it because + +- Currently in Python commas bind very loosely, which means it might be common + to misread ``(int, str -> bool)`` as a tuple whose first element is an int, + rather than a two-parameter callable type. +- It is not very similar to function header syntax, and one of our goals was + familiar syntax inspired by function headers. +- This syntax may be more readable for deaply nested callables like the one + above, but deep nesting is not very common. Encouraging extra parentheses + around callable types in return position via a style guide would have most of + the readability benefit without the downsides. + +We also considered requiring parentheses on both the parameter list and the +outside, e.g. ``((int, str) -> bool)``. With this change, the example above +becomes:: + + def make_adder() -> ((int) -> int): + return lambda x: x + 1 + +We rejected this change because: + +- The outer parentheses only help readability in some cases, mostly when a + callable type is used in return position. In many other cases they hurt + readability rather than helping. +- We agree that it might make sense to encourage outer parentheses in several + cases, particularly callable types in function return annotations. But + + - We believe it is more appropriate to encourage this in style guides, + linters, and autoformatters than to bake it into the parser and throw + syntax errors. + - Moreover, if a type is complicated enough that readability is a concern + we can always use type aliases, for example:: + + IntToIntFunction: (int) -> int + + def make_adder() -> IntToIntFunction: + return lambda x: x + 1 + + +Making ``->`` bind tighter than ``|`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to allow both ``->`` and ``|`` tokens in type expressions we +had to choose precedence. In the current proposal, this is a function +returning an optional boolean:: + + (int, str) -> bool | None # equivalent ot (int, str) -> (bool | None) + +We considered having ``->`` bind tighter so that instead the expression +would parse as ``((int, str) -> bool) | None``. There are two advantages +to this: + +- It means we no would longer have to treat ``None | (int, str) -> + bool`` as a syntax error. +- Looking at typeshed today, optional callable arguments are very common + because using ``None`` as a default value is a standard Python idiom. + Having ``->`` bind tighter would make these easier to write. + +We decided against this for a few reasons: + +- The function header ``def f() -> int | None: ...`` is legal + and indicates a function returning an optional int. To be consistent + with function headers, callable types should do the same. +- TypeScript is the other popular language we know of that uses both + ``->`` and ``|`` tokens in type expressions, and they have ``|`` bind + tighter. While we do not have to follow their lead, we prefer to do + so. +- We do acknowledge that optional callable types are common and + having ``|`` bind tighter forces extra parentheses, which makes these + types harder to write. But code is read more often than written, and + we believe that requiring the outer parentheses for an optional callable + type like ``((int, str) -> bool) | None`` is preferable for readability. + + Introducing type-strings ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -850,6 +957,37 @@ from the Steering Council on ensuring that type expressions do not diverge from the rest of Python's syntax. +Improving Usability of the Indexed Callable Type +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If we do not want to add new syntax for callable types, we could +look at how to make the existing type easier to read. One proposal +would be to make the builtin ``callable`` function indexable so +that it could be used as a type:: + + callable[[int, str], bool] + +This change would be analogous to PEP 585 that made built in collections +like ``list`` and ``dict`` usable as types, and would make imports +more convenient, but it wouldn't help readability of the types themselves +much. + +In order to reduce the number of brackets needed in complex callable +types, it would be possible to allow tuples for the argument list:: + + callable[(int, str), bool] + +This actually is a significant readability improvement for +multi-argument functions, but the problem is that it makes callables +with one arguments, which are the most common arity, hard to +write: because ``(x)`` evaluates to ``x``, they would have to be +written like ``callable[(int,), bool]``. This is awkward enough that +we dislike this idea. + +Moreover, none of these ideas help as much with reducing verbosity +as the current proposal, nor do they introduce as strong a visual cue +as the ``->`` between the parameter types and the return type. + Backward Compatibility ======================