Generic typealiases with additional constraints?

From SE-48 "Generic Type Aliases":

This makes sense: generic typealiases with additional constraints are not "simple type synonyms" for some existing type. If you want to add additional constraints, you have to declare a new type, which may be quite heavyweight.


I wonder if anyone's thought about how to actually "[extend] the model" to support generic typealiases with additional constraints? Would it involve generating implicit type declarations?

5 Likes

You can write this today and it compiles:

typealias ComparableArray<T> = Array<T> where T: Comparable

func f<T>(_ arr: ComparableArray<T>) -> Bool {
    return arr[0] < arr[1]
}
5 Likes

Oh thanks, generic typealiases do support where clauses!
That solves my use case: Add `Sequential{N}` typealiases. by dan-zheng · Pull Request #934 · tensorflow/swift-apis · GitHub

/// A layer that sequentially composes 3 layers.
public typealias Sequential3<L1: Module, L2: Layer, L3: Layer> = Sequential<L1, Sequential<L2, L3>>
where
  L1.Output == L2.Input, L2.Output == L3.Input,
  L1.TangentVector.VectorSpaceScalar == L2.TangentVector.VectorSpaceScalar,
  L2.TangentVector.VectorSpaceScalar == L3.TangentVector.VectorSpaceScalar

I suppose the section in SE-48 should be updated so readers aren't misled into thinking generic typealiases don't support where clauses. I'm not sure when support for that was added though - maybe not at the time of writing.

Where clauses aren't supported on non-generic typealiases though, even though it seems useful:

protocol Differentiable {
  associatedtype TangentVector
}

// Doesn't work:
typealias EuclideanDifferentiable = Differentiable where TangentVector == Self

// Need to define a refinement protocol instead, adding burden to users:
protocol EuclideanDifferentiable: Differentiable where TangentVector == Self {}
typealias.swift:5:52: error: 'where' clause cannot be applied to a non-generic top-level declaration
typealias EuclideanDifferentiable = Differentiable where TangentVector == Self
                                                   ^

I wonder if supporting this makes sense at all?

I found a previous discussion about this, it seems like non-generic typealiases don't have any type parameters for where clauses to bind to.

1 Like

I wonder if supporting this makes sense at all?

I think it probably does (e.g. you can have aliases for constraints in Haskell, Rust has it as an unstable feature, and I think you can do this in Scala too). Of course, this is just my 2c. :slight_smile:

The questions would be the same as for any other language feature:

  1. What are the main problems that this feature will solve and what is out of scope?
  2. How should the feature be designed? (Hopefully in an extensible manner, so that we can accommodate it with potential changes to the generics system.) What is the mental model and specifically how does scoping work?
  3. How should the feature be implemented? (It probably can, given that there's already a way to get the same behavior as someone pointed out in that thread.)
  4. Who will implement it?

What you're asking here is for a generalized existential. You're constraining a nested type of the existential type Differentiable which is not supported.

1 Like

How is that different from the workaround in this comment: Constraining associatedtype of non-generic typealias - #2 by Karl

typealias IsFloatColl<T> = T where T: Collection, T.Element: Strideable, T.Element.Stride: BinaryFloatingPoint

which already works today. Isn't a matter of syntax whether you introduce the T explicitly or implicitly desugar something like

// strawman short syntax
typealias IsFloatColl = Collection where Element: Strideable, Element.Stride: BinaryFloatingPoint

// (somehow) implicitly desugared to
typealias IsFloatColl<T> = T where T: Collection, T.Element: Strideable, T.Element.Stride: BinaryFloatingPoint

? Now it's a separate question of how the desugaring would work, but I don't see how it needs a new form of existentials.

2 Likes

No, it doesn’t need to be an existential at all. It can retain the same “only usable as a generic constraint” restriction that PATs already have.

1 Like

It actually doesn't seem like a trivial desugaring to me, because the IsFloatColl typealias has no generic parameters in one version, but does in the other.

The additional generic parameter impacts usability in the comment you shared:

extension Collection where IsFloatColl<Element>: Any { ... }

I think this matches my mental model as a user!

But there's no "PAT type" for the "generic typealias with additional constraints" to desugar to, hence why the workaround is to define a refinement protocol with the additional constraints.

I'm not saying it is super straightforward (or whether it's even desirable). All I'm saying is: it's probably possible to have some version of the feature which satisfies the presented requirements without requiring generalized existentials.

Hypothetically, we could have some kind of rule saying "introduce parameters based on the RHS" or something, kinda' like how you can write

extension Array where Element : MyProtocol { /* blah */ }

today without an explicitly introduced <Element>.

Same goes for the IsFloatColl<Element>: Any constraint -- we don't really need it once we have a notion of a "protocol alias" (this is purely syntax that is giving us to attach the newly introduced constraints so to speak) -- you can probably just write Element : IsFloatColl and we'll rewrite it for you based on the protocol alias. Again, whether that should be done or not is a separate question (and not one I'm commenting on). But it certainly seems do-able :slight_smile:. It doesn't seem like an insurmountable problem, only a tricky one.

The spirit of your comments makes sense to me.

To users, "generic typealiases with additional requirements" do seem like some kind of syntactic sugar:

protocol Differentiable {
  associatedtype TangentVector
}
func foo<T>(_ x: T) where T: Differentiable, T.TangentVector == T {}

// Syntactically similar to above:
typealias EuclideanDifferentiable = Differentiable where TangentVector == Self
func foo<T>(_ x: T) where T: EuclideanDifferentiable {}
1 Like

My understanding is that @dan-zheng is suggesting something more like this:

typealias EuclideanDifferentiable = exists T where T : Differentiable, TangentVector == T

That is, EuclideanDifferentiable would be an existential type with additional constraints on the wrapped value.

Right, but it would behave like an existential type when used in a generic constraint.

If I wrote this today

typealias Foo<T> = T where T : Sequence, T.Element == Int

Then it is not valid to write

func foo<T : Foo>(_: T) {}

But presumably, you could do that if you instead wrote

typealias Foo = Sequence where .Element == Int

So the syntax being proposed here is quite different than a generic typealias. It is more like a shorthand for a protocol type and a where clause to be expanded at the point of use (even if you decide not to allow it to be used as a type of a value).

In fact the above syntax is almost the same as the proposed syntax for generalized existentials:

Any<Sequence where .Element == Int>

The only difference is that the proposed typealias form forces you to name the new type.

1 Like

Yes, this is pretty much what I had in mind. Maybe what others had in mind too.

This new topic deviated from generic typealiases after @typesanitizer answered my initial question, sorry. This new topic should probably be its own thread to avoid confusion.

So the syntax being proposed here is quite different than a generic typealias. It is more like a shorthand for a protocol type and a where clause to be expanded at the point of use (even if you decide not to allow it to be used as a type of a value).

(emphasis added)

I agree with this interpretation -- I think what is being asked for is a way to bundle up constraints and refer to them by some shorthand, without any additional exists quantifier. (And this is what I was referring to in my original comment about Haskell's constraint aliases and Rust's trait aliases).

1 Like

IIUC, depending on whether Self is specified, it does look like opaque typeAlias, or protocolAlias (both of which we don't have).