Skip to content

Instantly share code, notes, and snippets.

@klaaspieter
Last active March 28, 2017 02:59
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save klaaspieter/9d8ce4485007fcb973b7 to your computer and use it in GitHub Desktop.
Save klaaspieter/9d8ce4485007fcb973b7 to your computer and use it in GitHub Desktop.
Ruby's tap method in a Swift playground (original blog post: http://www.annema.me/blog/post/edit/rubys-tap-method-in-swift)
import Foundation
/*:
Ruby has a nice method called `tap`, which I wanted to try and port to Swift. To learn what it does, let’s take a look at [Ruby’s documentation](http://ruby-doc.org/core-2.2.0/Object.html#method-i-tap):
> Yields self to the block, and then returns self. The primary purpose of this method is to “tap into” a method chain, in order to perform operations on intermediate results within the chain.
Yeah, right. I don’t really know what that means. Googling yields (no pun intended) many contrived examples like these:
[1, 2, 3, 4].tap &:reverse! # [4, 3, 2, 1]
In Swift the same can be written as:
tap([1, 2, 3, 4], reverse) // [4, 3, 2, 1]
A less contrived example would be:
User.new.tap do |user|
user.name = "Yukihiro Matsumoto"
user.known_for = "Ruby"
end
In the latter case tap is acting as a sort of [improptu builder](http://www.annema.me/the-builder-pattern-in-swift). In Swift:
tap(user) {
$0.name = "Chris Lattner"
$0.knownFor = "Swift"
}
Note that the previous example requires every property of the user to be mutable (`var`). This example should only be seen as a comparison to Ruby. Please don’t do this in your actual code unless you absolutely have to.
That said, we’ve established a public API, it’s time to start implementing. If you want to follow along; I’ve made a [playground available](https://gist.github.com/klaaspieter/9d8ce4485007fcb973b7).
In the Ruby source [tap](https://github.com/ruby/ruby/blob/e28c3d5df4f5abc83e0d2de34e7ebf675c96a307/object.c#L684-L689) is implemented like so:
VALUE
rb_obj_tap(VALUE obj)
{
rb_yield(obj);
return obj;
}
Translating that to actual Ruby:
class Object
def tap
yield self
self
end
end
My first attempt was a copy of Ruby’s implementation, but in a global function. I’m not a huge fan of the global functions in Swift, but it works out nicely because we won’t have to extend any existing objects.
func tap(object: AnyObject, block: (AnyObject) -> (AnyObject)) -> AnyObject {
return block(object)
}
Great, we threw out all type safety in an attempt to be as dynamic as Ruby. It also doesn’t compile.
Let’s introduce some generics:
*/
func tap<A>(var object: A, block: (inout A) -> ()) -> A {
block(&object)
return object
}
/*:
This does compile, and it’s completely type safe. For example this works because we’re providing and return an array.
*/
func reverseInPlace<T>(inout array: [T]) {
var left = 0
var right = array.count - 1
while left < right {
let temp = array[left]
array[left] = array[right]
array[right] = temp
left++
right--
}
}
tap([1, 2, 3], reverseInPlace)
/*:
A less contrived example would be:
*/
tap(NSDateComponents()) {
$0.day = 18
$0.month = 06
$0.year = 1986
$0.calendar = NSCalendar.currentCalendar()
}.date
/*:
If you got this far, you might be wondering: do we need a `tap` function? The answer is: maybe. In Objective-C we could use [GCC code block evaluation](http://www.annema.me/builder-and-gcc-code-block-evaluation) to similar effect. In Swift we should be able to write:
let date = {
let c = NSDateComponents()
c.day = 18
c.month = 06
c.year = 1986
c.calendar = NSCalendar.currentCalendar()
return c
}().date
Unfortunately this confuses the type system. It is unable to infer the return type of the block.
Instead we have to write:
*/
let date = { () -> NSDateComponents in
let c = NSDateComponents()
c.day = 18
c.month = 06
c.year = 1986
c.calendar = NSCalendar.currentCalendar()
return c
}().date
/*:
For now, `tap` is a safer and arguably more readable alternative but it’s likely that the Swift compiler will solve the entire issue more elegantly in the future.
**Update**: Several people on Twitter have pointed out to me that my implementation wasn’t an exact reproduction. The post has been updated with a better implementation. If you’re interested in the process and my incorrect implementations take a long at the revision history of [the gist](https://gist.github.com/klaaspieter/9d8ce4485007fcb973b7/revisions),
*/
@klaaspieter
Copy link
Author

Yes. I updated the post on my blog to reflect this, but forgot to update the playground as well. Thanks for pointing this out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment