Fixing Angle Bracket Blindness (and unboxing existentials while we're at it...)

Here's the idea:

Proposed New Syntax

func doSomething (with value: <Value>)

is less blinding than:

// Current blinding syntax
func doSomething <Value> (with value: Value)

Allows Constraints

func first (of collection: <C: Collection>) -> C.Element

Angle Brackets Must Appear On First Usage

func assign (_ newValue: <Value>, to destination: inout Value)

Works With Associated Types

func feed (_ food: <Recipient: Eater>.Food, to recipient: Recipient) -> Recipient.FormOfThanks

Allows Generic Computed Properties

var anyKindOfSevenYouWant: <T: ExpressibleByIntegerLiteral> {
    .init(integerLiteral: 7)
}

Works With Local Variables

let foo: <T> = 7

desugars to:

typealias T = Int
let foo = 7

thereby allowing:

let maximumInteger = T.max // This is `Int.max`

Unboxing Existentials

let boxedUpValue: some Equatable = ...
let value: <Value> = boxedUpValue
if let dynamicallyCasted = someOtherValue as? Value {
    print(value == dynamicallyCasted)
}

Extensions On Any

extension <T> {
    func mutated (by mutation: (inout Self)->()) -> Self {
        var copy = self
        mutation(&copy)
        return copy
    }
}

I proposed this a while ago with many more words and with a few differences. In this post I've incorporated a few of the main pieces of feedback from the original post and I've skipped all of the description because the syntax mostly speaks for itself. Let me know what you think!

3 Likes

There’s already an idea that some P in argument position will alleviate the need for angle brackets on cases where type parameters don’t need to be named:

func doSomething(with value: some Any)

A couple of these overlap significantly with other ideas that have been recently discussed, so I'll point those out here.

As Kyle noted, this use case is subsumed by opaque parameters, a Swift 5.7 feature that was recently accepted and implemented as SE-0341. This example would be written as:

func doSomething(with value: some Any)

SE-0326, which is under active review right now, composes with SE-0341 to allow one to specify, e.g., the element type of generic collections. Your example doesn't translate precisely, but it's very much in the same space. I do think it makes the case for your suggested syntax a much harder sell, because it's eliminating the angle brackets entirely in a subset of the use cases.

I think generic computed properties would be a good feature on its own, and is orthogonal to the syntax you describe.

There's an active pitch on implicitly opening existentials that's in the same spirit as this.

Another separable and good feature, which we often call "parameterized extensions".

Doug

15 Likes

@Douglas_Gregor Thanks for the thorough consideration, much appreciated. The idea actually originally came to me because I was considering how we might spell the unboxing of existentials, and I hit upon this syntax:

func receivesExistential (_ existential: any Equatable) {
    let unboxed: <Value> = existential
    // `Value` is now the local name of the dynamic type contained within the existential.
    // and `unboxed` is the existential value with its type now known and named.
}

After thinking of this syntax I pondered why it seemed like a natural spelling to me, and that led me to think about what such a syntax would logically mean if it were used in other situations where a type name is expected, for example in function arguments:

func doSomething (with value: <Value>)

or as the type being extended:

extension <T> {

}

and I realized that almost no matter where I used it it yielded either an improved method of doing something that's already possible or the ability to do something previous not possible.

With that being said, I have two concrete questions:

  1. Do you think that there's an argument to be made that there's value in having a single unifying syntax for all of the features I described, even though it is also possible to build them all separately?

  2. In my somewhat quick read of the new existential-unboxing feature that you mentioned it seemed to me that it would not be possible to write this function:

static func == (lhs: any Equatable, rhs: Any) -> Bool {
    let unboxedLHS: <LHS> = lhs
    if let dynamicallyCastedRHS = rhs as? LHS {
        return unboxedLHS == dynamicallyCastedRHS
    } else {
        return false
    }
}

Am I correct that this would not be possible?

Thanks Doug!

It's possible. You’re correct to point out that one could not refer to the opened type of lhs by name. At first glance this may seem like a big deal, but in fact it’s not an issue:

To preserve the signature verbatim, you'd use two functions:

/* static can't be declared outside a type */
func == (lhs: any Equatable, rhs: Any) -> Bool {
  _equals(lhs as some Equatable, Any) // (1)
}

func _equals<T: Equatable>(lhs: T, rhs: Any) -> Bool {
  if let rhs = rhs as? T {
    return lhs == rhs
  } else {
    return false // Probably not wise, but we're not here to discuss that.
  }
}

Now, you'll notice that Doug has proposed implicit existential opening, so in fact any arguments that can be passed to == can be passed to _equals without explicitly opening the existential first (shown at 1), making the entirety of the outer wrapper function redundant. In other words, if you're not under ABI stability constraints, you don't need to go through this trampoline at all and can just write:

func == <T: Equatable>(lhs: T, rhs: Any) -> Bool {
  if let rhs = rhs as? T {
    return lhs == rhs
  } else {
    return false // Still probably not wise, but we'll move on.
  }
}

Notice that not only is this very straightforward to write, it's a function that you can write already; Doug's proposed solution would make it Just Work to pass an existential box as the first argument. So in fact the genius of Doug's proposed solution is that it makes what you're asking for not just possible but also simultaneously unnecessary. Nice.

2 Likes

Abstractly, I could agree to the argument that it's useful to express some of these ideas. Concretely, we already have two levels of syntax for describing parameterization---generic parameter lists and some---and adding a third doesn't automatically mean there's more expressiveness. What I see here are a few different features that could be expressed using the syntactic ingredients we already have. The missing piece is to design how these features work, without focusing on the syntax.

Xiaodi answered this splendidly.

Doug

3 Likes