Skip to content

Commit

Permalink
Add protocol operators
Browse files Browse the repository at this point in the history
Resolves   #709.
  • Loading branch information
evhub committed Apr 24, 2023
1 parent c82a8cf commit d5ad1f8
Show file tree
Hide file tree
Showing 13 changed files with 321 additions and 76 deletions.
105 changes: 86 additions & 19 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,9 +397,9 @@ The line magic `%load_ext coconut` will load Coconut as an extension, providing

_Note: Unlike the normal Coconut command-line, `%%coconut` defaults to the `sys` target rather than the `universal` target._

#### MyPy Integration
#### Type Checking

##### Setup
##### MyPy Integration

Coconut has the ability to integrate with [MyPy](http://mypy-lang.org/) to provide optional static type_checking, including for all Coconut built-ins. Simply pass `--mypy` to `coconut` to enable MyPy integration, though be careful to pass it only as the last argument, since all arguments after `--mypy` are passed to `mypy`, not Coconut.

Expand All @@ -416,8 +416,8 @@ To explicitly annotate your code with types to be checked, Coconut supports:
* [Python 3 function type annotations](https://www.python.org/dev/peps/pep-0484/),
* [Python 3.6 variable type annotations](https://www.python.org/dev/peps/pep-0526/),
* [PEP 695 type parameter syntax](#type-parameter-syntax) for easily adding type parameters to classes, functions, [`data` types](#data), and type aliases,
* Coconut's [protocol intersection operator](#protocol-intersection), and
* Coconut's own [enhanced type annotation syntax](#enhanced-type-annotation).
* Coconut's own [enhanced type annotation syntax](#enhanced-type-annotation), and
* Coconut's [protocol intersection operator](#protocol-intersection).

By default, all type annotations are compiled to Python-2-compatible type comments, which means it all works on any Python version.

Expand Down Expand Up @@ -967,6 +967,10 @@ import functools

Coconut uses the `&:` operator to indicate protocol intersection. That is, for two [`typing.Protocol`s](https://docs.python.org/3/library/typing.html#typing.Protocol) `Protocol1` and `Protocol1`, `Protocol1 &: Protocol2` is equivalent to a `Protocol` that combines the requirements of both `Protocol1` and `Protocol2`.

The recommended way to use Coconut's protocol intersection operator is in combination with Coconut's [operator `Protocol`s](#supported-protocols). Note, however, that while `&:` will work anywhere, operator `Protocol`s will only work inside type annotations (which means, for example, you'll need to do `type HasAdd = (+)` instead of just `HasAdd = (+)`).

See Coconut's [enhanced type annotation](#enhanced-type-annotation) for more information on how Coconut handles type annotations more generally.

##### Example

**Coconut:**
Expand All @@ -979,20 +983,15 @@ class X(Protocol):
class Y(Protocol):
y: str
class xy:
def __init__(self, x, y):
self.x = x
self.y = y
def foo(xy: X &: Y) -> None:
print(xy.x, xy.y)
foo(xy("a", "b"))
type CanAddAndSub = (+) &: (-)
```

**Python:**
```coconut_python
from typing import Protocol
from typing import Protocol, TypeVar, Generic
class X(Protocol):
x: str
Expand All @@ -1003,15 +1002,20 @@ class Y(Protocol):
class XY(X, Y, Protocol):
pass
class xy:
def __init__(self, x, y):
self.x = x
self.y = y
def foo(xy: XY) -> None:
print(xy.x, xy.y)
foo(xy("a", "b"))
T = TypeVar("T", infer_variance=True)
U = TypeVar("U", infer_variance=True)
V = TypeVar("V", infer_variance=True)
class CanAddAndSub(Protocol, Generic[T, U, V]):
def __add__(self: T, other: U) -> V:
raise NotImplementedError
def __sub__(self: T, other: U) -> V:
raise NotImplementedError
def __neg__(self: T) -> V:
raise NotImplementedError
```

### Unicode Alternatives
Expand Down Expand Up @@ -1791,6 +1795,8 @@ async (<args>) -> <ret>
```
where `typing` is the Python 3.5 built-in [`typing` module](https://docs.python.org/3/library/typing.html). For more information on the Callable syntax, see [PEP 677](https://peps.python.org/pep-0677), which Coconut fully supports.

Additionally, many of Coconut's [operator functions](#operator-functions) will compile into equivalent [`Protocol`s](https://docs.python.org/3/library/typing.html#typing.Protocol) instead when inside a type annotation. See below for the full list and specification.

_Note: The transformation to `Union` is not done on Python 3.10 as Python 3.10 has native [PEP 604](https://www.python.org/dev/peps/pep-0604) support._

To use these transformations in a [type alias](https://peps.python.org/pep-0484/#type-aliases), use the syntax
Expand All @@ -1801,7 +1807,52 @@ which will allow `<type>` to include Coconut's special type annotation syntax an

Such type alias statements—as well as all `class`, `data`, and function definitions in Coconut—also support Coconut's [type parameter syntax](#type-parameter-syntax), allowing you to do things like `type OrStr[T] = T | str`.

Importantly, note that `int[]` does not map onto `typing.List[int]` but onto `typing.Sequence[int]`. This is because, when writing in an idiomatic functional style, assignment should be rare and tuples should be common. Using `Sequence` covers both cases, accommodating tuples and lists and preventing indexed assignment. When an indexed assignment is attempted into a variable typed with `Sequence`, MyPy will generate an error:
##### Supported Protocols

Using Coconut's [operator function](#operator-functions) syntax inside of a type annotation will instead produce a [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol) corresponding to that operator (or raise a syntax error if no such `Protocol` is available). All available `Protocol`s are listed below.

For the operator functions
```
(+)
(*)
(**)
(/)
(//)
(%)
(&)
(^)
(|)
(<<)
(>>)
(@)
```
the resulting `Protocol` is
```coconut
class SupportsOp[T, U, V](Protocol):
def __op__(self: T, other: U) -> V:
raise NotImplementedError(...)
```
where `__op__` is the magic method corresponding to that operator.

For the operator function `(-)`, the resulting `Protocol` is:
```coconut
class SupportsMinus[T, U, V](Protocol):
def __sub__(self: T, other: U) -> V:
raise NotImplementedError
def __neg__(self: T) -> V:
raise NotImplementedError
```

For the operator function `(~)`, the resulting `Protocol` is:
```coconut
class SupportsInv[T, V](Protocol):
def __invert__(self: T) -> V:
raise NotImplementedError(...)
```

##### `List` vs. `Sequence`

Importantly, note that `T[]` does not map onto `typing.List[T]` but onto `typing.Sequence[T]`. This allows the resulting type to be covariant, such that if `U` is a subtype of `T`, then `U[]` is a subtype of `T[]`. Additionally, `Sequence[T]` allows for tuples, and when writing in an idiomatic functional style, assignment should be rare and tuples should be common. Using `Sequence` covers both cases, accommodating tuples and lists and preventing indexed assignment. When an indexed assignment is attempted into a variable typed with `Sequence`, MyPy will generate an error:

```coconut
foo: int[] = [0, 1, 2, 3, 4, 5]
Expand All @@ -1821,17 +1872,31 @@ def int_map(
xs: int[],
) -> int[] =
xs |> map$(f) |> list
type CanAddAndSub = (+) &: (-)
```

**Python:**
```coconut_python
import typing # unlike this typing import, Coconut produces universal code
def int_map(
f, # type: typing.Callable[[int], int]
xs, # type: typing.Sequence[int]
):
# type: (...) -> typing.Sequence[int]
return list(map(f, xs))
T = typing.TypeVar("T", infer_variance=True)
U = typing.TypeVar("U", infer_variance=True)
V = typing.TypeVar("V", infer_variance=True)
class CanAddAndSub(typing.Protocol, typing.Generic[T, U, V]):
def __add__(self: T, other: U) -> V:
raise NotImplementedError
def __sub__(self: T, other: U) -> V:
raise NotImplementedError
def __neg__(self: T) -> V:
raise NotImplementedError
```

### Multidimensional Array Literal/Concatenation Syntax
Expand Down Expand Up @@ -2296,7 +2361,7 @@ the resulting `inner_func`s will each return a _different_ `x` value rather than

If `global` or `nonlocal` are used in a `copyclosure` function, they will not be able to modify variables in enclosing scopes. However, they will allow state to be preserved accross multiple calls to the `copyclosure` function.

_Note: due to the way `copyclosure` functions are compiled, [type checking](#mypy-integration) won't work for them._
_Note: due to the way `copyclosure` functions are compiled, [type checking](#type-checking) won't work for them._

##### Example

Expand Down Expand Up @@ -2412,6 +2477,8 @@ Coconut fully supports [PEP 695](https://peps.python.org/pep-0695/) type paramet

That includes type parameters for classes, [`data` types](#data), and [all types of function definition](#function-definition). For different types of function definition, the type parameters always come in brackets right after the function name. Coconut's [enhanced type annotation syntax](#enhanced-type-annotation) is supported for all type parameter bounds.

_Warning: until `mypy` adds support for `infer_variance=True` in `TypeVar`, `TypeVar`s created this way will always be invariant._

Additionally, Coconut supports the alternative bounds syntax of `type NewType[T <: bound] = ...` rather than `type NewType[T: bound] = ...`, to make it more clear that it is an upper bound rather than a type. In `--strict` mode, `<:` is required over `:` for all type parameter bounds. _DEPRECATED: `<=` can also be used as an alternative to `<:`._

_Note that, by default, all type declarations are wrapped in strings to enable forward references and improve runtime performance. If you don't want that—e.g. because you want to use type annotations at runtime—simply pass the `--no-wrap-types` flag._
Expand Down
1 change: 0 additions & 1 deletion HELP.md
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,6 @@ Another useful Coconut feature is implicit partials. Coconut supports a number o
```coconut
.attr
.method(args)
obj.
func$
seq[]
iter$[]
Expand Down
61 changes: 60 additions & 1 deletion __coconut__/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ if sys.version_info >= (3, 7):
from dataclasses import dataclass as _dataclass
else:
@_dataclass_transform()
def _dataclass(cls: type[_T], **kwargs: _t.Any) -> type[_T]: ...
def _dataclass(cls: t_coype[_T], **kwargs: _t.Any) -> type[_T]: ...

try:
from typing_extensions import deprecated as _deprecated # type: ignore
Expand Down Expand Up @@ -1334,3 +1334,62 @@ def _coconut_multi_dim_arr(

@_t.overload
def _coconut_multi_dim_arr(arrs: _Tuple, dim: int) -> _Sequence: ...


class _coconut_SupportsAdd(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __add__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsMinus(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __sub__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError
def __neg__(self: _Tco) -> _Vco:
raise NotImplementedError

class _coconut_SupportsMul(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __mul__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsPow(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __pow__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsTruediv(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __truediv__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsFloordiv(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __floordiv__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsMod(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __mod__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsAnd(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __and__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsXor(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __xor__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsOr(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __or__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsLshift(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __lshift__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsRshift(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __rshift__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsMatmul(_t.Protocol, _t.Generic[_Tco, _Ucontra, _Vco]):
def __matmul__(self: _Tco, other: _Ucontra) -> _Vco:
raise NotImplementedError

class _coconut_SupportsInv(_t.Protocol, _t.Generic[_Tco, _Vco]):
def __invert__(self: _Tco) -> _Vco:
raise NotImplementedError
38 changes: 23 additions & 15 deletions _coconut/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -60,40 +60,46 @@ except ImportError:
else:
_abc.Sequence.register(_numpy.ndarray)

# -----------------------------------------------------------------------------------------------------------------------
# TYPING:
# -----------------------------------------------------------------------------------------------------------------------

typing = _t

from typing_extensions import TypeVar
typing.TypeVar = TypeVar # type: ignore

if sys.version_info < (3, 8):
try:
from typing_extensions import Protocol
except ImportError:
Protocol = ...
typing.Protocol = Protocol
Protocol = ... # type: ignore
typing.Protocol = Protocol # type: ignore

if sys.version_info < (3, 10):
try:
from typing_extensions import TypeAlias, ParamSpec, Concatenate
except ImportError:
TypeAlias = ...
ParamSpec = ...
Concatenate = ...
typing.TypeAlias = TypeAlias
typing.ParamSpec = ParamSpec
typing.Concatenate = Concatenate

TypeAlias = ... # type: ignore
ParamSpec = ... # type: ignore
Concatenate = ... # type: ignore
typing.TypeAlias = TypeAlias # type: ignore
typing.ParamSpec = ParamSpec # type: ignore
typing.Concatenate = Concatenate # type: ignore

if sys.version_info < (3, 11):
try:
from typing_extensions import TypeVarTuple, Unpack
except ImportError:
TypeVarTuple = ...
Unpack = ...
typing.TypeVarTuple = TypeVarTuple
typing.Unpack = Unpack
TypeVarTuple = ... # type: ignore
Unpack = ... # type: ignore
typing.TypeVarTuple = TypeVarTuple # type: ignore
typing.Unpack = Unpack # type: ignore

# -----------------------------------------------------------------------------------------------------------------------
# STUB:
# -----------------------------------------------------------------------------------------------------------------------

typing = _t

collections = _collections
copy = _copy
functools = _functools
Expand All @@ -116,8 +122,10 @@ if sys.version_info >= (2, 7):
OrderedDict = collections.OrderedDict
else:
OrderedDict = dict

abc = _abc
abc.Sequence.register(collections.deque)

numpy = _numpy
npt = _npt # Fake, like typing
zip_longest = _zip_longest
Expand Down
2 changes: 1 addition & 1 deletion coconut/__coconut__.pyi
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from __coconut__ import *
from __coconut__ import _coconut_tail_call, _coconut_tco, _coconut_call_set_names, _coconut_handle_cls_kwargs, _coconut_handle_cls_stargs, _namedtuple_of, _coconut, _coconut_Expected, _coconut_MatchError, _coconut_iter_getitem, _coconut_base_compose, _coconut_forward_compose, _coconut_back_compose, _coconut_forward_star_compose, _coconut_back_star_compose, _coconut_forward_dubstar_compose, _coconut_back_dubstar_compose, _coconut_pipe, _coconut_star_pipe, _coconut_dubstar_pipe, _coconut_back_pipe, _coconut_back_star_pipe, _coconut_back_dubstar_pipe, _coconut_none_pipe, _coconut_none_star_pipe, _coconut_none_dubstar_pipe, _coconut_bool_and, _coconut_bool_or, _coconut_none_coalesce, _coconut_minus, _coconut_map, _coconut_partial, _coconut_get_function_match_error, _coconut_base_pattern_func, _coconut_addpattern, _coconut_sentinel, _coconut_assert, _coconut_raise, _coconut_mark_as_match, _coconut_reiterable, _coconut_self_match_types, _coconut_dict_merge, _coconut_exec, _coconut_comma_op, _coconut_multi_dim_arr, _coconut_mk_anon_namedtuple, _coconut_matmul, _coconut_py_str, _coconut_flatten, _coconut_multiset, _coconut_back_none_pipe, _coconut_back_none_star_pipe, _coconut_back_none_dubstar_pipe, _coconut_forward_none_compose, _coconut_back_none_compose, _coconut_forward_none_star_compose, _coconut_back_none_star_compose, _coconut_forward_none_dubstar_compose, _coconut_back_none_dubstar_compose, _coconut_call_or_coefficient, _coconut_in, _coconut_not_in
from __coconut__ import _coconut_tail_call, _coconut_tco, _coconut_call_set_names, _coconut_handle_cls_kwargs, _coconut_handle_cls_stargs, _namedtuple_of, _coconut, _coconut_Expected, _coconut_MatchError, _coconut_SupportsAdd, _coconut_SupportsMinus, _coconut_SupportsMul, _coconut_SupportsPow, _coconut_SupportsTruediv, _coconut_SupportsFloordiv, _coconut_SupportsMod, _coconut_SupportsAnd, _coconut_SupportsXor, _coconut_SupportsOr, _coconut_SupportsLshift, _coconut_SupportsRshift, _coconut_SupportsMatmul, _coconut_SupportsInv, _coconut_Expected, _coconut_MatchError, _coconut_iter_getitem, _coconut_base_compose, _coconut_forward_compose, _coconut_back_compose, _coconut_forward_star_compose, _coconut_back_star_compose, _coconut_forward_dubstar_compose, _coconut_back_dubstar_compose, _coconut_pipe, _coconut_star_pipe, _coconut_dubstar_pipe, _coconut_back_pipe, _coconut_back_star_pipe, _coconut_back_dubstar_pipe, _coconut_none_pipe, _coconut_none_star_pipe, _coconut_none_dubstar_pipe, _coconut_bool_and, _coconut_bool_or, _coconut_none_coalesce, _coconut_minus, _coconut_map, _coconut_partial, _coconut_get_function_match_error, _coconut_base_pattern_func, _coconut_addpattern, _coconut_sentinel, _coconut_assert, _coconut_raise, _coconut_mark_as_match, _coconut_reiterable, _coconut_self_match_types, _coconut_dict_merge, _coconut_exec, _coconut_comma_op, _coconut_multi_dim_arr, _coconut_mk_anon_namedtuple, _coconut_matmul, _coconut_py_str, _coconut_flatten, _coconut_multiset, _coconut_back_none_pipe, _coconut_back_none_star_pipe, _coconut_back_none_dubstar_pipe, _coconut_forward_none_compose, _coconut_back_none_compose, _coconut_forward_none_star_compose, _coconut_back_none_star_compose, _coconut_forward_none_dubstar_compose, _coconut_back_none_dubstar_compose, _coconut_call_or_coefficient, _coconut_in, _coconut_not_in

0 comments on commit d5ad1f8

Please sign in to comment.