Let me find out

Finders keepers

The find method is one of the first methods I suggest people use when they’re suffering from eachitis.

EACHITIS: A condition where all solutions involving collections are solved using the each method.

One problem with find is how to interpret the funky method signature.

1
find(ifnone = nil) { |obj| block }  obj or nil

From this signature, you might think you could just give an argument to find and it will be returned if no value is found. But that’s when disaster strikes.

1
2
(1..5).find("nope"){|num| num > 9000}
# => NoMethodError: undefined method `call' for "nope":String

Rude! It turns out, Ruby wants a callable object to be passed in as an argument, which unfortunately, a string is not. The first callable object that comes to mind is the terrorizing lambda. Let’s see what happens if we wrap our default value in a lambda instead of passing a string. We’ll use the stabby lambda syntax to keep it short.

1
2
(1..5).find(->{"nope"}){|num| num > 9000}
# => "nope"

Great! You might be asking yourself, “Why the hell is this a thing?”. Well reader, checkout this example to see if it makes more sense.

1
2
(1..5).find(->{raise "hell"}){|num| num > 9000}
# => Hell has been raised

Lambdas are a way of delaying execution. Here we’re delaying the calling of an exception until it’s necessary. You can also use this to make calls to an API only when needed.

The wheels are turning

The beauty of Duck Typing is that all we need is an object that responds to call. That means we can do some pretty crazy stuff.

Take this class for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
require 'json'
require 'rest-client'
class PostNotFound < StandardError; end
Post = Struct.new(:title, :url)

class Reddit
  attr_reader :subreddit, :posts

  def initialize(subreddit)
    @subreddit = subreddit
    @posts = []
  end

  def search(term, not_found=nil)
    not_found ||= -> {method(:retry_search).curry.call(term)}
    posts.find(not_found){|post| post.title =~ /#{term}/ }
  end

  def refresh
    results = JSON.parse(RestClient.get("http://www.reddit.com/r/#{subreddit}.json"))
    posts.concat(to_posts(results["data"]["children"])).uniq
  end

  private
  def retry_search(term)
    refresh
    search(term, -> {raise PostNotFound, "#{term} not found"})
  end

  def to_posts(post_collection)
    post_collection.map{|post| Post.new(post["data"]["title"], post["data"]["url"])}
  end
end

client = Reddit.new('vegetarianism')
client.search("Ham")

Calling search fails, it calls the retry_search method. It literally calls the ruby Method object. Pretty freakin’ sweet.

Conclusion

Ruby’s language features are one of the most robust I’ve had the pleasure to work with. This mundane method offers a nice splash of awesome by taking a callable object as its requirement.

comments powered by Disqus