Introduction
With the acceptance of SE-0235 (Add Result to Standard Library), Swift added another type to the Standard Library that implements a map(_ : ) function. This is a growing pattern and builds upon the functional aspects of Swift. As such, it seems logical to formalize some of these behaviors so that we can optimize code and enhance productivity.
Proposal
public protocol Mappable {
associatedtype Input
associatedtype Intermediate
associatedtype Output
func map(_ : (Input) throws -> Intermediate) rethrows -> Output
}
Existing Code
Within the existing code, we already have this implemented in Optional, Array, and Dictionary types. All three can be declared to implement Mappable without change.
// Extracted from Optional
func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
// Extracted from Array
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
// Extracted from Dictionary
func map<T>(_ transform: ((key: Key, value: Value)) throws -> T) rethrows -> [T]
The new Result type has a slightly different signature with the transform function not allowing throwing.
// Extracted from Result in SE-0235
public func map<NewValue>(_ transform: (Value) -> NewValue) -> Result<NewValue, Error>
A simple additive solution would be to add conditional conformance.
extension Result: Mappable where Error == Swift.Error {
public func map<NewValue>(_ transform: (Value) throws -> NewValue) rethrows -> Result<NewValue, Error>
}
What This Accomplishes
By extracting a protocol, container types can perform map(_ : ) operation on their contents.
extension Optional where Wrapped : Mappable {
public func map(_ transform: (Wrapped.Input) throws -> Wrapped.Intermediate) rethrows -> Wrapped.Output?
}
extension Array where Element : Mappable {
public func map(_ transform: (Element.Input) throws -> Element.Intermediate) rethrows -> [Element.Output]
}
extension Result where Value : Mappable {
public func map(_ transform: (Value.Input) throws -> Value.Intermediate) rethrows -> Result<Value.Output, Error>
Optimizing
With a consistent signature, we can improve workflow and code readability by adding some further extensions.
extension Mappable {
public func wrappedMap(_ transform: (Input) throws -> Intermediate) -> Result<Output, Error> {
return Result {
return try map(transform)
}
}
}
Since Result is a Mappable type, this facilitates longer chains of mapping functions. With the help of some syntactical improvements, this greatly improves readability.
infix operator |> : LogicalDisjunctionPrecedence
public func |> <MapSource> (lhs: MapSource, rhs: (MapSource.Input) throws -> MapSource.Intermediate) -> Result<MapSource.Output, Error> where MapSource: Mappable {
return lhs.wrappedMap(rhs)
}
The code can then be written like this:
let output = input
|> firstFunction
|> secondFunction
|> thirdFunction