Perhaps it would make sense to have an automatic conformance to Unwrappable for ExpressibleByNilLiteral types. While you can still compare the result to nil, it should universally make sense to optional-chain and if let unwrap on types conforming to ExpressibleByNilLiteral.
The issue with this is that it doesn't help much: self will still be a wrapped type, and the whole purpose of having Unwrappable is to get to the wrapped value easily!
Is such a thing out of scope for the language? We have optional sugar, throwing sugar, and proposed async/await sugar that all amount to the same abstraction. Any reason not to unify them and allow for other types to take advantage when the type system allows?
Another approach to avoiding the one-true-future-type problem of C# could be to have a general language feature for chaining continuations through a monadic interface. Although this provides a more general language feature, it still has many of the shortcomings discussed above; it would still perform only a shallow transform of the current function body and introduce a temporary value at every point the coroutine is "awaited". Monads also compose poorly with each other, and require additional lifting and transformation logic to plumb through higher-order operations, which were some of the reasons we also chose not to base Swift's error handling model on sugar over Result types. Note that the delimited continuation primitives offered in this proposal are general purpose and can in fact be used to represent monadic unwrapping operations for types like Optional or Result.
Monads that represent repeated or nondeterministic operations would not be representable this way due to the one-shot constraint on continuations, but representing such computations as straight-line code in an imperative language with shared mutable state seems like a recipe for disaster to us.
I like the idea of this proposal, but I don't like the implementation for three reasons.
The first is that it doesn't support using the unwrapped value as an lvalue. This is a subtle but important part of force unwrapping. It can be fixed by replacing unwrap() with a read-write property:
protocol Unwrappable {
associatedtype Wrapped
var unwrapped: Wrapped? { get set }
}
The second issue is that this is not very useful in a generic context because there's no initializer. I would add one. (In theory, with the initializer included you might not need the property; you could instead create a new instance when assigned. This may be less efficient in some cases, though.)
protocol Unwrappable {
associatedtype Wrapped
var unwrapped: Wrapped? { get set }
init(_ unwrapped: Wrapped)
}
Finally, I don't like the fact that optional chaining always produce Optionals, not variants of the current type. Unfortunately, this is impossible to fix without some sort of higher-kinded type feature, but if we had that, it might look like this:
Unfortunately, introducing Mapped<Rewrapped> later would probably be a breaking change, so if we ever expect to be able to do this, I think Unwrappable should wait until we can. That's kind of disappointing, but I don't want to be stuck with a subpar design.
Totally agree with all your points. It might make sense to wait until we get HKT, or until we have some kind of confirmation that HKT won’t be coming for a while.
If someone implements this, we should keep an eye on its impact on type-checker performance, since it will have the effect of making every ? and if let overloaded. We can likely optimize for the common case of these operations applying to Optional, but it's something to be mindful of.
Practically, this functionality seems to me like it's only really useful for Optional and Result, or types isomorphic to them. It's not a fully general monadic binding operation, since it doesn't have the power to suspend or repeat computation of the environment after the binding. Furthermore, if let is not a particularly great tool for unwrapping Result, since it provides no convenient way to get the 'failure' payload on the 'else' branch (unless we add an else let x syntax or something like that.) If we standardize Result, would a generic Unwrappable protocol still carry its weight?
I don't think this can be considered outside of @stephencelis 's complementary proposal for autogenerating enumeration accessors. The ability to do optional-chaining on any biased enum would be convenient, when you're interested in grabbing information or applying a method more than determining which case load to unpack.
product?.storeToInventory()
Unwrappable doesn't limit itself to A-B choices like Optional and Result. It says there is a particular type (which can be a tuple), that can be unwrapped from certain cases to be operated upon (optional chaining), default values provided for other cases (nil-coalescing), and a formal way to extract values from one or more cases in guard conditions.
It can be useful for quick unit testing if you just want to test for failure/success. However, it's probably still better to use helpers akin to StdlibUnittest's helpers:
StdlibUnittest Helpers
public func expectThrows<ErrorType: Error & Equatable>(
_ expectedError: ErrorType? = nil, _ test: () throws -> Void, ${TRACE}) {
do {
try test()
} catch let error as ErrorType {
if let expectedError = expectedError {
expectEqual(expectedError, error)
}
} catch {
expectationFailure("unexpected error thrown: \"\(error)\"", trace: ${trace})
}
}
public func expectDoesNotThrow(_ test: () throws -> Void, ${TRACE}) {
do {
try test()
} catch {
expectationFailure("unexpected error thrown: \"\(error)\"", trace: ${trace})
}
}
If this were an active problem, then we should consider adding an explicit way to ignore a thrown result. If it isn't an active problem, then I'm not sure I see the concern.
Thanks for writing this up Erica! Feel free to put yourself as the first author :).
As you know, I'm very much +1 on hoisting the special case hacks for optional out of the compiler and putting them as general language features that Optional can use. That said, to be clear, my attraction to this is mostly idealogical (all of Swift should work that way) not driven by practical concerns like Result specifically.
Taking a step back, I can see a couple of different directions we could go here:
We could generalize current compiler magic for optionals (e.g., if let binding, chaining, x! being an lvalue, pattern matching x? patterns) with individual extension points, allowing types to opt into each piece individually. This would also argue for the stdlib changing to make ?? and other library features for optionals be defined in terms of protocols that other types can opt into.
We could take this proposal and deem all of these as "optional like" and have a single protocol that unifies them.
We could say that optional is magic and well known in swift, but that types can opt into being implicitly convertible in specific places, e.g. Result could say that it is implicitly convertible to optional in a chaining position.
To me, there are a few questions that we can evaluate to drive the direction: for example, whenever we get back to pattern matching of structs, we will have to support matching against computed properties. When/if that happens, we could say that a x? pattern is syntactic sugar for {.some=x, *} (or however that pattern is spelled). This would allow Result and other types to participate without language extensions.
You can use x? for multiple enum cases, like the grades example upthread:
for grade? in classGrades { ...process passing and failing grades, ignoring incompletes... }
Admittedly there aren't a huge number of biased same-type enumerations. Either is unbiased. JSON is unbiased. But a result-y type with multiple error payload styles is biased, as is any single fail/many success styles like the grades one.
It would be useful and cleaner to have the else follow the same if-let convention. It makes the code concise, taking advantage that the binary state nature of Unwrappable. No reason to do more seemingly needless "if" checking in the else scenario.
For example,
if let value = result {
// use result
} else {
if let error = result.fail.value {
throw error
}
Fatal.unreachable("Should never get here") // Dave DeLong's Fatal umbrella type
}
could turn into
if let value = result {
// use result
} else let error { // mirrors if-let syntax
throw error
}
// or guard let value = result else let error { throw error }
or
if let value = result {
// use result
} else(let error) { // mirrors switch-case syntax for associative enums
throw error
}
Thinking about this if-let-else-let syntax above, one could make this happen with something like the protocol below. Internally, the elseValue would need to be checked to make sure it was non-nil. I see that the use of ! in this scenario is now deprecated.
What if Element and ElseElement are non-nil? What would be Invalid but your type signature supports it. The correct way would be for unwrapWithElse to return a sum type. Like Result<V,E>... Now you the same as the initial proposal but with Result instead of Optional and you could argue that Optional<T> is a spacial case of Result<V,Never>.
While the idea seemed rather neat initially, on second (and third) thought I'm not sure I'd even want something like this in Swift.
I don't like programming patterns that are based on discarding information, and in general using existing syntactic sugar for things that are not perfectly expressed by it seems backwards: it would be better to have more types that express specific semantics in the standard library, and sugar that works specifically for them.
For example, if a type has a success/failure semantics, sugar should carry over the failure (if it's the case) and not discard it: try? is a mistake as a language feature, that's exactly where the language is supposed be annoying.
The key is to offer the right sugar for making the language expressive and concise (I also think that the do/catch construct is a mistake) while preserving all the information: in my case, I very frequently find myself wanting to accumulate some alternate state (e.g. errors) in a chain of method calls, so I can handle everything at the boundary, and for that a specific applicative functor implementation of the Result type is perfect (but not in the standard library, and not sugared).