Implicit parameters
There are certain types of applications which are configurable where it makes
sense to model this configurability as a global or semi-global set of
configuration values that some or all parts of the program can "implicitly"
access. These configuration values are called "implicit parameters".
ImplicitParams
in Haskell
Haskell already has support for implicit parameters via the ImplicitParams
extension. However, ImplicitParams
has several flaws and
is barely used at all in modern Haskell code. Many Haskellers consider its
(ab)use of let
/where
bindings to pass implicit parameters to be ugly.
Also, it's questionable how "implicit" the implicit parameters of
ImplicitParams
actually are, as they show up in the context of the type
signature of any function which uses them. There's also no way you can call a
function taking an implicit parameter without passing it that parameter if it
isn't already in context: i.e., there is no way to specify a "default" value
for an implicit parameter if none is passed.
Motivating example
implicit-params
solves some of these problems and introduces new problems of
its own. However, there is one particular use case which motivated me to
develop implicit-params
that isn't supported by the existing
ImplicitParams
extension. Imagine you have the following code:
app :: Config -> IO ()
app config = doStuffWith config
defaultConfig :: Config
defaultConfig = ...
Which is used by a program as follows:
main = app defaultConfig
There are two problems with this code. One is that app
has to manually plumb
the Config
value it was given around everywhere. One solution here would be
for app
to use a Reader monad internally, but that can complicate
the code in some ways and it seems like overkill. If it used the
ImplicitParams
extension, the above code would look like this:
app :: (?config :: Config) => IO ()
app = doStuffWith ?config
defaultConfig :: Config
defaultConfig = ...
main = let ?config = defaultConfig in app
You can see why ImplicitParams
isn't very highly regarded: it made the type
signature of app
longer, as well as the code that uses it. However, at least
the internals of app
will be somewhat nicer now as the Config
value won't
have to be manually plumbed around everywhere.
data-default
The data-default
package provides a type class Default
which represents the class of types which have a "default" value. It has a
single operation def
which returns the default value for a given type (the
type is given by type inference). Using Default
the above code could be made
a little nicer:
app :: (?config :: Config) => IO ()
app = doStuffWith ?config
instance Default Config where def = ...
main = let ?config = def in app
However, the above code is still so ugly considering what we're trying to
do: all we want to do is run app
with the defaults. This should be as simple
as typing app
, and only if we're overriding the defaults should the code
need to be any longer than this. This is exactly what implicit-params
does.
If an implicit parameter is not explicitly given to a function which requires
it, its value is given by def
for the Default
instance for the type of the
parameter. And if the type does not have a Default
instance, then it is a
type error to call that function without explicitly setting the implicit
parameter (but it will work fine if you do set it). This is how the above code
looks using implicit-params
:
app :: Implicit_ Config => IO ()
app = doStuffWith param_
instance Default Config where def = defaultConfig
main = app
Perfect! What if we want to pass a non-default config to app
? That's easy
too:
main = setParam_ (def {option = 1}) app
setParam_
even has an infix synonym $~
which makes the above code even
nicer:
main = app $~ def {option = 1}
(Bonus points for not abusing let
/where
bindings.)
Named implicit parameters
The above code uses unnamed implicit parameters, which will suffice for most
code. Sometimes you might want to pass more than one implicit parameter of the
same type to a single function, and for this you need some way of selecting
the particular implicit parameter on which to operate. implicit-params
uses
type level symbols for this, which require the DataKinds
extension.
Implicit_
, which is used in the examples above, denotes an unnamed implicit
parameter. Implicit "foo"
can be used to denote a named implicit parameter
named "foo"
. Named implicit parameters are slightly more awkward to use
because they require passing Proxy
parameters to the param
and
setParam
functions to specify the names of the implicit parameters on which
they are to operate. See the Haddock documentation of the Data.Implicit
module for more details.
Acknowledgements
This wouldn't be possible without techniques that I learnt from
Edward Kmett and Philip JF. In particular, this package
uses ideas from Edward's packages tagged, constraints
and reflection and Philip's blog posts Haskell Supports
First-Class Instances and Using Compiler Bugs for Fun and
Profit: Introducing Cartesian Closed Constraints.