SE-0452: Integer Generic Parameters

Hi Swift community,

The review of SE-0452: Integer Generic Parameters begins now and runs through 19th of November, 2024.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

https://github.com/apple/swift-evolution/blob/main/process.md

Thank you,

Ben Cohen
Review Manager

31 Likes

Just as a data point - and a use case beyond collection sizes - back in the day I used the C++ equivalent of this feature when wrapping Carbon Event APIs which had lots of four-character codes stored as integers. So I was able to use templates to, for example, associate the 'wind' code with the WindowRef type, so I could add a measure of type safety to the interface.

3 Likes

Reviewers may be interested in this PR against the swift-playdate-examples repo, which uses this feature in combination with others pending review to implement a fixed-size array type:

11 Likes

Overall: +1

My knowledge and review is fairly light - the primary use case In desperately after is the fixed size pieces, and this is a crucial element of enabling that.

The β€˜let’ in the generic parameters feels verbose, like it’s extra ceremony, but functionally I’m fine with it. I don’t know if lighter alternatives would be available, or what I’d even propose as an alternative. I do suspect that relatively new Swift developers encountering that definition will understand it easily, but may be notably confused on its limitations and how to use it themselves.

All that said, very much in favor of the feature, and with the declarations as they stand in this proposal are very worth the effort to get stable fixed size types in Swift.

1 Like

Nov 19, 2023 (looks like its fixed already)? Definitely would have liked this a year earlier. :smile:

I read & like the whole proposal. Integer parameter syntax, overloads, extensions, future directions (especially constant bindings and generic arithmetic). Looks like everything was peer reviewed and improved through multiple iterations, which ultimately makes it easier to read and understand for everyone. Great job to everyone involved!

I 100% agree with the distinction of lowerCamelCase for generic parameters and UpperCamelCase for generic types.

While I agree that the default type should be Int, I also think the compiler should not allow negative integer types from the get-go and UInt should not be the default type. Then again you can still create negative bound types (like arrays) to this day, so its not that big of a deal I guess.

I do have one question: Would we be able to write an extension where the generic integer is even (or odd)? How would this proposal handle this:

struct Something<let n: Int, Element> {
}

extension Something where n % 2 == 0 { // n is even | would this be allowed, a future direction, or not allowed?
}

This would be useful to split it into two (or more) half-sized Somethings, similar to SIMD's lowHalf and highHalf.

+1, but one thing that seems missing from this is the ability to define protocols with integer generic parameters. Something like:

protocol Foo {
  associatedtype let n: Int
}

I'm wondering if any thought was put into this. May be worthwhile having a quick sketch and adding to the "Future Directions" section just to make sure the proposed design will work with protocols (even if that isn't implemented for v0).

11 Likes

Since Int has different sizes on 32- and 64-bit platforms, does that mean integer generic types cannot be safely used with distributed actors until the ability to use fixed-size integer types arrives in the future?

7 Likes

+1, proposal lays down the path to dependent typing in Swift. I hope one day to be able to use types generic over isolation as a more expressive alternative to sendability checking.

Proposal is very conservative, and I could not think of anything that would hinder future extensions.

I am in favor of using some keyword to highlight that generic parameter is a value.

If was thinking if let is future-proof enough. If in the future we decide to introduce a stricter version of let-declarations that can be evaluated at compile time, which use a different syntax, e.g. using a const keyword, then one might argue that the hypothetical const keyword in generic signature better reflects current limitations. But also existing type generic arguments allow to create new types by substituting parameters known only at the run time. So probably the same eventually will be true for value generic arguments. So my conclusion is that let is indeed future-proof.

2 Likes

The "Syntactic separation of value and type parameters" subsection in "Alternatives Considered" (link) seems like it ends early.

It may be worth considering a design that separates value generic parameters by putting them in a different set of brackets separate from the type generic parameters, like Vector[count]<Element>. This would avoid the need for disambiguation if an arbitrary expression can be used as a count in the future.

Is there a missing paragraph that explains why that alternative was rejected?

1 Like

Hm, distributed calls only care about how a type is mangled so if the mangling is going to be Swift.Int the target function identifier would be the same I guess, and runtime would get the parameter type from "local" function on the recipient, so I think an invocation could work? With integers basically this means that decodeNextArgument<Int>() throws is called in the system impl, and if the decoded value would be larger than an int can fit you'd throw in there...

If sending a large 64 bit type value and recipient is a 32bit system, the actor system going to be invoked with decodeNextArgument<Array<SomeLargeLongValue>>() throws so I wonder if that'd have to crash as we cannot express this "too large numeric value" on the recipient side. I'd definitely ask @Joe_Groff to work with me to make sure such types wont cause unexpected crashes but just some "cannot find type" as we try to getTypeFromName it -- I expect that would just return nil if the integer is too large...?

2 Likes

More of a process comment, but I had a major sense of deja-vu seeing this proposal, and it may be useful to add a link to the original pitch thread.

As for the proposal, big +1 from me for enabling this extension of the generic constraint system. That said, as I originally mentioned in the pitch thread, I would like to see the constraints being taken further, specifically calling out that functions that take an index as an argument could perform compile time checks to make sure that index is in range (or greater than some constant) to automatically take a fast path without runtime checks, or throw a compile-time error, for instance.

This only really works if there is a fallback for when the parameter cannot be determined within range at compile time, and is further made more expressible by possibly returning an optional or different type in those circumstances. I would also expect suitable error messages if a non-constraint variant is not provided, but a non-constant argument is passed in: "Passing a non-constant argument requires that functionName() provides a non-constrained variant" (or similar).

2 Likes

If the actor system obtains the Array<SomeLargeLongValue> type metadata by asking the runtime (such as by asking for _typeByName given a mangled name or something like that), we should make sure that mangled type names involving too-large integer arguments fail to demangle. Does the actor system recover gracefully if an argument's type fails to be instantiated? I could imagine failure to access an argument type already coming up because of version skew among nodes and situations like that.

Right, as long as we get no type out of the _typeByName it should be fine and just fail the call gracefully.

I have read the proposal (and some of the original pitch thread) and am generally in favour, but with some reservations:

  1. I do think that the declaration of the integer generic parameter does need some kind of separation from generic type parameters so the use of let n: Int seems preferable to just a bare n, but could this cause the let keyword to be overloaded in an unfortunate way?
  2. The possibility discussed at the end of the document to place value parameters separately from type parameters (especially to avoid the parsing problems inherent in the use of <> for generics) should be explored further.
1 Like

The proposal includes an upcoming feature flag:

  • Upcoming Feature Flag: ValueGenerics

The source compatibility section of the proposal does not list any source incompatibilities.

Was this supposed to be an experimental feature flag?

Or is there some syntax or behavior that the ValueGenerics flag will enable once this feature is improved and implemented in a release version of Swift?

You are right, there should be no source backward compatibility issues with the proposal. Could the proposal template be updated to describe this distinction? As is, it only recommends "Upcoming Feature Flag".

1 Like

I will draft something and put up a PR. I think the description of Upcoming Feature Field could also be clarified more.

1 Like

Generally +1. My personal use case is range-restricted integer values (e.g. "my binary protocol has a 4-bit int packed into a larger bitstream and I want to cause 8bit-to-4bit truncations to be caught at compile-time already"), and this is one half of being able to implement this.

As George pointed out, I'd want to be able to declare protocols that nail down generic arguments, too.

That said, I'm fine with first defining the general approach with Ints and then later widening it to UInt8 and Int128 or whatever. After all, I'd still need some static assert-like facility (contracts? prerequisites?) for my particular use case.

I like the proposal and what it enables. In particular, I'd love to see the possibility mentioned in future directions of specifying the size of parameter packs.

1 Like

After using generic Int value parameters with Vector I'm pretty happy with them! The only really sharp edge I have hit is that you can't constrain the generic parameter with a function parameter hint like you can with generic type parameters. For example:

This works with type parameters:

func inferType<T>(_: T.Type = T.self) -> T { fatalError() }

let x = inferType(String.self)
type(of: x) // String.self

This doesn't work with int value parameters

func inferValue<let newCount: Int>(_: Int = newCount) -> Vector<newCount, Int> { fatalError() }

let x = inferValue(10) // error: unable to infer return type

I would really like to see this restriction lifted in this proposal.


Another QOL change I would like is making any generic parameter of a type, a static member of the type.

I found myself needing to refer to Vector.count frequently, e.g.:

struct StrongType {
  typealias Bytes = Vector<13, UInt8>
  var bytes: Bytes
}

// use: StrongType.Bytes.count 
1 Like