[Pitch] Compile-Time Constant Values

I agree with needing to provide stronger guarantees than constexpr in C++. The new consteval feature in C++20 aims to do just that because the above lack of clarity is often confusing. We need to keep these ideas in mind for the potential future extensions that discuss compile-time evaluation.

I can imagine a few cases where this could be very useful:

  • The property wrapper example in the pitch that requires an initializer parameter to be a compile-time constant value can also have other parameters that can be runtime values - it can still be of benefit to enforce this property for only some of the parameters here.
  • When using a library with available source, in the future we could potentially allow a library function to perform compile-time computation on only the subset of its parameters that are compile-time-known values. For example, to emit custom compile-time errors based on those parameter values.

I’m very happy to see this moving forward. Compile time evaluation is a missing piece of the language that other newish languages are coming out from the get go with.

I’m bot a huge fan of ‘const’ tho. Having ‘let’ in the language already makes the const wording to confusing imo. I would much rather go with something more specific like ‘comptime’.

I’ve recently come across some newish languages (Jai, Zig…) that offer the option to run arbitrary functions at compile time with the exact same language (as opposed to a limited form of meta programming). It’s cool that we start with constants but is there a clear end goal where we can run Swift at compile time or would we rely on a separate meta system?

6 Likes

A few questions and a bike-shedding suggestion:

  • Because all the examples used in the pitch are literals, are non-literals (e.g. enum cases) within the scope?

  • It's not mentioned in the pitch, but I feel it's implied that const values can be passed to non-const parameters. If this is true, can they be be precondition'd?

    func divide(_ dividend: Double, by divisor: const Double) -> Double {
        precondition(divisor != 0)
        return dividend / divisor
    }
    
    divide(0, by: 0) // does this crash in runtime or error in compile-time?
    
  • Similarly, in the future direction of compile-time expressions and functions, can compile-time known run-time crashes automatically become compile-time errors:

    let x = Int("1", radix: 0) // can this become an error in compile-time?
    if true { fatalError() } // can this become an error in compile-time too?
    
  • I agree with the opinions upthread that const is ambiguous and "brevity over clarity". Something like compileTime is better imo.

1 Like

The term is not rigorously defined but I would describe enum cases as literals too.

Definitely they can.

This is more about a future feature where functions could be compile-time interpreted and generate compile-time failures when passed constant values. Such a feature would allow libraries to do the same thing as the standard library types do e.g. generate errors on things like (256 as Int8), so a NoneEmptyCollection could compile-time error when initialized with [].

I think this is definitely a goal, and is related to this pitch, but not really part of this pitch. Should probably be discussed elsewhere unless it specifically pertains to how this feature behaves.

These maxims must be applied with pragmatism in mind rather than as hard rules. For example, you could say func is brevity over clarity, except that the clarity of function is vanishingly small. This isn't such an extreme case, but compileTime is really very verbose for something that may become moderately common in Swift source, and I find it hard to believe it would make anything more clear except for the very earliest encounter of the feature, which is not what we should be optimizing for (and even then, it really doesn't tell you what you need to know, you still need to look it up to understand).

6 Likes

I see people coming up with different names, but nobody has proposed constexpr yet as a name

I'd prefer something more like compexpr or comptime since IIUC the "constness" of the expression is merely a side effect of the source code not being changed while it's being compiled, rather than anything inherent to the expression itself. Come to think of it, even if that's not true, it seems like "compile-time" is much more the point than "constant" (a concept for which we already have let, at least for value types).

Unrelated to @vegerot's post, should we be thinking about how this might interact with "pure" functions? Is one a subset of the other? They seem at least related, but I've been up for I think 20 hours and it's not an area I know a ton about anyway.

1 Like

Since const implies let, couldn't const replace let, and const be used as a parameter qualifier?

let s = "foo"
const s = "foo"

func foo(const input: Int) {...}

struct Bar {
    static const title = "Bar"
}

struct Baz {
    const title: String
    init(const title: String) {
        self.title = title
    }
}

protocol NeedsConstGreeting {
    static const greeting: String // { get }
}
4 Likes

Semantically, const is more a guarantee about the value and not about how the variable is stored (in contrast to let, var and static). So I believe that having const as a specifier for the value or type might be another good approach:

let s = const "foo"
let i: const Int = 1

That also stylistically matches using const as a type prefix in function signatures:

func constantFunction(string: const String) // ...

My reasoning for placing it there in functions is that const is in a similar category to inout (it's about how the value is passed to the function and what values are valid, unlike a result builder where it is changing how the value is interpreted).

7 Likes

Personally, I don't like the word const for this. let is already understood to be "a constant", and const in other languages has a different meaning from what is proposed here.

What about using fix for fixed as a three-letter sibling to var and let?

var a = "foo"
let b = "foo"
fix c = "foo"

A fixed value could become synonym to compile-time constant, which needs to be differentiated from a run-time constant. const makes that differentiation more complicated.


If we're not going to add it not as a new variable type, I think the non-shortened expression fixed would be better though:

fixed let d = "foo"

Or with @stackotter's suggestion above which I like better:

let e = fixed "foo"
7 Likes

I think const is clear when used as a prefix for expressions and types because there is a clear distinction between the semantics of let and const, however I definitely see where you’re coming from. Because certain languages (looking at you js) have made very different use of const (in js, const doesn’t even mean immutable, it’s more akin to a let variable containing a reference type - the value itself still being mutable).

fixed makes sense to me in terms of expressions and variable types, however as a specifier in function arguments it almost sounds like the parameter has to be the same every time (which wouldn’t make sense, but it’s how it reads). But that might just be a bias, because I already have an understanding of what const might mean from other languages.

Full everyday words like fixed might also lead to confusion when used with types like Bug or Point. To me const Bug() sounds more clear than fixed Bug(). Even when it’s not forming a play on words I still find myself leaning towards const, but I can’t pinpoint why other than I’m used to const

1 Like

Thanks for detailed response. Some more questions:

  1. If the function throwable, will it be possible to initialize constant without do-catch block? If the function throws then we get compile error. let numbers: NonEmptyArray<Int> = try .init()
  2. If the function returns optional value, will it be possible to initialize constant without if-let binding or force unwrapping? let url: URL = URL(string: "")

Thanks for the the answers to my questions!

I've always thought of literals as a piece of hard-coded data written in code, but I can see it being understood differently. If enum cases are covered in this pitch, do you think things that behave like them such as static Self typed properties like Double.pi are covered in this pitch too?

I understand and agree to an extent with the argument of pragmatism. However, I think the pragmatism of clear and distinct meaning should take precedence over that of less verbosity. As many upthread have pointed out, "constant", which "const" is short for, is too close in meaning in English to "immutable". This in my opinion makes the feature less teachable if we use the const spelling, because both "constant" and "immutable" have been used in documentation to describe let properties/values/declarations.

A search for "constant" in the latest TSPL shows 340 result:

I did not check each of the 340 results, but all those that I checked use "constant" for let. Meanwhile, "immutable" only shows up 3 times:

This shows a clear history of "constant" being used as the primary description for let. It will be difficult for people new to the language to learn that even though we call let "constants", we only write const for some of them in code, and they are treated differently from other "constants", because they're const. Even for people already familiar with the language, const in my opinion still messes with the mental model after they have learnt about it. Because although writing in Swift is not writing in English, your understanding of the English word still seeps in when you use the word.

I agree with this statement from you:

but I think it's somewhat off the point.

I believe what many of us are saying isn't that we want longer keywords, but keywords that disambiguate from existing usage. The difference between the pitched feature and let is not the constant-ness, but the compile-time known-ness of the constant-ness. This is why keywords like compileTime and predefined are suggested as alternatives. If they are too long, then they can be trimmed shorter, but whether they're in long or short forms, they're more clear than const.

And here is a counter-example: def is to define as const is to constant. Although def is shorter than function (and func), just as const is shorter than compileTime, it shouldn't be a more pragmatic choice than function is, because it doesn't disambiguate from other declaration keywords.

7 Likes

This is an elegant solution. @JuneBash has already proposed this idea.

Oh yes, you're right, there. I'm late to the party, and all credits go to @JuneBash :+1:

The idea is great, I really like it. Sometimes we need to remove something instead of adding to make things better. If two people propose the same solution independently, then I guess the idea is great.

Thanks. To me the biggest advantage in introducing const on the side of let and var is that the fact of being "compile-time" is not an attribute of the type. There's no const String type. There are String values that are known at compile time.

This comes with some neat consistency, and unlocks compile-time constant values in pattern matching and loops as well:

// Variable declaration
var a = "foo"
let a = "foo"
const a = "foo"

// Pattern matching (in future directions?):
if case let .foo(bar) = value { ... }
if case var .foo(bar) = value { ... }
if case const .foo(bar) = value { ... } // OK if `value` is itself const

// Loops (in future directions?)
for element in array { ... }
for var element in array { ... }
for const element in array { ... } // OK if `array` is itself const

There is a more subtle consistency: const as a legal parameter qualifier (just as var used to be until SE-0003):

func foo(var input: String) { }   // ❌ Legal until Swift 3
func foo(input: String) { }       // Implicit let
func foo(const input: String) { } // New
1 Like

I'm re-reading the rationale notes for SE-0003:

‘var’ in a parameter list is problematic for a number of reasons:

  • Parameter lists currently allow both “inout” and “var”, and real confusion occurs for some people learning swift that expect “var” to provide reference semantics.

This is not an argument against const.

  • Parameter lists are not “patterns” in the language, so neither “let” nor “var” really make sense there. Parameters were patterns in a much older swift design, but that was eliminated a long time ago and this aspect never got reconsidered.

This is difficult to interpret for me.

  • “var” in a parameter list is odd because it is an implementation detail being foisted on the interface to the function. To be fair, we already have this with "API names" vs "internal names”, but this is a bigger semantic implementation detail.

Not this time: const is not an implementation detail, but a real part of the function signature (a change in the const-ness of a parameter is an api change).

Wouldn't it make sense to move the const marker out of the parameter list (presumably every param would need to be marked anyway?) to indicate that the whole function or initialiser can be used in a compile-time evaluated context provided that all of the parameters were also known at compile time? Like so:

func parseInt(_ string: String, radix: Int = 10) const -> Int? { ... }

This would also align well with async and throws, I think, even if const functions might be limited to synchronous calls only, for example.

Functions defined like this would of course need to be defined in terms of compile-time evaluable parts only in its body, but OTOH the function could also be called at run time with run-time known arguments.


I agree with others that const in place of let or var might be a natural place to declare compile-time constant values.

But could compile-time const var be a thing for constructing values procedurally for instance in the body of a compile-time evaluated function? If not, why not? (Edit: See Gwendal's and my posts below, no need for const values inside a const function where everything's implicitly const unless called at run time.)

1 Like

This is a neat idea, and really inline with this post by @Ben_Cohen: [Pre-pitch] SwiftPM Manifest based on Result Builders - #28 by Ben_Cohen

Maybe you'd not declare a variable as const var, but you'd need functions that can be executed at compile time:

func makeValue() const -> String { ... }
const value = makeValue()

I'm not sure "functions that can be executed at compile time" are part of the pitch, but they are a nice extension indeed.

For this, I think we'd need reconst, or else we'd be quite crippled (not even able to perform string concatenation):

// New declaration of String concatenation in the stdlib:
func + (const lhs: String, const rhs: String) reconst -> String { ... }

// OK now
func makeValue() const -> String {
    "foo" + "bar"
}
1 Like