Функция `map` для не-коллекционных типов в Swift - PullRequest
0 голосов
/ 17 марта 2020

Swift имеет несколько функций высокого порядка , таких как map, которые поддерживают парадигмы функционального программирования (например, объединение функций в цепочку).

Например:

users                                         // e.g. [User]
  .map(Person(from:))                         // e.g. protocol Person { init(from: User) }
  .map { $0.name }                            // e.g. protocol Person { var name: String { get } }
  .map(Person.normalizedName(fromName:))      // e.g. protocol Person { static func normalizedName(fromName: String) -> String }
  .map(User(username:)                        // e.g. protocol User { init(username: String) }

Это работает, потому что оно начиналось с массива [User], и каждый элемент был мутирован по пути.


Теперь представьте мы пытаемся манипулировать CGPoint вместо массива с помощью следующего традиционного кода:

let point: CGPoint = input
let flippedPoint = CGPoint.flipHorizontally(point)
let halvedPoint = CGPoint(x: flippedPoint.x / 2.0, y: flippedPoint.y)
let rect = CGRect(at: halvedPoint)

Этот код невыгоден, потому что: (A) он требует, чтобы на каждом шаге создавалось имя промежуточной переменной функции, и (B) вы можете легко сделать ошибку logi c, которая прекрасно компилируется (например, CGPoint(x: point.x / 2.0, y: flippedPoint.y)).

Вы можете попытаться решить (A), связав все это в одну строку :

CGRect(at: CGPoint(x: CGPoint.flipHorizontally(point).x / 2.0, y: CGPoint.flipHorizontally(point).y))

Однако, это грязно и еще более подвержено ошибкам (например, обратите внимание на два вызова, которые нужно сделать на CGPoint.flipHorizontally(point).


Теперь представьте, что это будет выглядеть, как если бы вы могли позвонить map на CGPoint:

point                                          // e.g. CGPoint
  .map(CGPoint.flipHorizontally(_:))           // e.g. extension CGPoint { static func flipHorizontally(_ point: CGPoint) -> CGPoint }
  .map { CGPoint(x: $0.x / 2.0, y: $0.y) }
  .map(CGRect.init(at:))                       // e.g. extension CGRect { init(at: CGPoint) }

Этот код намного чище, чем любой из двух вариантов.


Is Есть ли способ вызвать map (или другие высокоуровневые функции) для не-типа коллекции?


Я понимаю, что один из обходных путей - это обернуть / развернуть массив, но я надеюсь, что есть более неуклюжий способ:

[point]                                          
  .map(CGPoint.flipHorizontally(_:))           
  .map { CGPoint(x: $0.x / 2.0, y: $0.y) }
  .map(CGRect.init(at:))                       
  .first!

Второй обходной путь - обернуть в Optional, но это допускает только map и flatMap и также неуклюже:

Optional(point)
  .map(CGPoint.flipHorizontally(_:))           
  .map { CGPoint(x: $0.x / 2.0, y: $0.y) }
  .map(CGRect.init(at:))!

1 Ответ

1 голос
/ 17 марта 2020

Кажется, вы просите программировать в функциональном стиле. Разве это не просто вопрос вооружения себя функциями заранее? Непонятно, что ваши опубликованные flipHorizontally и init(at:) на самом деле должны делать , но давайте просто сделаем некоторые поддельные функции для них. Тогда, если это обычные вещи, вы можете добавить их в CGPoint как расширения, как вы предлагали:

extension CGPoint {
    func flipHorizontally() -> CGPoint {
        CGPoint(x:y, y:x) // or whatever
    }
    func halvedPoint() -> CGPoint {
        CGPoint(x:x/2.0, y:y)
    }
    func makeRect(at:CGPoint) -> CGRect {
        CGRect(origin:at, size:CGSize(width:100,height:100))
        // or whatever
    }
}

И вот ваша цепочка:

let r =
    CGPoint(x:7,y:9)
        .flipHorizontally()
        .halvedPoint()
        .makeRect()

Но если это не обычные вещи, и вы хотите express функционально использовать их в точке использования (через анонимные функции, например map), тогда вы можете начать с такого рода расширения:

extension CGPoint {
    func munge<T>(f:(CGPoint)->T) -> T {
        f(self)
    }
}

И теперь вы можете говорить так:

let r =
    CGPoint(x:7,y:9)
        .munge { CGPoint(x:$0.y, y:$0.x) }
        .munge { CGPoint(x:$0.x/2.0, y:$0.y) }
        .munge { CGRect(origin:$0, 
                          size:CGSize(width:100,height:100))}

Это очень похожая на карту цепочка, конечно.


Вы спросили, нужно ли вам писать расширение для каждый тип, который вы хотите ввести munge в. Не совсем; Вы могли бы просто написать обобщенный протокол c и сделать так, чтобы все эти типы приняли протокол:

protocol Mungeable {
    func munge<T>(f:(Self)->T) -> T
}
extension Mungeable {
    func munge<T>(f:(Self)->T) -> T {
        f(self)
    }
}
extension CGPoint:Mungeable {}
// and so too for other types

Наконец, я должен отметить, что платформа Combine позволяет вам делать именно то, что вы описываете , фактически используя слово map, хотя это и не является целью фреймворка Combine:

var r : CGRect!
_ = Just(CGPoint(x:7,y:9))
    .map { CGPoint(x:$0.y, y:$0.x) }
    .map { CGPoint(x:$0.x/2.0, y:$0.y) }
    .map { CGRect(origin:$0, size:CGSize(width:100,height:100)) }
    .sink {r = $0}
...