Ciaran McCreesh’s Blag
Now with 17% more caffeine
Recursive Lambdas in Ruby using Object#tap
Paludis represents an ebuild’s homepage as a dependency-style heirarchy, since PMS allows use-conditional blocks like:
HOMEPAGE="http://example.org/foo gtk? ( http://example.org/foo-gtk )"
Given this, we want a quick way of extracting the URLs using the Ruby bindings. One could of course use a function:
def extract_homepage_recursively(spec) case spec when AllDepSpec, ConditionalDepSpec spec.each { | child | extract_homepage_recursively(child) } when SimpleURIDepSpec puts spec end end extract_homepage_recursively(id.homepage_key.value) if id.homepage_key
But that’s rather crude. It would be much nicer to use a lambda, since we don’t need a new name for something we’re only using once.
Unfortunately, recursive lambdas are sometimes rather pesky. There are various solutions, most of which involve passing the lambda as a parameter to itself. In the general case, there’s the infamous Y combinator, but since Ruby has language-level recognition for recursion there’s no need to resort to that kind of silliness. We could just use a variable:
recurse = lambda do | recurse, spec | case spec when AllDepSpec, ConditionalDepSpec spec.each { | child | recurse.call(recurse, child) } when SimpleURIDepSpec puts spec end end recurse.call(recurse, id.homepage_key.value) if id.homepage_key
But that’s still a pointless waste of a name. We can do better than that.
Ruby 1.9 adds an Object#tap method, which is rather nifty. Ruby 1.8 doesn’t have it, but we can provide it easily:
if not Object.respond_to? :tap class Object def tap yield self self end end end
Then, we don’t need a variable or a horrid untyped lambda calculus construct at all:
lambda do | recurse, spec | case spec when AllDepSpec, ConditionalDepSpec spec.each { | child | recurse.call(recurse, child) } when SimpleURIDepSpec puts spec end end.tap { | r | r.call(r, id.homepage_key.value) } if id.homepage_key
If you assign a name to the lambda (as you did in the second example) you also don’t need to pass in the lambda… so you can say:
rec = lambda do | spec |
case spec
when AllDepSpec, ConditionalDepSpec
spec.each { |child| rec.call(child) }
when SimpleURIDepSpec
puts spec
end
end
rec.call(id.homepage_key.value) if id.homepage_key
Of course then you don’t get to use the nifty tap method :)
Hrm, I didn’t realise Ruby let you get away with that. But apparently it does, with the rather bizarre result that
a = ainitialises a with nil.Yep, lambda’s create lexical closures (sort of) just like in other functional programming languages, so if you name the lexical closure it give you recursive access to it.
I agree with you on the a = a initializing to a to nil though, since if you instead said x = y and neither were defined you’d get an error (or if just y wasn’t defined).
The reason I wasn’t expecting it to work is that the local name rec doesn’t exist at the time the lambda is created. This gives an error:
x = lambda { puts y }
y = 1
x.call()
because y doesn’t exist at the time the lambda is created. But there seems to be a horribly weird special case for assigning an rvalue mentioning the lvalue to an lvalue.
Isn’t the “puts” slightly iffy? Try defining
class Proc
def selfcall(*args)
self.call(self, *args)
end
end
Then you can define and call an anonymous recursive lambda and have it return a useful result.
Enjoy, Mike
Strictly speaking, the Y-combinator doesn’t have to be untyped, it’s just that most type systems aren’t powerful enough to handle it in its conventional, anonymous form. Adding equi-recursive types to System-F gives us this definition (call-by-name since it’s simpler):
fix = All T . lambda f: T -> T .
((lambda x: (Rec X . X -> X -> T) . f (x x))
(lambda x: (Rec X . X -> X -> T) . f (x x)))
There you have it: a type-safe Y-combinator. The call-by-value version is left as an exercise to the reader; it isn’t too different. Likewise, conversion to iso-recursive types (much more common in real languages) also requires a little extra effort.
Correction:
sed s/Rec X . X -> X -> T/Rec X . X -> T/g
Guess I got a little carried away…
Pingback: Dando um “tapa” no Ruby: o método tap | Ruby Brasil