Senseful
Senseful

Reputation: 91711

`map` function for non-collection types in Swift

Swift has several high-order functions like map which encourage functional programming paradigms (e.g. chaining functions together).

For example:

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) }

This works because it started with an array of [User] and each element was mutated along the way.


Now imagine we're trying to manipulate a CGPoint instead of an array with the following traditional code:

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)

This code is disadvantageous because: (A) it requires coming up with an intermediary variable name at each step of the function, and (B) you can easily make a logic error that compiles just fine (e.g. CGPoint(x: point.x / 2.0, y: flippedPoint.y)).

You could attempt to solve (A) by chaining it all on one line:

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

However, this is messy, and even more error prone (e.g. notice the two calls that need to be made to CGPoint.flipHorizontally(point).


Now imagine what it would look like if you could call map on a 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) }

That code is much cleaner than either of the two alternatives.


Is there any way to call map (or other high-order functions) on a non-collection type?


I realize one workaround is to wrap/unwrap in an array, but I'm hoping there is a less clunky way:

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

A second workaround is to wrap in Optional, but this only allows map and flatMap and is also clunky:

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

Upvotes: 0

Views: 209

Answers (1)

matt
matt

Reputation: 535315

It seems you are asking to program in a functional style. Isn't it just a question of arming yourself with functions beforehand? Unclear what your posted flipHorizontally and init(at:) are actually supposed to do, but let's just make up some fake functionality for them. Then if these are common things to do, you could inject them into CGPoint as extensions, as you suggested:

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
    }
}

And there's your chain:

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

But if those are not common things to do and you want to express them functionally at the point of use (through anonymous functions, like with map), then you could start with this sort of extension:

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

And now you can talk like this:

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))}

That's a very map-like chain, surely.


You asked whether you'd have to write an extension for every type you wanted to inject munge into. Not exactly; you could just write a generic protocol and have all those types adopt the protocol:

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

Finally I should point out that the Combine framework does let you do exactly what you're describing, actually using the word map, even though that is not the Combine framework's intent:

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}

Upvotes: 1

Related Questions