Control flow abstraction
Any programmer, even if he does not look at his work in this way, is constantly engaged in the construction of abstractions. Most often, we abstract computing (writing functions) or behavior (procedures and classes), but besides these two cases, in our work there are many repeating patterns especially when handling errors, managing resources, writing standard handlers and optimizations.
What does it mean to abstract the control flow or “control flow”, as our overseas friends put it? In the case when no one is showing off, the control structures are engaged in the flow. Sometimes these control structures are not enough and we add our own abstracting the desired behavior of the program. It is simple in languages like lisp, ruby or perl, but in other languages it is possible, for example, using higher-order functions.
Start over. What needs to be done to build a new abstraction?
I must say that the third point is not always feasible. Implementation is highly dependent on the flexibility of the language and what you abstract.
What if your language is not flexible enough? It's okay, instead of implementing, you can simply describe your technique in detail, make it popular and, thus, give rise to a new “design pattern”. Or just switch to a more powerful language if patterning doesn't appeal to you.
But enough theory, let's get down to business ...
Regular python code (taken from a real project with minimal changes):
This code has many aspects: iterating over the url list, uploading images, collecting uploaded images in photos, skipping small pictures, retrying uploads when network errors occur. All these aspects are entangled in a single piece of code, although many of them would be useful in themselves, if only we could isolate them.
In particular, iteration + collection of results is implemented in the built-in function
Let's try to fish out the other aspects. Let's start by skipping small pictures, it could look like this:
Not bad, but there is a drawback - I had to abandon the use
Only this will not work,
Let's return to the functions of a higher order, and more precisely to their special variety - decorators:
As we can see, this approach fits well with the use
We rewrite it
We can also implement this:
What is the result? Now all aspects of our code are in sight, easily distinguishable, mutable, replaceable, and removable. And as a bonus, we got a set of abstractions that can be used in the future. And also, I hope I made someone see a new way to make my code better.
PS The implementation
PPS Other examples of abstraction of control flow: manipulation of functions in underscore.js , list and generator expressions, overloading functions , cache wrappers for functions, and much more.
PPPS Seriously, you need to come up with a better translation for the expression “control flow”.
What does it mean to abstract the control flow or “control flow”, as our overseas friends put it? In the case when no one is showing off, the control structures are engaged in the flow. Sometimes these control structures are not enough and we add our own abstracting the desired behavior of the program. It is simple in languages like lisp, ruby or perl, but in other languages it is possible, for example, using higher-order functions.
Abstraction
Start over. What needs to be done to build a new abstraction?
- Highlight some piece of functionality or behavior.
- Give him a name.
- Implement it.
- Hide the implementation behind the selected name.
I must say that the third point is not always feasible. Implementation is highly dependent on the flexibility of the language and what you abstract.
What if your language is not flexible enough? It's okay, instead of implementing, you can simply describe your technique in detail, make it popular and, thus, give rise to a new “design pattern”. Or just switch to a more powerful language if patterning doesn't appeal to you.
But enough theory, let's get down to business ...
Life example
Regular python code (taken from a real project with minimal changes):
urls = ...
photos = []
for url in urls:
for attempt in range(DOWNLOAD_TRIES):
try:
photos.append(download_image(url))
break
except ImageTooSmall:
pass # пропускаем урл мелкой картинки
except (urllib2.URLError, httplib.BadStatusLine, socket.error), e:
if attempt + 1 == DOWNLOAD_TRIES:
raise
This code has many aspects: iterating over the url list, uploading images, collecting uploaded images in photos, skipping small pictures, retrying uploads when network errors occur. All these aspects are entangled in a single piece of code, although many of them would be useful in themselves, if only we could isolate them.
In particular, iteration + collection of results is implemented in the built-in function
map
:photos = map(download_image, urls)
Let's try to fish out the other aspects. Let's start by skipping small pictures, it could look like this:
@contextmanager
def skip(error):
try:
yield
except error:
pass
for url in urls:
with skip(ImageTooSmall):
photos.append(download_image(url))
Not bad, but there is a drawback - I had to abandon the use
map
. Let us leave this problem for now and move on to the network error tolerance aspect. Similarly to the previous abstraction, one could write:with retry(DOWNLOAD_TRIES, (urllib2.URLError, httplib.BadStatusLine, socket.error)):
# ... do stuff
Only this will not work,
with
in python it cannot execute its code block more than once. We stumbled into the limitations of the language and now we are forced to either minimize and use alternative solutions, or generate another “pattern”. It is important to notice such situations if you want to understand the differences in languages, and how one can be more powerful than the other, despite the fact that they are all Turing-complete. In ruby and with less convenience in perl, we could continue to manipulate blocks, in Lisp - blocks or code (the latter in this case, apparently, to nothing), in python we will have to use an alternative option. Let's return to the functions of a higher order, and more precisely to their special variety - decorators:
@decorator
def retry(call, tries, errors=Exception):
for attempt in range(tries):
try:
return call()
except errors:
if attempt + 1 == tries:
raise
http_retry = retry(DOWNLOAD_TRIES, (urllib2.URLError, httplib.BadStatusLine, socket.error))
harder_download_image = http_retry(download_image)
photos = map(harder_download_image, urls)
As we can see, this approach fits well with the use
map
, we also got a couple of little things that will be useful to us someday - retry
and http_retry
. We rewrite it
skip
in the same style:@decorator
def skip(call, errors=Exception):
try:
return call()
except errors:
return None
skip_small = skip(ImageTooSmall)
http_retry = retry(DOWNLOAD_TRIES, (urllib2.URLError, httplib.BadStatusLine, socket.error))
download = http_retry(skip_small(download_image))
photos = filter(None, map(download, urls))
filter
needed to skip discarded pictures. In fact, the pattern is filter(None, map(f, seq))
so common that in some languages there is a built-in function for this case . We can also implement this:
def keep(f, seq):
return filter(None, map(f, seq))
photos = keep(download, urls)
What is the result? Now all aspects of our code are in sight, easily distinguishable, mutable, replaceable, and removable. And as a bonus, we got a set of abstractions that can be used in the future. And also, I hope I made someone see a new way to make my code better.
PS The implementation
@decorator
can be taken here . PPS Other examples of abstraction of control flow: manipulation of functions in underscore.js , list and generator expressions, overloading functions , cache wrappers for functions, and much more.
PPPS Seriously, you need to come up with a better translation for the expression “control flow”.