Function decorators in Python
In Python, decorator syntax refers to the desugaring of
f is called the “decorated” function, and
g is called the “decorator.”
Here we look at several examples using decorators in Python, compare them to similar situations in Haskell, and discuss how lambdas let us achieve the same result conveniently without having a special syntax for decorators.
Uses of decorators
We broadly categorize decorators into two kinds of uses:
- To make some alteration to the decorated function
- To perform some side effect during module initializationAlthough Python a module usually looks like a declarative list of functions, it’s important to remember that a module is actually a script that performs side effects (even I/O!) as it loads.
Example: Property testing
The Hypothesis library has a decorator called
given. This is one that alters the decorated function.
In the example below, we want to verify the assertion that if you double a number
x, the result will be greater than or equal to the input. We define a function
test_double which expresses this.
And we also applied the
given decorator to it. This replaces the
test_double function we wrote with a function that runs our function on many inputs in search of an input for which the assertion fails.
>>> test_double() Falsifying example: test_double(x = -1)
And we see it did find a counterexample: The property we’re verifying was wrong, because it doesn’t hold for negative numbers.
QuickCheck has a function called
quickCheck which serves a similar role:The actual type of
quickCheck is more general than this, but here we give a specialized type for simplicity.
quickCheck transforms a function which expresses the property into an action which checks the property.
And we can run it and see that we get a similar result:
λ> testDouble *** Failed! Falsifiable (after 7 tests and 2 shrinks): -1
Why aren’t we suffering from lack of a decorator syntax in the Haskell example? Because we were able to express the property as a lambda
rather than as a named function
and we can pass a lambda directly as an argument to the
quickCheck function without any fuss.
Example: Web routing
>>> app.run(port = 8000) * Running on http://127.0.0.1:8000/ (Press CTRL+C to quit)
$ curl http://localhost:8000 Hello World! $ curl http://localhost:8000/hello/Julie Hello Julie!
Because Python has no syntax for writing multi-line anonymous functions, we needed two declarations to create each route –
- Define the route’s behavior by declaring a named function
app.routeto the named function to create the route
– but decorator syntax gave us a nice concise way to perform the second step without having to repeat the name of the function.
Now let’s build the same web server using Scotty.
We will use the
addroute function which serves a similar role to
λ> scotty 8000 app Setting phasers to stun... (port 8000) (ctrl-c to quit)
In contrast with the Python server, the Haskell routes didn’t require two steps. This is because we didn’t need to name the route behaviors; we just passed them directly as arguments to Scotty’s
Timing decorator (Python)
For this example we’ll write our own decorator. The
stopwatch decorator modifies a function so that when the function runs, it also prints a line that says how long it took.
>>> delay(1) delay: 0:00:01.001225
Timing function (Haskell)
import Control.Concurrent import Data.Time.Clock stopwatch :: String -> (a -> IO b) -> (a -> IO b) stopwatch name action x = do start <- getCurrentTime result <- action x end <- getCurrentTime putStrLn (name ++ ": " ++ show (end `diffUTCTime` start)) return result delay :: Int -> IO () delay = stopwatch "delay" $ \x -> threadDelay x
λ> delay 1000000 delay: 1.001164885s
Why is the “decorator” (the
stopwatch function) more concise in Haskell? Because Python requires a decorator to be curried – it is used via three separate invocations
rather than taking all arguments at once
stopwatch(name, action, x)
– but Python syntax doesn’t easily support currying, so we had to use some funny workarounds like declaring a named function
f and then returning it.
Using a higher-order function in Haskell is extremely similar to using a decorator in Python. That’s because that’s almost exactly what a decorator is. The Python glossary entry for “decorator” actually defines it as:We find this definition slightly inaccurate; more precisely, a Python decorator is a function that accepts a function argument, and usually (but not necessarily) returns a function.
A function returning another function
So it should be no surprise that this is something at which Haskell excels!
To emphasize the similarity between decorators in Python and higher-order functions in Haskell, let’s put the final examples side-by-side to see how close they are: