Reputation: 91711
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
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