pre-PEP: Simple Thunks

Brian Sabbey sabbey at u.washington.edu
Sun Apr 17 18:02:12 EDT 2005


Ron_Adam wrote:
> On Sat, 16 Apr 2005 17:25:00 -0700, Brian Sabbey
>> Yes, much of what thunks do can also be done by passing a function
>> argument.  But thunks are different because they share the surrounding
>> function's namespace (which inner functions do not), and because they can
>> be defined in a more readable way.
>
> Generally my reason for using a function is to group and separate code
> from the current name space.  I don't see that as a drawback.

I agree that one almost always wants separate namespaces when defining a 
function, but the same is not true when considering only callback 
functions.  Thunks are a type of anonymous callback functions, and so a 
separate namespace usually isn't required or desired.

> Are thunks a way to group and reuse expressions in the current scope?
> If so, why even use arguments?  Wouldn't it be easier to just declare
> what I need right before calling the group?  Maybe just informally
> declare the calling procedure in a comment.
>
> def thunkit:	  # no argument list defines a local group.
> 	# set thunk,x and y before calling.
> 	before()
> 	result = thunk(x,y)
> 	after()
>
> def foo(x,y):
> 	x, y = y, x
> 	return x,y
>
> thunk = foo
> x,y = 1,2
> do thunkit
> print result
>
> -> (2,1)
>
> Since everything is in local name space, you  really don't need to
> pass arguments or return values.

I'm kicking myself for the first example I gave in my original post in 
this thread because, looking at it again, I see now that it really gives 
the wrong impression about what I want thunks to be in python.  The 
'thunkit' function above shouldn't be in the same namespace as the thunk. 
It is supposed to be a re-usable function, for example, to acquire and 
release a resource.  On the other hand, the 'foo' function is supposed to 
be in the namespace of the surrounding code; it's not re-usable.  So your 
example above is pretty much the opposite of what I was trying to get 
across.

>
> The 'do' keyword says to evaluate the group.  Sort of like eval() or
> exec would, but in a much more controlled way.  And the group as a
> whole can be passed around by not using the 'do'.  But then it starts
> to look and act like a class with limits on it.  But maybe a good
> replacement for lambas?
>
> I sort of wonder if this is one of those things that looks like it
> could be useful at first, but it turns out that using functions and
> class's in the proper way, is also the best way. (?)

I don't think so.  My pickled_file example below can't be done as cleanly 
with a class.  If I were to want to ensure the closing of the pickled 
file, the required try/finally could not be encapsulated in a class or 
function.

>
>> You're right that, in this case, it would be better to just write
>> "f(stuff, 27, 28)".  That example was just an attempt at describing the
>> syntax and semantics rather than to provide any sort of motivation.  If
>> the thunk contained anything more than a call to 'stuff', though, it would
>> not be as easy as passing 'stuff' to 'f'.  For example,
>>
>> do f(27, 28):
>> 	print stuff()
>>
>> would require one to define and pass a callback function to 'f'.  To me,
>> 'do' should be used in any situation in which a callback *could* be used,
>> but rarely is because doing so would be awkward.  Probably the simplest
>> real-world example is opening and closing a file.  Rarely will you see
>> code like this:
>>
>> def with_file(callback, filename):
>> 	f = open(filename)
>> 	callback(f)
>> 	f.close()
>>
>> def print_file(file):
>> 	print file.read()
>>
>> with_file(print_file, 'file.txt')
>>
>> For obvious reasons, it usually appears like this:
>>
>> f = open('file.txt')
>> print f.read()
>> f.close()
>>
>> Normally, though, one wants to do a lot more than just print the file.
>> There may be many lines between 'open' and 'close'.  In this case, it is
>> easy to introduce a bug, such as returning before calling 'close', or
>> re-binding 'f' to a different file (the former bug is avoidable by using
>> 'try'/'finally', but the latter is not).  It would be nice to be able to
>> avoid these types of bugs by abstracting open/close.  Thunks allow you to
>> make this abstraction in a way that is more concise and more readable than
>> the callback example given above:
>
> How would abstracting open/close help reduce bugs?

I gave two examples of bugs that one can encounter when using open/close. 
Personally, I have run into the first one at least once.

> I'm really used to using function calls, so anything that does things
> differently tend to be less readable to me. But this is my own
> preference.  What is most readable to people tends to be what they use
> most. IMHO
>
>> do f in with_file('file.txt'):
>> 	print f.read()
>>
>> Thunks are also more useful than callbacks in many cases since they allow
>> variables to be rebound:
>>
>> t = "no file read yet"
>> do f in with_file('file.txt'):
>> 	t = f.read()
>>
>> Using a callback to do the above example is, in my opinion, more
>> difficult:
>>
>> def with_file(callback, filename):
>> 	f = open(filename)
>> 	t = callback(f)
>> 	f.close()
>> 	return t
>>
>> def my_read(f):
>> 	return f.read()
>>
>> t = with_file(my_read, 'file.txt')
>
> Wouldn't your with_file thunk def look pretty much the same as the
> callback?

It would look exactly the same.  You would be able to use the same 
'with_file' function in both situations.

> I wouldn't use either of these examples.  To me the open/read/close
> example you gave as the normal case would work fine, with some basic
> error checking of course.

But worrying about the error checking is what one wants to avoid.  Even if 
it is trivial to remember to close a file, it's annoying to have to think 
about this every time one wants to use a file.  It would be nice to be 
able to worry about closing the file exactly once.

It's also annoying to have to use try/finally over and over again as one 
would in many real-life situations.  It would be nice to be able to think 
about the try/finally code once and put it in a re-usable function.

The open/close file is just the simplest example.  Instead of a file, it 
maybe be a database or something more complex.

> Since Python's return statement can handle
> multiple values, it's no problem to put everything in a single
> function and return both the status with an error code if any, and the
> result.  I would keep the open, read/write, and close statements in
> the same function and not split them up.

What about try/finally?  What if it is more complex than just opening and 
closing a file?  The example that got me annoyed enough to write this 
pre-PEP is pickling and unpickling.  I want to unpickle a file, modify it, 
and immediately pickle it again.  This is a pretty easy thing to do, but 
if you're doing it over and over again, there gets to be a lot of 
boilerplate.  One can of course create a class to handle the boilerplate, 
but instantiating a class is still more complicated than it has to be. 
Here is an example of using thunks to do this:

def pickled_file(thunk, name):
 	f = open(name, 'r')
 	l = pickle.load(f)
 	f.close()
 	thunk(l)
 	f = open(name, 'w')
 	pickle.dump(l, f)
 	f.close()

Now I can re-use pickled_file whenever I have to modify a pickled file:

do data in pickled_file('pickled.txt'):
 	data.append('more data')
 	data.append('even more data')

In my opinion, that is easier and faster to write, more readable, and less 
bug-prone than any non-thunk alternative.

>>> When I see 'do', it reminds me of 'do loops'. That is 'Do' involves
>>> some sort of flow control.  I gather you mean it as do items in a
>>> list, but with the capability to substitute the named function.  Is
>>> this correct?
>>
>> I used 'do' because that's what ruby uses for something similar.
>>
>
> I could see using do as an inverse 'for' operator.  Where 'do' would
> give values to items in a list, verses taking them from a list. I'm
> not exactly sure how that would work. Maybe...
>
> def fa(a,b):
> 	return a+b
> def fb(c,d):
> 	return c*b
> def fc(e,f):
> 	return e**f
>
> fgroup:
> 	fa(a,b)
> 	fb(c,d)
> 	fc(e,f)
>
> results = do 2,3 in flist
>
> print results
> ->  (5, 6, 8)
>
> But this is something else, and not what your thunk is trying to do.
>
>
>
> So it looks to me you have two basic concepts here.
>
> (1.) Grouping code in local name space.
>
> I can see where this could be useful. It would be cool if the group
> inherited the name space it was called with.
>
> (2.) A way to pass values to items in the group.
>
> Since you are using local space, that would be assignment statements
> in place of arguments.
>
> Would this work ok for what you want to do?
>
> def with_file:				# no argument list, local group.
> 	f = open(filename)
> 	t = callback(f)
> 	f.close
>
> def my_read(f):
> 	return f.read()
>
> callback = my_read
> filename = 'filename'
> do with_file

This wouldn't work since with_file wouldn't be re-usable.  It also doesn't 
get rid of the awkwardness of defining a callback.

-Brian



More information about the Python-list mailing list