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
This commit is contained in:
Steven Troxler 2022-01-10 21:46:13 -08:00 committed by GitHub
parent 79c2ad8b9f
commit 5cb9636c4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 138 additions and 0 deletions

View File

@ -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
======================