24 Days of GHC Extensions: Implicit Parameters

{-# LANGUAGE ImplicitParams #-}
import Data.Char

Yesterday we looked at nullary type classes - type classes that don’t vary over any types - and saw how they can be used to leave part of a program undefined. Our particular example looked at building a library that needs to call a logging function, but we leave the implementation up to the library author.

However, using a nullary type class for this has some drawbacks - the biggest is that we are now tied to a single choice of logging function. This can be problematic if we ever need to vary what it means to log throughout the life time of a program. This may crop up if we decide that we need to transform or discard log entries at smaller parts of our program.

To solve this, lets rewrite our library to take logging as a parameter to a function:

type LogFunction = String -> IO ()

type Present = String

queueNewChristmasPresents :: LogFunction -> [Present] -> IO ()
queueNewChristmasPresents logMessage presents = do
  mapM (logMessage . ("Queueing present for delivery: " ++)) presents
  return ()

This isn’t much different from what we saw yesterday, but it quickly becomes painful to use in practice. Whenever we want to abstract over queueNewChristmasPresents, we have to either choose to commit to a specific log function, or we have to manually propagate the LogFunction to the parent function.

One solution might be to move the LogFunction to a reader monad, but this still carries a cost and the program will need to be transformed. A less invasive technique is to use an implicit parameter.

Implicit parameters act like parameters to a function, except the caller never has to apply the function to the argument. Instead, the argument is automatically passed to the function by merely being in scope. Let’s see how this works out for our logger:

queueNewChristmasPresents2 :: (?logMessage :: LogFunction) => [Present] -> IO ()
queueNewChristmasPresents2 presents = do
  mapM (?logMessage . ("Queueing present for delivery: " ++)) presents
  return ()

As you can see, something interesting has happened. The LogFunction is no longer a parameter to the function, but is rather part of the context of the function - constraints that must be satisfied when we use the program. The body of the program is mostly the same, other than the leading ? that prefixes implicit parameters.

To supply an implicit parameter, all we need to do is bring an appropriately named variable into scope:

ex1 :: IO ()
ex1 =
  let ?logMessage = \t -> putStrLn ("[XMAS LOG]: " ++ t)
  in queueNewChristmasPresents2 ["Cuddly Lambda", "Gamma Christmas Pudding"]
.> ex1
[XMAS LOG]: Queueing present for delivery: Cuddly Lambda
[XMAS LOG]: Queueing present for delivery: Gamma Christmas Pudding

Perfect, we’ve got the same type of programs as we saw yesterday!

However, we now have the ability to use two different types of loggers in the same program:

ex2 :: IO ()
ex2 = do
  -- Specifies its own logger
  ex1

  -- We can locally define a new logging function
  let ?logMessage = \t -> putStrLn (zipWith (\i c -> if even i
                                                     then c
                                                     else toUpper c)
                                           [0..]
                                           t)
  queueNewChristmasPresents2 ["Category Theory Books"]
[XMAS LOG]: Queueing present for delivery: Cuddly Lambda
[XMAS LOG]: Queueing present for delivery: Gamma Christmas Pudding
QUeUeInG PrEsEnT FoR DeLiVeRy: CaTeGoRy THeOrY BoOkS

Neat!

Implicit parameters are something of a love-hate extension, to the best of my knowledge - though it can be mighty useful sometimes. Also, as with many aspects of Haskell - it is useful for things that were never even anticipated. Tom Ellis has a blog post that demonstrates how one can “fake” a module system by using implicit parameters.


This post is part of 24 Days of GHC Extensions - for more posts like this, check out the calendar.


You can contact me via email at ollie@ocharles.org.uk or tweet to me @acid2. I share almost all of my work at GitHub. This post is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.