Decorator syntax refers to the desugaring of
@g(...)
def f(...):
...
into
def f(...):
...= g(...)(f) f
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 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 initialization Although 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
Hypothesis (Python)
The Hypothesis libraryHypothesis 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 (Haskell)
QuickCheck has a function called quickCheck
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 :: Arbitrary a => (a -> Bool) -> IO ()
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
-> x * 2 >= x \x
rather than as a named function
=
testDouble x * 2 >= x x
and we can pass a lambda directly as an argument to the quickCheck
function without any fuss.
Example: Web routing
Flask (Python)
With Flask,Flask we create a web server by first constructing a Flask
instance and then adding routes via its route
method.route
>>> 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
- Apply
app.route
to 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.
Scotty (Haskell)
Now let’s build the same web server using Scotty.The scotty
package on Hackage
We will use the addroute
functionaddroute
which serves a similar role to Flask.route
:
addroute :: StdMethod -> RoutePattern -> ActionM () -> ScottyM ()
import Data.Semigroup
import Network.HTTP.Types.Method
import Web.Scotty
app :: ScottyM ()
=
app do
GET "/"
addroute do
"Hello World!"
text GET "/hello/:name" $
addroute do
<- param "name"
name "Hello " <> name <> "!\n") text (
λ> 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 addroute
function.
Example: Timing
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)
Now we’ll do the same thing in Haskell.Control.Concurrent
We’ll be using the Control.Concurrent
module from base
, andtime
the Data.Time.Clock
module from the time
package.
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
stopwatch(name)(action)(x)
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 glossaryPython glossary: ‘decorator’ 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: