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?
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 beprecondition
'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 likecompileTime
is better imo.
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).
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.
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 }
}
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).
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"
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
Thanks for detailed response. Some more questions:
- 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()
- 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.
This is an elegant solution. @JuneBash has already proposed this idea.
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
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.)
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"
}