I moved some code* into extensions of the protocol below today and it got me thinking. Aside from question mark sugar, are there reasons we still have Optional other than historical ones? I have not thought about this for very long, but it seems like Optional might as well just be a typealias for this:
extension Result: ExpressibleByNilLiteral where Failure == Optional<Success>.UnwrapError {
public init(nilLiteral: ()) {
self = .failure(.init())
}
}
public extension Optional {
/// Represents that an `Optional` was `nil`.
struct UnwrapError: Error & Equatable {
public init() { }
}
}
let none: Result<Int, _> = nil
(Optional.UnwrapError would be called something else if Optional didn't exist.)
Note: You can't actually turn either of them into a real property wrapper yet. [1][2]
/// A type that represents a `get throws` accessor.
public protocol ThrowingPropertyWrapper<WrappedValue> {
associatedtype WrappedValue
var wrappedValue: WrappedValue { get throws }
}
extension Optional: ThrowingPropertyWrapper {
/// - Throws: `UnwrapError`
public var wrappedValue: Wrapped {
@inlinable get throws {
switch self {
case let wrapped?: return wrapped
case nil: throw UnwrapError()
}
}
}
}
extension Result: ThrowingPropertyWrapper {
/// - Throws: `Failure`
public var wrappedValue: Success {
@inlinable get throws { try get() }
}
}
* E.g.
infix operator =?: AssignmentPrecedence
public extension ThrowingPropertyWrapper {
/// Assign only non-throwing values.
static func =? (wrappedValue: inout WrappedValue, self: Self) {
try? wrappedValue = self.wrappedValue
}
}
var value = "đť"
value =? "đ¸" as Optional
value =? Result { "đŞ" }
Optional<T> is arguably isomorphic to Result<T, Never> (rather than a Result with an error type), but As for what Optional can do that Result can't â nothing comes to mind, as Result is a more general type. If you're willing to ignore any historical baggage, ignore ABI stability and source compatibility, and be willing to reimplement all of the optimizations that are currently associated with Optional (which may not exist for Result<T, Never>; I'm legitimately not sure), then theoretically, you could replace Optional with a typealias.
It doesn't sound like you're proposing this as an idea, but if you were interested in getting rid of Optional entirely in favor of Result, you would need to contend with the increased mental overhead of using Result everywhere (e.g., while writing this, I had to double-check the order of the generic parameters to Result [<Success, Failure>, or <Failure, Success>?] â which happen to be the reverse of, say, Either from Haskell). Optional is also likely a better name than Result for storage purposes, for instance.
Ah, right â of course. You and @anon9791410 are both right; I was thinking about the consuming side of switching on Result (which is possible to do with Result<T, Never>), but you could never construct an instance unless the parameter were lazy. My mistake!
Optional<T> is isomorphic to Result<T, Void>. And if you want to go further, Bool is isomorphic to Optional<Void>, which is isomorphic to Result<Void, Void>. There are tradeoffs to modeling them all as specializations of the same type or as separate types.
Joeâs not talking about isomorphisms that preserve the semantics or even the validity of every existing language operation on the respective types (e.g., you of course canât use a value of type Option<()> as the condition of an if statement), but for a given isomorphism you could consistently redefine/reimplement whatever functionality you wanted.
The idea of mapping Optional<T> to Result<T, Void> is that the error in this case is a) fixed and b) trivial, so that matching a .failure(()) case can be âemulatedâ in the Optional case by matching .none and immediately constructing the empty tuple if itâs really needed for some reason. The Error value itself is meaningless.
You can imbue all of these types and values with additional informal meaning beyond their formal structure, and the isomorphism may not preserve that (e.g., I donât actually consider .none to âtrulyâ represent an error case the way .failure doesâsomething could be missing for all kinds of reasons that arenât âreallyâ failures).
No, sometimes it's helpful to just know which Optional type threw.
public extension Any? {
/// The wrapped value, whether `Wrapped` is an `Optional` or not.
/// - Throws: `Any?.UnwrapError` when `nil`,
/// or `Any??.UnwrapError` when wrapping another `Optional` that is `nil`.
var doublyUnwrapped: Wrapped {
get throws {
switch self {
case let doubleWrapped?? as Self?:
return doubleWrapped
case _?:
throw Self?.UnwrapError()
case nil:
throw UnwrapError()
}
}
}
}
Yeah, if you really wanted to define the type you need the error type to be able to conform to protocols, which (currently) excludes tuples. As you note youâd really need to define an empty struct to get a type thatâs valid Swift today, so the use of Result<T, Void> is a bit hand-wavy.
the thing is, i think that the error type should (statically) carry the Wrapped type of the original optional, so the actual result type would look like
let _:Result<T, NilOptionalError<T>>
and at this point i feel like thatâs just too many angle brackets to be a desirable replacement for T?âŚ
If NilOptionalError<T> is trivial then you still have an isomorphism, because you could construct NilOptionalError<T>() immediately upon matching a .none in the Optional case.
I donât think anyone is seriously proposing any âreplacementâ here, or suggesting that such replacement would be desirable. The original question was whether Optional solves any problems that Result doesnât. The language chooses to model Optional as a separate type rather than a specialization of Result with a trivial error type, and provides different ergonomics and APIs between the types. Swift could have chosen to model things differently (since the types are isomorphic), but itâs a choice that comes with tradeoffs, as Joe mentions.
Yeah, programmers tend to use "isomorphic" rather loosely as a fancy way of saying that two things have roughly equivalent semantics, but Joe here is using the term with a specific technical meaning--two objects in a category are isomorphic if there is an invertible morphism between them. So Optional<T> and Either<T, Void> are isomorphic because we can define a Swift function f that transforms an Optional into an Either, and another function g which transforms an Either into an Optional, and these functions f and g are inverses of each other, in the sense that their compositions are the identity on Optional and the identity on Either, depending on the order in which you compose them. Another example is that (Int, (String, Float)) and (Int, String, Float) are isomorphic types, but they're certainly not the same type.
I'm not sure if I totally understand the question, because you've basically written this just above:
If the error type isn't totally fixed but is still fixed per wrapped/success type, then you've still effectively turned Result from a generic type with two type parameters into a type with one type parameter, e.g.,
I agree with that, but ? already ought to work better with Result, even if fully subsuming Optional, including reappropriating ? in the form you mentioned, would be a bad idea.
let print = Result { { Swift.print($0) } }
(try? print.get())?("đ¨ď¸")
Why is this not
print?("đ¨ď¸")
Maybe Result is not special, and it's just time that try? ⌠turned into � for all chains with throwing expressions, or types that wrap them.
You two know the history better than I do. From a more external perspective, it seems more like Optional doesn't make use of errors because it predates errors. I didn't see developers really start annotating frequently with throws, rather than try?-ing and else return-ing until the recent days of concurrencyâthe Swift 1.x style of Optional had a lot of inertia.
that just returns the payload value if the result is a success(_:) case. unfortunately any time this idea comes up, it inevitably gets bogged down in the âcase pathsâ evolution quicksand, so weâre kind of stuck with
(try? result.get())
for now.
this doesnât really line up with my memory; throws has been around for a very long time and i definitely remember seeing a lot of it before concurrency became a thing. then again this is all completely anecdotal so who knowsâŚ