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.

    Abstraction


    Start over. What needs to be done to build a new abstraction?
    1. Highlight some piece of functionality or behavior.
    2. Give him a name.
    3. Implement it.
    4. 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, within 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 - retryand http_retry.

    We rewrite it skipin 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))
    

    filterneeded 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 @decoratorcan 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”.

    Also popular now: