Nice work! Have you happened to done any measurements to the impact on compiler performance of the new conversion rule for Never
?
I've run the WIP through the compiler performance smoke tests (Make Never the bottom type by owenv · Pull Request #31327 · apple/swift · GitHub), but haven't had a chance to evaluate it much beyond that yet. The early results are promising, but kind of inconclusive. The increase in constraint scopes seems negligible, but there's an ~1% increase in wall time that might be significant. Kind of off topic for this thread, but if anyone has advice on how to measure this more rigorously, I'd appreciate it!
Compiler performance is important, and the core team would want to ensure that this doesn't have an adverse effect before accepting this proposal, so I think it's relevant to the topic at hand. I'll ask around for advice on measuring performance.
Sorry, I didn’t mean to imply performance wasn’t important here; I definitely don’t think this should move beyond a pitch if the impact ends up being significant. I plan on looking into this more soon.
I'm concerned about any change that adds new subtyping rules to the type checker. We already have multiple related constraint kinds (subtype, conversion, argument conversion, operator argument conversion) with complex rules used in subtly different situations. We should be trying to simplify this situation instead of adding new complication to the implementation and mental model of the language.
In particular I would like us to get to the point where common performance problems in the type checker are either fixed or at least the solution is understood (for example, complex math expressions, ??
, and so on) before we add new rules that might have performance impacts.
How does this type check? I understand that never can substitute for any tupe but with type inference how does the second line know that the type is correct?
Is it inferring the type backtracking multiple statements?
Good catch. It can't work. The example should be written:
Ah yes, it can be inferred as Never
as explained below.
As I understand it, fullSizeImage is inferred to be of type Never
, the return type of TODO, and passing it as an argument on the next line is allowable under the pitch.
This explanation is correct, and it's why func TODO() -> Never
with this pitch is preferable to func TODO<T>() -> T
; there's no need for a type annotation that will be redundant once the final implementation is filled in.
I did say this earlier as well.
This:
let x = y ?? fatalError("y shouldn't be nil because ...")
Doesn't provide any more value than this:
// y shouldn't be nil because...
let x = y!
The only real benefit around this area would be to collect the context which fails the assumption, but I don't think that's in any of the argument here.
That makes sense thanks!
On board, however personally I'll be avoiding the syntax:
let x = y ?? fatalError("y shouldn't be nil because ...")
or, if its also allowed:
let x = y ?? throw SomeError()
It makes sense to me that this operator is always value ?? value
. By permitting these Never
expressions – statements really – with semantics that more aligns with a force unwrap, I think it would chip away at my mental models.
So I for one will be defining my own !!
operator as in SE-0217 so that I can use it in place of ??
in those instances:
let x = y !! fatalError("y shouldn't be nil because ...")
Or when I'm feeling cheeky:
let x = y !! "y shouldn't be nil because ..."
Can you please provide some real world examples, when this feature is useful enough to increase language complexity?
As for me, a had neither use nor seen such constructions in practice:
let w: Int = fatalError()
let f1: (Int) -> Never = { x in fatalError("\(x)") }
let plusOne = (fatalError() as [Int]).map {$0 + 1}
All provided examples, if I'm not mistaken, are all about some kind of syntax sugar. The cost is more difficult for understating type system.
Also, fatalError() function is not used extensively in everyday programming. Usually we handle errors with throwing functions or Reslt<> type, and try to recover them, not crash the app. Calling fatalError() is almost always what we don't want, and users don't like app crashes too.
At first look it seems that this feature has not so much sense for ordinary iOS developer. And for others it is possible to express all of that using current language features, although writing a bit more code.
May be, it is useful for library developers or system programming, which is out of my own practice. Despite this, it would be great to understand in what cases it can really be useful.
My favorite example is usage with "either" types (such as Result
), where you want to indicate, via the type system, that one case can never occur. Static type safety is one of Swift's goals, after all.
This is possible to express right now, in Swift 5.0. My question was about examples of real cases or needs, that can't be expressed in Swift 5.0.
optional ?? fatalError("no no no")
is currently not possible (if you don't create a custom @autoclosure () -> Never
overload of ??
operator).
It can only be expressed now for types with unconstrained generic parameters.
typealias NoErrorIntResult = Result<Int, Never>
NoErrorIntResult { // error: Referencing initializer 'init(catching:)' on 'Result' requires the types 'Never' and 'Error' be equivalent
3
}
Yes, it is not possible. But you can do the same with current syntax, without modyfying Type system.
let value: Int
if let number = optional {
value = number
} else {
fatalError()
}
Of course, this is a bit longer. Bu in practice, we rarely call fatalError.
Sure sure, you can also have func ?? <T>(lhs: T?, rhs: @autoclosure () throws -> Never) rethrows -> T
to achieve the syntax, but my point is that this overload was rejected back then because we should eventually get the bottom type and gain that functionality from natural sub-type relation.
Hmm, we can write NoErrorIntResult.success(3), and error will disappear. Or is there something I'm missing?