[Withdrawn] Add Mappable Protocol to Standard Library


(Holly Schilling) #1

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

(Brent Royal-Gordon) #2

Have you tried this?

error: repl.swift:9:1: error: type 'Optional<Wrapped>' does not conform to protocol 'Mappable'
extension Optional: Mappable {}
^

repl.swift:2:20: note: protocol requires nested type 'Input'; do you want to add it?
    associatedtype Input
                   ^

repl.swift:3:20: note: protocol requires nested type 'Intermediate'; do you want to add it?
    associatedtype Intermediate
                   ^

repl.swift:4:20: note: protocol requires nested type 'Output'; do you want to add it?
    associatedtype Output
                   ^


error: repl.swift:9:1: error: type 'Array<Element>' does not conform to protocol 'Mappable'
extension Array: Mappable {}
^

repl.swift:2:20: note: protocol requires nested type 'Input'; do you want to add it?
    associatedtype Input
                   ^

repl.swift:3:20: note: protocol requires nested type 'Intermediate'; do you want to add it?
    associatedtype Intermediate
                   ^

repl.swift:4:20: note: protocol requires nested type 'Output'; do you want to add it?
    associatedtype Output
                   ^


error: repl.swift:9:1: error: type 'Dictionary<Key, Value>' does not conform to protocol 'Mappable'
extension Dictionary: Mappable {}
^

repl.swift:2:20: note: protocol requires nested type 'Input'; do you want to add it?
    associatedtype Input
                   ^

repl.swift:3:20: note: protocol requires nested type 'Intermediate'; do you want to add it?
    associatedtype Intermediate
                   ^

repl.swift:4:20: note: protocol requires nested type 'Output'; do you want to add it?
    associatedtype Output
                   ^

The problem is, your definition of Mappable is incorrect. There is not one particular Intermediate type and Output type; you can use any Intermediate type and a related Output type will be selected for you. You would need to define a protocol like:

public protocol Mappable {
    associatedtype Input
    associatedtype Output<Intermediate>

    func map<Intermediate>(_: (Input) throws -> Intermediate) rethrows -> Output<Intermediate>
}

But if you try that, you'll get a different error:

error: repl.swift:3:26: error: associated types must not have a generic parameter list
    associatedtype Output<Intermediate>
                         ^~~~~~~~~~~~~~

Swift doesn't yet support this feature, or anything similar that would allow you to express Mappable's requirements correctly. Perhaps it will someday—higher-kinded types are on the generics manifesto's "Maybe" list.


(Paul Cantrell) #3

I do like the spirit of this proposal, and would be encouraged to see the language evolve in directions that support it.


(Holly Schilling) #4

I wrote this based on code that I have working in production. I assumed I could extract the behavior into a protocol. You know what they say about assuming... I'll write up a new proposal that has something concrete that can actually be implemented.