Monday, October 7, 2013

Tell, don't Ask.

tl;dr

If you rely on asking, you're asking for trouble. Instead:

  1. Tell functions what to do, don't make them ask.
  2. Tell processes what to do, don't make them ask.
  3. Keep all your environment variable queries in one place, apart from the rest of the code.

You probably think this is obvious, but it isn't

The principle of asking versus telling has many faces in programming. Some of the faces are obvious—others are more subtle. This articles moves from the more obvious to the more subtle.

So if you find yourself saying, "Well, duh!" Keep reading.

Telling v. Asking

This Python code illustrates telling:

# teller.py
import sqlite3

def connectToDatabase(filename):
return sqlite3.connect(filename)

The connectToDatabase function accepts an argument for the database connection details. Other code that calls connectionToDatabase tells the function what it wants to do.

This Python code illustrates asking:

# asker.py
import sqlite3

DATABASE_FILENAME='/tmp/database.sqlite'

def connectToDatabase():
return sqlite3.connect(DATABASE_FILENAME)

The connectToDatabase function in the above snippet is not told database connection details. Instead, the function asks for the connection details—in this case, it asks from the global scope (which is a bad place to be in).

Telling > Asking

Telling, as described above, is better than Asking for the following reasons:

  1. The code is more flexible for reuse.

    I can more easily connect to different databases using the teller.py.

  2. The code is easier to test.

    Because teller.py is more flexible for reuse, I can use the code in tests very easily.

  3. It's easier to know how to use the code and harder to use incorrectly.

    It's obvious in teller.py that I must provide a filename to connect to (because of the argument spec of the function). I can't accidentally connect to /tmp/database.sqlite.

    To use asker.py I must know that the function looks at DATABASE_FILENAME either from reading the source code or the docstring of the function (which is absent in this case). This would be much more difficult to do if connectToDatabase called other functions in other files which accessed a global variable.

Abusing Environment Variables

In asker.py the function asks for information from the global scope. The global scope is just one place to get information from. The environment is another. Take a look at this:

# asker-env.py
import sqlite3
import os

def connectToDatabase():
return sqlite3.connect(os.environ['DATABASE_FILENAME'])

Instead of asking for the database filename from the global scope, asker-env.py asks the environment of the process for the database filename. This is more reusable that asker.py but is still not as good as teller.py because:

  • it still suffers from problem #3: that you must rely on the docstring or reading the source code to understand how to use it, and
  • you can only have one connection per process.

A better approach would be to tell the process which database filename to use as in this example:

# teller-cli.py
import sqlite3
import argparse

def connectToDatabase(filename):
return sqlite3.connect(filename)

if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('filename', help="Filename of the SQLite database")
args = parser.parse_args()
connectToDatabase(args.filename)

teller-cli.py is better than asker-env.py because you can ask it (using teller-cli.py --help) what you need to tell it instead of having to read docstrings or source code. Having a --help option which fully describes configuration options and is enforced when running the process is similar to teller.py having an argument spec that is enforced by Python.

But I thought environment variables were good...

(this is a picture of an environment -- much better than the one on your computer)

If you subscribe to The Twelve-Factor App's ideas, you will store all your configuration in environment variables. Or if you use Travis-CI or Heroku you will also have used environment variables to great effect. Environment variables seem like a great way to do configuration.

Environment variables are cross-language and easy to change. They have huge benefits. Environment variables are a great way to do configuration! It would be nice to leverage the great qualities of environment variables along with the great qualities of code that is told.

You can!

Convert Asking to Telling

To write Telling code that also uses environment variables, restrict the environment variable querying to a single, documented place. Consider this:

# env-runner.py
import argparse
import os

# Define ALL the environment variables this process might use.
env_vars = [
('DATABASE_FILENAME', 'Filename of the SQLite database.'),
]

# Read the environment. This function must only be called from within this
# module if you want to prevent writing asking code.
def getArgs(environ, config):
ret = {}
for (env_name, description) in config:
try:
ret[env_name] = environ[env_name]
except KeyError:
print 'Missing env var: %s %s' % (env_name, description)
raise
return ret


def main():
from teller import connectToDatabase
args = getArgs(os.environ, env_vars)
db = connectToDatabase(args['DATABASE_FILENAME'])
# ...

The above snippet has all the benefits of telling code and the flexibility of asking code:

  1. The code is flexible for reuse.
  2. The code is easy to test.
  3. It's easy to know how to use this code and hard to use incorrectly.

Real-world examples

As proof that the concept of Telling instead of Asking is not obvious, here are some real-world examples (both good and bad):

Klein's improvement on Flask

Flask, a micro web framework for Python, loves the global scope. This is the Hello, World! from the front page:

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run()

And this is the recommended way for accessing a database:

from flask import g

app = Flask(__name__)

@app.before_request
def before_request():
g.db = connect_db()

@app.route("/")
def hello():
db = g.db
# ...

You use a package-global named g, which is "magical" and comes with appropriate warnings:

We store our current database connection on the special g object that Flask provides for us. This object stores information for one request only and is available from within each function. Never store such things on other objects because this would not work with threaded environments. That special g object does some magic behind the scenes to ensure it does the right thing. http://flask.pocoo.org/docs/tutorial/dbcon/

Consider the improvement offered by Klein, a similar micro web framework. With Klein you can easily make apps with Non-global state:

from klein import Klein

class MyApp(object):

app = Klein()

def __init__(self, db_connection):
self.db_connection = db_connection

@app.route('/')
def hello(self, request):
db = self.db_connection
# ...

There is no magical, global g here. You can instantiate MyApp with a database connection, or even have three different instances of MyApp with three different database connections all running in the same app.

Klein lets you tell instead of ask.

Ansible

Ansible is a (really good) configuration management tool. It's mostly straightforward, but it's easy to use it in an asking way—which becomes unmaintainable.

For instance, if in one of our tasks we want to download a resource from http://dev.example.com if we're in the development environment or from https://production.example.com if we're in the production environment, Ansible easily lets us do this:

# main.yml
- name: Get the files
command: wget {{ source_server }}/thefile.tgz /tmp/thefile.tgz
creates=/tmp/thefile.tgz

The command asks for source_server. This task likely lives in a role's task file, which could be in roles/mymachine/tasks/main.yml, deep within the directory structure of my configuration. The problem is that I have no way of knowing (short of manually parsing the task file) when writing my inventory file or anything else that uses/includes main.yml, that source_server is a needed variable.

Ansible lets you ask yourself into an unmaintainable hole. To be more maintainable, Ansible should provide a mechanism for specifying the parameters needed by tasks. Perhaps something like:

# main-with-vars.yml
- variables:
- name: source_server
description: URL of the server to download source files from. For
example: http://foo.com
default: http://example.com

- name: Get the files
command: wget {{ source_server }}/thefile.tgz /tmp/thefile.tgz
creates=/tmp/thefile.tgz

Such a file would allow you to produce a list of all the configurable variables for a role/task and then be able to tell instead of ask.

AngularJS

AngularJS does a lot to help you avoid asking through dependency injection.

Twisted's Reactor

Twisted is currently working toward making the reactor not global in an effort to make testing easier and perhaps allow for new features.

Conclusion

In conclusion, read the tl;dr at the top :) Also, do you have some example of telling v. asking? Or counter-arguments? Post a comment.

29 comments:

  1. This seems like a good, specific example of how to keep your code and configuration loosely coupled. By definition, your "ask" examples require you to understand the code much more intricately in order to write your config (making them tightly coupled). Nice writeup.

    ReplyDelete

  2. Thanks for sharing the good information and post more information.Talent flames company is one of the best training and placement company in Hyderabad. Providing training on Technologies like Java,Sql,Oracle,..,etc with 100% Placement Assistance. Now Interviews going on if you want to attend just visit our website and drop your resume. for more information visit us http://talentflames.com/
    training and placement company in Hyderabad

    ReplyDelete

  3. Thank you for giving the information and it is useful for me. training with placement company in Hyderabad

    ReplyDelete
  4. Today Telugu news updates provide us the information of breaking news and live updates. we get live news, political, education, technology, etc. Today Telugu news gives the best news updates. It also keeps its readers informed about the latest happenings in the world with instant updates.

    ReplyDelete
  5. Very knowledgeable post thank you for sharing...
    AngularJS Training in Bangalore | AngularJS Course Fees | AngularJS 6 - i Digital Academy - AngularJS Training in Bangalore - Learn AngularJS 6 from Expert Real-time Trainers
    at i Digital Academy with Live Projects and Placement Assistance. Book a Free Demo Today.

    ReplyDelete
  6. There are a lot of Love Calculator, True love calculator, love meter, love crush calculator and much more but only a few are authentic. True Love Calculator Soulmate provides a very authentic and accurate love percentage. It calculates love percentage by the couple's name. I have experienced on this site and it is a genuine Friendship calculator.

    ReplyDelete
  7. Great experience, so speedy, sensible and uncommonly obliging and pleasant experience, clear credits, would firmly propose.
    Personal Loans for Bad credit
    Loans for Bad credit

    ReplyDelete
  8. Exceptionally simple to apply. Also lower interest than any remaining credit suppliers. I would energetically suggest
    Online Loans for Bad credit
    Loans for people with bad credit

    ReplyDelete
  9. Truly content easily of administration - no off-kilter calls and took care of totally on the web
    Bad Credit Loans Personal
    Bad Credit Loans for people

    ReplyDelete
  10. The site is requesting the greater part from the requests that they need to grasp our money related conditions. We are happy to share my experience.. Continuously suggested!!No check credit loan
    Installment Loans for Bad credit

    ReplyDelete
  11. I applied for the explanation I would be turned down regarding credit. They ended up underwriting the credit. I was so bright. The strain I had is gone! Much gratitude to you, Allbadcreditloan, for the chance to pull together!
    Loans for Bad Credit Online
    Loans for Bad credit score

    ReplyDelete
  12. From my basic application for development to when the leasers were paid, I was glad about how smooth and bare it was. It has dropped a mind-boggling load from my shoulders. I would unequivocally recommend it to anyone expecting to join Mastercards.
    Auto Loans for Bad credit
    Urgent Loans for Bad credit

    ReplyDelete
  13. Getting a development was quick and direct. This is my fourth development, and no issues until now.
    Small Loans for Bad credit
    Home Loans for Bad credit

    ReplyDelete
  14. I found that working with allbadcreditloan was just comparably basic as pie! they were incredibly obliging and given answers for all of my requests!

    Car Loans for Bad credit
    Bad Credit Loans

    ReplyDelete
  15. This is the second time I have helped an advance through allbadcreditloan. The collaboration is particularly fundamental and the development analyst makes the decision basic on the sum to obtain, the portion and stretch of time for the development.
    Bad Credit Loans Personal
    Bad Credit Loans for people

    ReplyDelete
  16. This was so basic and quick . The application online was non muddled and didn't need transferring archives
    Online Loans for Bad credit
    Loans for people with bad credit

    ReplyDelete
  17. It was exceptionally simple to help the credit through Allbadcreditloan. The reps were exceptionally useful through the entire cycle.
    Personal Loans for Bad credit
    Loans for Bad credit

    ReplyDelete
  18. The interaction to acquire a credit with allbadcreditloan was the best I've encountered so far. I endeavored to acquire a credit with my 2 individual banks and was denied. I was anxious to approve of them however they made the cycle so natural. I felt open to going ahead with the credit acknowledgment.
    No check credit loan
    Installment Loans for Bad credit

    ReplyDelete
  19. Everything was explained thoroughly. I went through the process late on a Friday afternoon and by the following Tuesday my funds were in my account. Just in time pay for my roof replacement! I do have
    Loans for Bad Credit Online
    Loans for Bad credit score

    ReplyDelete
  20. The experience from meeting all requirements to getting the assets was exceptionally simple and FAST! The internet based advances were obvious. I followed the bearings in general, talked with client assistance to deal with business confirmation and settling my credit arrangement and presto I was finished!
    Auto Loans for Bad credit
    Urgent Loans for Bad credit

    ReplyDelete
  21. Amazing, allbadcreditloan truly got this one right! It was extremely simple to finish the application and the subsidizing after endorsement was quick. Incredible rate and no charge? This one is a champ. Much obliged, allbadcreditloan !
    Small Loans for Bad credit
    Home Loans for Bad credit

    ReplyDelete
  22. Thankful for sharing your contemplations. I like your undertakings, and I will be holding on to your further surveys. Much obliged to you before long.
    Personal Loans for Bad credit
    Loans for Bad credit

    ReplyDelete
  23. You're so cool! I don't acknowledge that I've scrutinized anything like that beforehand. So uncommon to track down somebody for explicit extraordinary thoughts on this subject. Without a doubt.. thankful for terminating this up. This website is one thing that is expected on the web, someone with some development!
    Bad Credit Loans Personal
    Bad Credit Loans for people

    ReplyDelete
  24. awe-inspiring idea Much obliged to you for the information; it will be advantageous for every one of us and don't stop giving additional information.
    Car Loans for Bad credit
    Bad Credit Loans

    ReplyDelete
  25. Hi very informative blog. Got to know a lot. You have written it precisely. Thanks a lot for sharing this information.
    Data Analytics Courses in Kenya

    ReplyDelete