A discussion that happened today on discord between @sora, @martinvuyk and I. There’s a few problems with origins that have cropped up that I think bear some deeper discussion.
The first is that, since the origin is part of the type, it greatly limits a lot of traits. For instance, StringSlice
doesn’t implement EqualityComparable
because EqualityComparable
requires the types to be the same. I think it’s somewhat likely that many, many people will write traits assuming that making multiple arguments be typed as Self
, which leaves us with an issue, since those traits won’t be able to interact with origins outside of the most trivial cases. To me, this means that we need a way to make this easy to do. Consider a type Tensor[rank: UInt, origin: MutableOrigin]
, if I were to implement normal, boolean equality on it, I would want to constrain the ranks to be equal, but I don’t care about the origins. Right now, the traits, for lack of a better term, capture too much. We can fix some of this with parametric traits, but I think we may need a “don’t care” for parameters. For example (bit of Rust-flavored syntax)
trait EqualityComparable[T]:
fn __eq__(read self, read other: T) -> Bool:
...
impl[rank: UInt, origin1: ImmutableOrigin, origin2: ImmutableOrigin] EqualityComparable[Tensor[rank, origin]] for Tensor[rank, origin2]:
fn __eq__(read self, read other: Tensor[rank, origin]) -> Bool:
...
This is fairly cumbersome, but something that I would expect to work at some point in the future.
trait EqualityComparable[T]:
fn __eq__(read self, read other: T) -> Bool:
...
impl[rank: UInt] EqualityComparable[Tensor[rank]] for Tensor[rank]:
fn __eq__(read self, read other: Tensor[rank]) -> Bool:
...
This version allows me to ignore parts of the type that this function doesn’t care about. The user only writes the parts of the type they want to “match on”. Now, I have no idea how the trait solver works internally, so I’m working off of some very uneducated guesses as to how I would write one here. My guess is that, since the trait solver has to handle things like wildcard implementations for variadic types at some point, it’s demand-based, meaning that when a parametric trait is implemented for a type, it doesn’t actually try to fill out the entire cross product of input types to serve up when needed, and instead stamps out an implementation when something needs it. My question is whether it would be possible to allow that to match with unbound fields, filling in whatever data is available from the demand, and enabling users to “not care” about anything that isn’t specified. This would also help with the problem of default parameters needing to be overridden, since it forces trait authors to handle them or only rely on the intersection of all possible values of that parameter. This does mean that types would act a bit different in traits vs in normal code, so that does bring context-dependent grammar hazards.
@sora also pitched the idea of “erasable parameters” as a way to do this explicitly,
The conversation then got a bit sidetracked into an adjacent issue. Right now, we need to have our collections decide whether they store values or references. Due to type requirements, List[Pointer[T]]
can only have origins which are available at the time the list is created, which is a problem for long-lived collections, and for vectored IO, which for tasks like serialization typically involves building a List[Span[UInt8, _]]
into some data strucuture so that you avoid copying it all into a single buffer.
Both of these examples have one big thing in common, the problems go away if origins no longer participate in type equality. I’d like to open discussion on whether that is a good idea.
cc: @clattner