views:

203

answers:

6

There is a useful Ruby idiom that uses tap which allows you to create an object, do some operations on it and return it (I use a list here only as an example, my real code is more involved):

def foo
  [].tap do |a|
    b = 1 + 2
    # ... and some more processing, maybe some logging, etc.
    a << b
  end
end

>> foo
=> [1]

With Rails there's a similar method called returning, so you can write:

def foo
  returning([]) do |a|
    b = 1 + 2
    # ... and some more processing, maybe some logging, etc.
    a << b
  end
end

which speaks for itself. No matter how much processing you do on the object, it's still clear that it's the return value of the function.

In Python I have to write:

def foo():
  a = []
  b = 1 + 2
  # ... and some more processing, maybe some logging, etc.
  a.append(b)
  return a

and I wonder if there is a way to port this Ruby idiom into Python. My first thought was to use with statement, but return with is not valid syntax.

+2  A: 

You can implement it in Python as follows:

def tap(x, f):
    f(x)
    return x

Usage:

>>> tap([], lambda x: x.append(1))
[1]

However it won't be so much use in Python 2.x as it is in Ruby because lambda functions in Python are quite restrictive. For example you can't inline a call to print because it is a keyword, so you can't use it for inline debugging code. You can do this in Python 3.x although it isn't as clean as the Ruby syntax.

>>> tap(2, lambda x: print(x)) + 3
2
5
Mark Byers
Lambda allows me to add only a single expression there and doesn't look very good.Ideally I'd like something like this: def foo(): return with [] as a: a.append(1)but obviously that doesn't work. :(
Michał Kwiatkowski
@Michał Kwiatkowski: You can evaluate multiple expressions by putting them in a list: `tap([], lambda x: [x.append(1), x.append(2)])`, but yes I agree with you that it doesn't look good and is impractical in Python because of the verbose syntax and the restrictions of lambda expressions.
Mark Byers
+3  A: 

If you want this bad enough, you can create a context manager

class Tap(object):
    def __enter__(self, obj):
        return obj

    def __exit__(*args):
        pass

which you can use like:

def foo():
    with Tap([]) as a:
        a.append(1)
        return a

There's no getting around the return statement and with really doesn't do anything here. But you do have Tap right at the start which clues you into what the function is about I suppose. It is better than using lambdas because you aren't limited to expressions and can have pretty much whatever you want in the with statement.

Overall, I would say that if you want tap that bad, then stick with ruby and if you need to program in python, use python to write python and not ruby. When I get around to learning ruby, I intend to write ruby ;)

aaronasterling
Variables created due to `with` statements are still visible in code following the `with` block, so you could pull the `return` back one level of indentation. And then I think you've got a Python `tap` -- some code whose only purpose is to add some function call overhead and another level of indentation.
llasram
@llasram, yeah, that's another way to do it. I prefer to keep my references to variables created as a result of statements like `with` and `for` in the block defined by those statements for forward compatibility. One day, they will fix that misfeature.
aaronasterling
Having the return at the end pretty much negates this approach as a solution to the "problem" (as it does with my decorator approach).Replacing the with `Tap([]) as a:` line with `a = []` has the exact same effect.
Glenjamin
@Glenjamin. I made this perfectly clear in my answer with the line '`with` doesn't really do anything here'. There's no good way to not have a return at the end of `foo`. Don't tell me to look at marks solution because (with all due respect to him) I don't think his solution is good.
aaronasterling
@aaronmcsmooth My only point of contention was that "doesn't really do anything here" wasn't as clear as it could have been unless you look at the code properly. I'm in agreement that both the problem and all the presented solutions are pretty un-pythonic.
Glenjamin
A: 

I partly agree with others in that it doesn't make much sense to implement this in Python. However, IMHO, Mark Byers's way is the way, but why lambdas(and all that comes with them)? can't you write a separate function to be called when needed?

Another way to do basically the same could be

map(afunction(), avariable)

but this beautiful feature is not a built-in in Python 3, I hear.

Mattia Gobbi
you mean `map(afunction, avariable)` and this assumes that `avariable` is an iterable which it is not guaranteed to be.
aaronasterling
@Aaron oops! that was too eager, now I see. EDIT: tricked by the list examples :-) then is it right to use any function in place of Mark's lambdas, or did I totally fail? if so, can you have a "main" function with (an object and more than one function) as arguments? that would do the trick in a quite pythonic way
Mattia Gobbi
+1  A: 

Hardly any Ruby programmers use tap in this way. In fact, all top Ruby programmers i know say tap has no use except in debugging.

Why not just do this in your code?

[].push(1)

and remember Array supports a fluent interface, so you can even do this:

[].push(1).push(2)
banister
As mentioned in the question I used an array only as an example. There are a lot of methods that are not chainable, thus the need for tap/returning.
Michał Kwiatkowski
Still, I would not call it a ruby 'idiom' as not many programmers use tap in this way.
banister
+11  A: 

Short answer: Ruby encourages method chaining, Python doesn't.

I guess the right question is: what is Ruby's tap useful for?

Now I don't know a lot about Ruby, but by googling I got the impression that tap is conceptually useful as method chaining.

In Ruby, the style: SomeObject.doThis().doThat().andAnotherThing() is quite idiomatic. It underlies the concept of fluent interfaces, for example. Ruby's tap is a special case of this where instead of having SomeObject.doThis() you define doThis on the fly.

WHy I am explaining all this? Because it tells us why tap doesn't have good support in Python. With due caveats, Python doesn't do call chaining.

For example, Python list methods generally return None rather than returning the mutated list. Functions like map and filter are not list methods. On the other hand, many Ruby array methods do return the modified array.

Other than certain cases like some ORMs, Python code doesn't use fluent interfaces.

In the end it is the difference between idiomatic Ruby and idiomatic Python. If you are going from one language to the other you need to adjust.

Muhammad Alkarouri
A: 

I had an idea to achieve this using function decorators, but due to the distinction in python between expressions and statements, this ended up still requiring the return to be at the end.

The ruby syntax is rarely used in my experience, and is far less readable than the explicit python approach. If python had implicit returns or a way to wrap multiple statements up into a single expression then this would be doable - but it has neither of those things by design.

Here's my - somewhat pointless - decorator approach, for reference:

class Tapper(object):
    def __init__(self, initial):
        self.initial = initial
    def __call__(self, func):
        func(self.initial)
        return self.initial

def tap(initial):
    return Tapper(initial)

if __name__ == "__main__":
    def tapping_example():
        @tap([])
        def tapping(t):
            t.append(1)
            t.append(2)
        return tapping

    print repr(tapping_example())
Glenjamin