The Decorator Pattern in Haskell

The Decorator Pattern in Haskell

Submission deadline: Monday, 12 May 2014, anywhere on Earth Functional Pearl: The Decorator Pattern in Haskell Nathan Collins Tim Sheard Portland State University [email protected] [email protected] Abstract Suppose we implement exponentiation in Haskell, using divide- The Python programming language makes it easy to implement and-conquer: decorators: generic function transformations that extend existing pow b p = functions with orthogonal features, such as logging, memoization, if p <= 1 and synchronization. Decorators are modular and reusable: the user then b * p does not have to look inside the definition of a function to decorate else pow b (p `div` 2) * pow b (p - (p `div` 2)) it, and the same decorator can be applied to many functions. In this paper we develop Python-style decorators in Haskell generally, And equivalently, in Python: and give examples of logging and memoization which illustrate the def pow(b, p): simplicity and power of our approach. if p <= 1: Standard decorator implementations in Python depend essen- return b * p tially on Python’s built-in support for arity-generic programming else: and imperative rebinding of top-level names. Such rebinding is not return pow(b, p//2) * pow(b, p - (p//2)) possible in Haskell, and Haskell has no built-in support for arity- generic programming. We emulate imperative rebinding using mu- Now, suppose we want to observe our function in order to debug tual recursion, and open recursion plus fixed points, and reduce the it. One way to do this would be to print out call-trace information arity-generic programming to arity-generic currying and uncurry- as the function runs. This could be accomplished by interleaving ing. In developing the examples we meet and solve interesting aux- print statements with our code (using Debug.Trace in Haskell): iliary problems, including arity-generic function composition and ugly, but it works. first-class implication between Haskell constraints. In Python, we can instead do something modular and reusable: we can write a generic call-tracing decorator: LEVEL = 0 1. Decorators by Example in Python and Haskell def trace(f): We begin by presenting Python and Haskell decorators by example, def traced(*args): while glossing over a lot of details which will be provided in later global LEVEL sections. This section serves both to motivate the problem and give prefix = "| " * LEVEL the intuition for our solution. The code described in this paper, print prefix + ("%s%s" % (f.__name__ , args)) and more elaborate examples not described here, are available on LEVEL += 1 GitHub [3]. r = f(*args) Our example decorators are call-tracing and memoization, and LEVEL -= 1 our example function to decorate is natural exponentiation bp. We print prefix + ("%s" % r) choose this example function because 1) it admits an obvious recur- return r sive implementation which makes redundant recursive calls, and 2) return traced it’s not a unary function.1 The recursion makes call-tracing inter- For those not familiar with Python, decorators in Python are esting and the redundant recursion makes memoization applicable. explained in more detail in Appendix A. Their utility depends We care about higher arity because we want our decorators to be heavily on being arity-generic2 and being able to trap recursive calls arity generic. to the function being traced3. After adding the line 1 For unary functions an obvious example is Fibonacci, which we consider pow = trace(pow) later in Section 2.1. to the source program, we run pow(2, 6) and see pow(2, 6) | pow(2, 3) | | pow(2, 1) 2 In Python, *args as a formal parameter, in def traced(*args), de- clares a variadic function, like defun traced (&rest args) in LISP; *args as an actual parameter, in f(*args), applies a function to a se- quence of arguments, like (apply f args) in LISP; % as a binary operator is format-string substitution. 3 In Python, function names are just lexically scoped mutable variables, so [Copyright notice will appear here once ’preprint’ option is removed.] trapping is simply a matter of redefinition. short description of paper 1 2014/8/27 | | 2 functions that instead take a single tuple as argument. In Haskell | | pow(2, 2) then, a good analogy is arity-generic currying and uncurrying:4 | | | pow(2, 1) curry f x1 ... xn = f (x1 , ... , xn) | | | 2 uncurryM f (x1 , ... , xn) = f x1 ... xn | | | pow(2, 1) | | | 2 Here curry f in Haskell corresponds to def f(*args): ... | | 4 in Python, and uncurryM f args in Haskell corresponds to | 8 f(*args) in Python. We’ll explain how to statically type and | pow(2, 3) implement these functions later, but for now we just need to un- | | pow(2, 1) derstand them operationally at an intuitive level. | | 2 With curry and uncurryM in hand we can write a well-typed5 | | pow(2, 2) call-tracing decorator in Haskell quite similar to the Python deco- | | | pow(2, 1) rator we saw earlier: | | | 2 | | | pow(2, 1) trace levelRef name f = curry traced where | | | 2 traced args = do | | 4 level <- readIORef levelRef | 8 let prefix = concat . replicate level $ "| " 64 putStrLn $ prefix ++ name ++ show args modifyIORef levelRef (+1) Noting the repeated sub computations, we see that memoization r <- uncurryM f args would be an improvement. So, we write a generic memoization modifyIORef levelRef (subtract 1) decorator: putStrLn $ prefix ++ show r def memoize(f): return r cache = dict() Similary, we can write a well-typed memoization decorator: def memoized(*args): if args not in cache: memoize cacheRef f = curry memoized where cache[args] = f(*args) memoized args = do return cache[args] cache <- readIORef cacheRef memoized.__name__ = f.__name__ case Map.lookup args cache of return memoized Just r -> return r Nothing -> do and replace the line r <- uncurryM f args pow = trace(pow) modifyIORef cacheRef (Map.insert args r) return r with These decorators are both monadic; we discuss non-monadic dec- pow = trace(memoize(pow)) orators in Section 3. To apply these decorators to we make two changes: 1) Running pow(2, 6), we see pow we rewrite pow as a monadic function, because the decorators are pow(2, 6) monadic; 2) we rewrite pow as an open-recursive function, so that | pow(2, 3) we can trap recursive calls. In general, (1) is obviously unnecessary | | pow(2, 1) if the function we want to decorate is already monadic, and for pure | | 2 functions we can actually use unsafePerformIO6 to avoid making | | pow(2, 2) the function monadic, as we explain in Section 3.2. For (2), we can | | | pow(2, 1) alternatively use mutual recursion, as we discuss in Section 2.1. | | | 2 Before decoration, a monadic version of pow using (unneces- | | | pow(2, 1) sary) open recursion is | | | 2 | | 4 openPowM pow b p = do | 8 if p <= 1 | pow(2, 3) then pure $ b * p | 8 else (*) <$> pow b (p `div` 2) <*> 64 pow b (p - (p `div` 2)) powM = fix openPowM Arity-generic decorators are easy to write in Python, and are reusable. In Haskell, things do not appear to be so simple. But, We can now decorate powM with both memoization and tracing with it turns out that, in Haskell it’s also easy to write arity-generic just a few lines: decorators! Indeed, that’s what this paper is about. powM :: Int -> Int -> IO Int An arity-generic decorator needs to solve two problems: inter- cept recursive calls and handle functions of any arity uniformly. In 4 The “M” in “uncurryM” stands for “monadic”. Python, arity genericity is easy to implement via the built-in *args 5 In fact, once curry and uncurryM are defined, GHC 7.6.3 can infer feature, and a function name is simply a statically scoped mutable the types of these decorators. In practice, the type annotations make good variable, so a simple assignment can be used to intercept recursive documentation, but we aren’t ready to explain the types yet, so we postpone calls. In Haskell these problems need to be solved in another way. them until Section 2.3.2. Let’s start with arity-genericity. What Python’s *args feature 6 Yes, unsafePerformIO is easily abused, but we think Debug.Trace is does is allow us to treat functions of any arity uniformly as unary good precedent here, at least in the call-tracing use case. short description of paper 2 2014/8/27 powM b p = do if n <= 1 levelRef <- newIORef 0 then n cacheRef <- newIORef Map.empty else fib (n-1) + fib (n-2) fix (trace levelRef "powM" . memoize cacheRef . openPowM) b p Writing recursive functions in this mutually recursive style may seem pointless, but it makes them amenable to decoration at a Running powM 2 6 we see7 neglibile cost – less than one line. It is also very easy to re-factor a function not written in this style using regexp-search and replace powM(2,(6,())) in your editor. Once done, this split into two mutually recursive | powM(2,(3,())) functions does not need to be undone if one decides that decoration | | powM(2,(1,())) is no longer necessary. For example, after using trace to debug | | 2 a function, we might want to disable tracing by removing the | | powM(2,(2,())) decorator. | | | powM(2,(1,())) Alternatively, we can use open recursion and fixed points. Given | | | 2 the open-recursive openFib defined by | | | powM(2,(1,())) | | | 2 openFib :: (Int -> Int) -> (Int -> Int) | | 4 openFib fib n = | 8 if n <= 1 | powM(2,(3,())) then n | 8 else fib (n-1) + fib (n-2) 64 we can rewrite fib as a fixed point of openFib: Of course, it may not yet be obvious how to implement curry and uncurryM.

View Full Text

Details

  • File Type
    pdf
  • Upload Time
    -
  • Content Languages
    English
  • Upload User
    Anonymous/Not logged-in
  • File Pages
    10 Page
  • File Size
    -

Download

Channel Download Status
Express Download Enable

Copyright

We respect the copyrights and intellectual property rights of all users. All uploaded documents are either original works of the uploader or authorized works of the rightful owners.

  • Not to be reproduced or distributed without explicit permission.
  • Not used for commercial purposes outside of approved use cases.
  • Not used to infringe on the rights of the original creators.
  • If you believe any content infringes your copyright, please contact us immediately.

Support

For help with questions, suggestions, or problems, please contact us