Funkify and Pattern-Proc: Messing with Ruby for Fun (but probably not profit)

Partial application is awesome. I love it. Same goes for functions as first-class citizens. I wish these were features in every language. I’m working in Ruby right now, and every time I use map() or reduce(), I find myself wishing I had them.

class MyClass
  def negate(x)
    -1 * x
  end

  def add(x, y)
    x + y
  end
end

obj = MyClass.new
values = [1,2,3]
# this is needlessly verbose
values.map do |val| 
  obj.negate(val) 
end # [-1, -2, -3]

# this is disgusting
values.map(&obj.method(:negate))

# Ruby, why u no allow this?
values.map(&obj.negate) # although dropping the & would be even better
# And how about instead of this
values.map do |val|
  obj.add(2, val)
end
# we could just do this?
values.map(&obj.add(2))

Funkify to the Rescue

To overcome these limitations (and because it sounded fun), I started looking for a gem that would help with writing code that supports partial application. Yes, you can do it manually with Procs, but it gets awkward fast and stops looking at all like Ruby code.

Eventually I found the gem funkify. It lets you make ordinary Ruby methods curry-able. Basically it works like this:

class MyClass
  auto_curry

  def add(x, y)
    x + y
  end
end

# ...

obj = MyClass.new
obj.add(3,4) # returns 7 - essentially normal method invocation
obj.add # returns a curried Proc which takes two arguments and the same behavior as add
obj.add(2) # returns a curried Proc which takes one argument and adds the number 2 to it, e.g.
obj.add(2).(8) # returns 10
# Now this is more elegant
values = [ 1, 2, 3 ]
values.map(&obj.add(2)) # returns [ 3, 5, 7 ]

The above use of map() can leverage partial application, and it does not require the use of method() (though it still requires the ampersand). So I find it much more elegant.

Funkify also lets you compose functions (left-to-right with | or right-to-left with *). So, for example, instead of this:

posts = BlogPostsFinder.posts_for_user(current_user)
updated_posts = BlogPostsSeenUpdater.update(Time.now, posts)
serialized_posts = BlogPostsSerializer.serialize(false, updated_posts)
render json: {
  posts: serialized_posts
}

which is clunky (it assigns variables for one use, namely to pass along to the next line), or this:

render json: {
  projects: BlogPostsSerializer.serialize(false, 
    BlogPostsSeenUpdater.update(Time.now, 
      BlogPostsFinder.posts_for_user(current_user)))
}

which I find hard to read, you can do this:

def action_find_posts_update_seen_and_serialize
  render json: {
      posts: pass(current_user) >=               
        BlogPostsFinder.posts_for_user |          
        BlogPostsSeenUpdater.update(Time.now) |  
        BlogPostsSerializer.serialize(false)
  }
end

which allows for the same logic, in intuitive order, and without the clunky assignments.

Note that pass(x) >= sends x into the pipeline of the composed function at the beginning of the expression. It is equivalent to call()‘ing the composed Proc:

(BlogPostsFinder.posts_for_user |          
  BlogPostsSeenUpdater.update(Time.now) |  
  BlogPostsSerializer.serialize(false)).(current_user)

I was pretty impressed with what I could do using funkify. Now I can pretend that Ruby is as cool as Haskell. <wink>

Performance Worries

I was a little bit worried about what adding partial application might do to performance. I ran a few tests, and basically the cost is roughly the same as if you wrote the logic manually with curried Procs. In a tight loop doing only simple operations inside each invocation, there is a noticeable overhead versus plain Ruby methods and blocks. But in something with more realistic overhead, such as a GET on a Rails Controller action, the overhead is negligible even in a loop — compared to the cost of going through the router, hitting the database, etc.

Testing

One major snag remained: tests. Using our previous example, if I wanted to test my controller action by mocking out BlogPostsFinder.posts_for_user(), BlogPostsSeenUpdater.update(), and BlogPostsSerializer.serialize() (to test the relationship between these functions as opposed to their individual functionality), under normal circumstances a mocking library like Mocha gets the job done fairly easily:

user_posts = [post1, post2, post3]
updated_posts = [updated_post1, updated_post2, updated_post3]
BlogPostsFinder.expects(:posts_for_user).with(some_user).returns(user_posts)
BlogPostsSeenUpdater.expects(:update).with(current_time, user_posts).returns(updated_posts)
BlogPostsSerializer.expects(:serialize).with(false, updated_posts).returns(some_json)

However, now that we are using partial application, BlogPostsSeenUpdater.update() isn’t called with both current_time AND the list of posts. Instead it is called with just current_time, and that partial application returns a proc. You could do:

BlogPostsSeenUpdater.expects(:update).with(current_time).returns( ->(posts) {
  if posts == user_posts
    updated_posts
  else
    raise "no match"
  end
})

But that seems like a lot of work, especially if you partially applied only the first argument to a four argument function, and then had to check all three remaining arguments for equality. Moreover, if you want to deal with other values for the posts argument, you have to delineate them in the Proc, rather than listing them out as separate expectations. So how can we make testing easy with funkify?

My Contribution: pattern-proc

Enter pattern-proc, a gem I created to make testing partial application functions easier in Ruby (though it also lets you do some other interesting / dangerous things). The gem is still in its infancy, but here’s what the “expectations” look like in pattern-proc:

BlogPostsFinder.pattern(:posts_for_user).with(some_user).returns(user_posts)
BlogPostsSeenUpdater.pattern(:update).with(current_time, user_posts).returns(updated_posts)
BlogPostsSerializer.pattern(:serialize).with(false, updated_posts).returns(some_json)

Huh — looks more like Mocha, doesn’t it? I no longer have to worry about how the invocations are partially applied; I just write expectations for the full method.

Pattern-proc creates a method on the object that supports partial application (not unlike funkify). However this method (actually the Proc underneath it) supports specifying multiple mappings from argument values to return value. This allows you to delineate the return value with pattern matching. Here’s an example:

orwell_adder.pattern(:add).with(2,2).returns(5)
orwell_adder.pattern(:add).with(1984) do |x|
  "duckspeak #{x}"
end
orwell_adder.pattern(:add).with do |x, y|
  x + y
end
orwell_adder.add(2,2).should == 5
orwell_adder.add(2,5).should == 7
orwell_adder.add(1984, 9).should == "duckspeak 9"
orwell_adder.add(1984, 11).should == "duckspeak 11"
orwell_adder.add(11, 22).should == 33

Pattern-proc lets you specify all arguments and a return value, or some arguments and a Proc that accepts additional arguments and determines the return value. This is handy if you need a custom matcher or just want to raise an exception when an unexpected invocation occurs.

Putting pattern-proc together with funkify let me write clearer tests for partial-application functions. I also thought it would be handy to add a Module you can include in your classes to let you compose a function with patterns:

class MyClass
  include PatternProc::ClassMethods

  pattern(:make_sandwich).with(["peanut butter", "jelly"]).returns "PB & J"
  pattern(:make_sandwich).with(["bacon", "lettuce", "tomato"]).returns "BLT"
  pattern(:make_sandwich).with do |ingredients|
    "unknown sandwich with #{ingredients.join(", ")}"
  end
end

obj = MyClass.new
obj.make_sandwich(["peanut butter", "jelly"]).should == "PB & J"
obj.make_sandwich(["bacon", "lettuce", "tomato"]).should == "BLT"
obj.make_sandwich(["peanut butter", "tomato"]).should == "Unknown sandwich with peanut butter, tomato"

Final thoughts

It’s been a lot of fun playing around with Ruby this way. And in the long term, I think this might even be a reasonable way to write production-ready Ruby code. For now, I’m just enjoying experimenting. I plan to keep building up these mechanisms on side projects until I can make a final call about using them in major projects.
 

Conversation
  • Penn Taylor says:

    In your initial chunk of code demonstrating motivation, I was confused why you wrote the following code, which is “needlessly verbose”:


    values.map do |val|
    obj.negate(val)
    end

    where I think most Rubyists would have written the equivalent, but more compact:

    values.map{|v| obj.negate(v)}

    That said, I fully agree the curried version is cleaner.

    • Will Pleasant-Ryan Will Pleasant-Ryan says:

      I used the do/end syntax mainly because of personal preference. I believe the convention is to use curly braces for one liners and do/end for multi-line blocks, but toggling between the two never made much sense to me. So I end up using do/end everywhere and use multi-line block style even when there is only one line in the block. Just my aesthetic taste. I admit you can be more concise in terms of characters by using curly braces, though it’s still the same grammatical complexity.

  • Comments are closed.