[swift-evolution] Optional Argument Chaining
Stephen Celis
stephen.celis at gmail.com
Wed Dec 13 23:13:19 CST 2017
> On Dec 13, 2017, at 9:53 PM, Erica Sadun <erica at ericasadun.com> wrote:
>
> Chris L had a beautiful solution for an "Unwrappable" protocol that allowed all of the optional sugar to be extended to any type that had a biased `Wrapped` item, allowing it to be used with `Either`, `Wrapped`, etc as well as form the basis for `Optional` itself.
>
> protocol Unwrappable {
> associatedtype Element
> func unwrap() -> Element?
> }
It would definitely be nice to make "Optional" sugar work for non-"Optional" types! (I think the goal for "async"/"await" is to make it work for any continuation, so it's basically Haskell "do" notation for Swift!)
I'm not sure the "Unwrappable" protocol can handle many of the examples I mentioned (accumulative errors, parallelism), though I wonder if it could be designed differently to do so. The applicative structure requires just 2 functions[1]:
protocol Applicative<A>: Functor<A> {
static func pure(_ value: A) -> Self
static func <*> <B>(lhs: Self<(A) -> B>, rhs: Self) -> Self<B>
}
Such a protocol isn't possible in Swift (yet), but it could unlock this kind of sugar for everyone. Here's "Optional" conformance:
extension Optional: Applicative<Wrapped> {
static func pure(_ value: Wrapped) -> Wrapped? {
return value // promoted to Optional.some
}
static func <*> <B>(lhs: Optional<(Wrapped) -> B>, rhs: Optional) -> B? {
guard let lhs = lhs, rhs = rhs else { return nil }
return lhs(rhs)
}
}
We can't conform to such an "Applicative" protocol today, but we _can_ still write and use these functions in a concrete manner! (Delete the conformance and see!)
The original post in this thread had this example:
getPostageEstimate(source: String, destination: String, weight: Double)
If we were dealing with this function in the world of optional arguments, here's how we could use our abstraction:
Optional.pure(curry(getPostageEstimate)) <*> john.address <*> alice.address <*> pure(2.0)
It's not _too_ bad, though it's kinda noisy, we have to maintain a bunch of custom code, we have to curry "getPostageEstimate" before passing it through, we lose our argument labels, and we're living in custom operator world, which can be disconcerting at first.
If Swift provided sugar over this structure, we'd merely need to do this:
getPostageEstimate(|source: john.address, destination: alice.address, weight: 2.0|)
What's even neater is we can use this same format for types that wrap values in other ways! Let's say the addresses are coming from untrusted sources and need to be validated:
getPostageEstimate(|source: try validateAddr(john), destination: try validateAddr(alice), weight: 2.0|)
If both addresses are invalid and "throw", we could get both errors and render them at once to our end user.
Another example: what if we want to get the postage estimate for two addresses that we need to fetch asynchronously:
getPostageEstimate(|source: await myAddress(), destination: await theirAddress(), weight: 2.0|)
Such a syntax allows those requests to run in parallel and not block :)
In these examples, "Optional" promotion becomes applicative promotion ("2.0" is getting wrapped automatically), which might open a can of worms but it's a fun can to think about!
--
[1]: Well, technically it also requires "map", since any applicative is a functor, and it also requires that it follows some math laws.
Stephen
More information about the swift-evolution
mailing list