Replies: 2 comments 1 reply
-
Big fan
I think better to just keep it to the Python one for simplicity |
Beta Was this translation helpful? Give feedback.
-
Yes, I think Protocols rather than explicit
IMHO, this is one of the main "problems" with the Rust/Haskell system - although it's powerful/useful, one can too-quickly get into a mess of conditions as to what impls what and how. (Particularly when you have multiple possibilities - with the proposed fallback semantics or Rust-like error-on-conflicting-impls, or Haskell warn-on-conflict...) I'm not sure how to avoid this here, and this "pythonic" syntax (not sure whether you are copying python or generalizing) is nasty too. Let's postpone this as long as we can 😉 and hope we can figure out something better? 😄
Note that the "regular functions" case is the easy case - you are just saying, there is some type, the compiler can know it, I'm just not giving it a name. The protocol-signature variant is shorthand for a protocol (Rust trait)-associated type, which each protocol impl defines to a concrete type. (I'm not sure how much subclassing we have in guppy - could further subclasses keep "narrowing" the concrete type? Or is it "abstract class may return protocol which subclasses must narrow, anything else = subclasses must not narrow", with the "regular function" falling into the latter category?) I also remember Conor's main complaint about this kind of scheme (I think it applies to both Protocol and |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
In my opinion, the most Pythonic way to add type-class-like features to Guppy is via the notion of protocols as they aready exist in Python (see here for details).
Rationale
The basic idea is that a type implements a protocol if it implements all the methods specified by the protocol. In the spirit of substructural typing, no explicit annotation like Rust's
impl Trait for Type
or Haskel'sinstance Class Type where
are needed. Also, it means that there is only one place where methods of a type go: direclty into theclass
declaration defining the type, just as in Python.My rationale is that this is easier to understand for Python users who are not familiar with Rust's trait system. Essentialy, it's one less thing to learn to pick up the language. Also, this reduces complexity since all methods are in one place, so no need to worry about the orphan rule or overlapping impls. Of course, this also reduces the expressive power, but I think it's flexible enough to cover many use cases we're interested in.
If it proves too restrictive, we could consider non-local
impl Protocol for Type
blocks that are separate from the regular methods as an extension in the future.Defining Protocols
Here, I suggest following Python one-to-one:
If we go with the
Protocol
base, we could even do@guppy
instead of@guppy.protocol
? But I think I prefer the more explicit latter.Also notice the
Self
type showing up here. It should behave similar to https://peps.python.org/pep-0673/ and for now should only be allowed in Protocol signatures.Protocols can also be generic:
Furthermore, protocols can extend other protocols:
Here, for a type to implement
Foo[T]
, it also needs to implementBar
andBaz[T, int]
.Explicit Implementations
Users should be able to explicitly specify that a type they defined implements a protocol by listing it as a base class:
Note that this is optional, the only effect of the annotation is that the compiler will error if the user made a mistake when implementing the protocol (e.g. missing a method or wrong signatures).
Default Implementations
We could also allow default implementations of some protocol members:
To be consistent with Python semantics, we should only allow calling these default members on types that explicitly implement the protocol:
Generic type bounds
To allow generic programming over protocols, we need type variables that are bound to satisfy a number of protocols. Again, the Python 3.12 syntax comes in handy:
For pre Python 3.12, we can allow bounds on
guppy.type_var
declarations:For multiple bounds, Python uses the following syntax:
On top of that, we could also allow Rust-style
Proto1 + Proto2 + Proto3
bounds with the same semantics.Bounds on Generic Implementations
For generic types, certain protocls might only be satisfied if the generic arguments also satisfy some bounds:
Multiple Impls
With more effort, we could also allow overloading to define multiple impls where the compiler picks the first one that fits (see #1000):
We could also try to detect overlapping impls and error in that case, but this will be more difficult to implement.
Syntax Sugar
Similar to Rust's
impl Trait
types, we should add some sugar to define things that are generic in protocols:The main reason to use explicit parameters over this sugar would be if the same tpye should be used multiple times:
Here,
bar
andbaz
are not the same:bar
would accept two argument with different types as long as they both implement the protocol, whereasbaz
requires that both arguments have the same type.Existential return types
Rust also allows
impl Trait
types in return position where they are treated as existential types. We also need this feature in order to support protocols like the following:Unlike Rust, I would restrict this to allow Protocol types in return positions only when defining protocol signatures, not in regular functions.
Implementation Plan
There is quite a lot here, so I suggest to implement the features listed here in multiple steps in the following priority order:
Beta Was this translation helpful? Give feedback.
All reactions