Python Context Manager Compared to Ruby DSLs
Python has the with
keyword that is analogous to using
in C#. It is used to manage resource access and wrap a block of code with setup and cleanup stuff. I like it.
Here’s an example adapted from the Python Enhancement Proposal (PEP) 343 that introduced the implementation in Python 2.6:
from contextlib import contextmanager
@contextmanager
def opening(filename):
f = open(filename)
try:
yield f
finally:
f.close()
with opening("the/filename") as f:
for line in f:
print line
I like it so much because it reminds me of the block-based resource access I’m now used to thanks to Ruby and Swift spoiling me for years. I’m not a fan of repeating imperative sequences between opening/closing file handle code since I know you can do it this way.
I’m still learning Python. That’s why I sometimes try to come up with equivalent Ruby code (which is my reference language of choice to explain new concepts in familiar terms). I noticed that the Python example above sports the yield
keywords, which is how blocks are called in Ruby, too. So I wondered what the @contextmanager
decorator is useful for, and why the with
keyword relies on the result of the expression to respond to the context manager protocol. This protocol is defined through the __enter__
and __exit__
methods being available. That’s how the block is wrapped.
In Ruby, there’s no language feature like decorators. These are basically function call wrappers, but you define the wrapping when you define the function, instead of at the call site. The code above is similar to contextmanager(open(filename))
; that’s the gist of decorators as far as I know.
How do you implement the stuff above in Ruby then?
class MyFile
def self.opening(filename)
f = File.open(filename)
yield f
ensure
f.close
end
end
MyFile.opening("the/filename") do |f|
f.each_line do |line|
puts line
end
end
That’s it, as far as I can tell. The (implicit) block passed to MyFile#opening
is invoked with the yield
keyword. There’s no built-in functionality apart from this. It’s similar to the Python code – but only on the surface level. Python has interesting things going on behind the scenes in the implementation of the with
keyword, and then there’s the code for the @contextmanager
decorator itself.
I think the crucial language difference here is that Ruby is designed to make writing block-based methods so easy. By design, Ruby encourages coming up with domain-specific languages (DSLs).
Python’s with
keyword reads nicer, but then again you can write this as part of your DSL in Ruby, too, if you support optional blocks in the wrapped method. It does not make sense, but there you go:
class MyFile
def self.opening(filename)
end
def self.opening(filename, &block)
# Wrap the method body in a lamda so we can either run or return it
impl = ->(&inner_block) {
f = File.open(filename)
try
# Explicit version of `yield`
inner_block.call(f)
ensure
f.close
end
}
# Return the lambda for later execution if this is
# not a direct call with a block
return impl if block.nil?
# If we pass a block, execute right away!
impl.call(&block)
end
end
def with(wrapped, &block)
wrapped.call(&block)
end
MyFile.opening("the/filename") do |f|
puts "regular:"
puts f.readlines
end
with MyFile.opening("wrapped/fn") do |f|
puts "wrapped:"
puts f.readlines
end