Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sugars for constraining associated types #1640

Open
kyouko-taiga opened this issue Jan 6, 2025 · 0 comments
Open

Sugars for constraining associated types #1640

kyouko-taiga opened this issue Jan 6, 2025 · 0 comments

Comments

@kyouko-taiga
Copy link
Contributor

kyouko-taiga commented Jan 6, 2025

In Swift we can write this:

func f<T: Sequence<Int>>(_ xs: T) { ... }

That is assuming Sequence is defined as follows:

protocol Sequence<Element> {
  associatedtype Element
  ...
}

This syntax is certainly convenient but I really think that we should consider alternatives. The two main issues are:

  • The syntax doesn't really make any sense if we think about what T<A, B> usually means;
  • The syntax strongly relates to the idea of generic traits, which we may possibly want in the future.

Taking inspiration from Scala (and Rust), we could use something along these lines:

fun f<T: Sequence{ Element == Int }>(_ xs: T) { ... }

Admittedly that isn't much shorter than the expanded form, which is <T: Sequence where T.Element == Int> but:

  • we gain the ability to omit possibly repeated occurrences of T in the where clause;
  • we gain the ability to name (or omit) any associated type without imposing any declaration order;
  • we can use the same syntax to mix conformance and equality constraints at any depth, e.g., Sequence{ Element: Sequence{ Element == Int } }
  • if we really want to elide Element we can borrow Swift's trick to write Sequence{ Element }.

I picked angle brackets because square brackets denote fixed-size arrays or projections and parentheses denote the application of a term constructor.


To expand a little on the first point, the use of <> in Hylo (and Swift for the most part) relates to type applications. For example, Array is a type constructor and Array<Int> applies that constructor to produce a type. A trait is a type parameterized by its conforming type. In fact, here's a relatively accurate description of what happens under the covers when we use traits in Hylo, expressed in C++:

// This is a trait declaration.
template<typename T>
struct StringConvertible {
  virtual std::string describe(T const& receiver) const = 0;
};

// This is a conformance declaration.
struct IntIsStringConvertible: public StringConvertible<int> {
  std::string describe(int const& receiver) const override { ... }
};

// This is a conformance constraint.
template<typename T>
void print(T const& item, StringConvertible<T> const& witness) {
  std::cout << witness.describe(item) << std::endl;
}

That's a lot to say that when I see Sequence<Int> I think about "a witness of Int's conformance to Sequence" rather than "a sequence of Ints". But more crucially, a witness of T's conformance to Sequence is not a type constructor and so it does not make any sense to apply it.


SE-0346 does mention generic traits:

We believe that constraining primary associated types is a more generally useful feature than generic protocols, and using angle-bracket syntax for constraining primary associated types gives users what they generally expect, with the clear analogy between Array and Collection.

For the reason mentioned above I believe that the analogy does not hold and is in fact misleading.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant