annema.me

Ruby's tap method in Swift

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:

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

In the Ruby source tap 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 {
    block(object)
    return 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 type safe to boot:

tap([1, 2, 3], reverseInPlace)

Note that Swift doesn’t have a built-in reverse in place method like Ruby does. If you’re interested in what that looks like; take a look at the gist.

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 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 my incorrect implementations take a look at the revision history of the gist.

This project is maintained by klaaspieter