Tips for using Effects in Idris

Effects is an Idris package that implements resource-dependent algebraic effects. This allows a developer to control at a fine-a-grained level the side affects (state, logging, IO) for effectful programs. A tutorial and more background on their use is available on line.

When writting my Idris libraries I have used Effects to manage exceptions, state, logging, and IO. Here are some tips in my experiences in using Effects.

Global Effects List

In an effectful function the effects used are declared in a List data structure of type List EFFECT. An effectful main function will look like:

fooMain : Eff () [FILE_IO, STDIO, SYSTEM]
fooMain = do
   ...

The top most list of effects will determine the complete set of effects allowed in sub-effectful programs. Good practice will be

In this list the order in which effects appear will have an effect on the order in which sub lists can be created for effectful programs called. An engineering tip will be to create a global list of effects that allows for the list of effects to be passed around to all effectful programs. It also saves on typing.

Effs.idr

Most, if not all, of my projects have a file called Effs.idr. This file is used to control the imports of effects, and creation of effectful utility functions for working with state, logging, and IO. This allows me to keep track of the effects I use and isolate the majority of utility functions to a single file.

Labelling Effects.

If you are creating a library to be used by others that contains effectful functions, be proactive and label the effects that are native to your library. For example, the file, stdio, log, and system effects are shared by all effectful programs and need not be labelled. The use of effects such as exception and state will be custom to your library. This advice holds if you are using only a single instance of the State effect, as it will be guaranteed that a user might use a library that has their own effect. Label it.

These labels should be unique and a simple naming scheme can be ==. For example:

  • Exceptions in Foo would be labelled =fooerr=
  • State in Foo would be labelled =foostate=

The only place in which you would not want to label your state effect if you know that the effect will be run in isolation from the main effectful program.

Hiding Labels

With the use of labels can come messy looking code and forgetfulness of label names. The latter especially if you hide the list of effects.

Take for example:

FooEffs : List EFFECT
FooEffs = [ FILE_IO (), SYSTEM, STDIO
          , 'foost ::: STATE FState
          , 'fopts ::: STATE FOpts
          , 'foosymtbl ::: STATE SymTbl
          , 'fooerr ::: Exception FooError]

fooMain : Eff () FooEffs
fooMain = do
  as <- getArgs
  opts <- processArgs as
  case opts of
    Nothing => 'fooerr :- raise InvalidOpts
    Just os => do
      'fopts :- put os
      st <- doInit
      case st of
        Nothing => 'fooerr :- raise InvalidState
        Just s => do
          'foost :- put st
          doStuff ...

A good trick is to re-domain the effects such that the effectful functions are accessed using the form =.=, or are placed in a wrapper functions. For example, take the Exception effect:

namespace Foo
  raise : FooError -> Eff b ['fooerr ::: Exception FooError]
  raise err = 'fooerr :- Exception.raise err

Accessing state becomes:

getFState : Eff FState ['foost ::: STATE FState]
getFState = 'foost :- get

updateFState : (FState -> FState) -> Eff () ['foost ::: STATE FState]
updateFState u = 'foost :- update (\st => u st)

setFState : FState -> Eff () ['foost ::: STATE FState]
setFState newST = 'foost :- put newST

And option accessors will have type:

getFOptions : Eff FOpts ['fopts ::: STATE FOpts]
putFOptions : FOpts -> Eff () ['fopts ::: STATE FOpts]

The program from above now becomes:

fooMain : Eff () FooEffs
fooMain = do
  as <- getArgs
  opts <- processArgs as
  case opts of
    Nothing => Foo.raise InvalidOpts
    Just os => do
      setFOptions os
      st <- doInit
      case st of
        Nothing => Foo.raise InvalidState
        Just s => do
          setFState s
          doStuff ...

An added benefit of this approach is that it adds a level of indirection in case you wish to update how you deal with your state affects… Which leads on to the final hint for using effects.

Collapse State Effects

If you find that you are using multiple state effects in your program that you define yourself, consider collapsing them into a single record and write some wrapper effectful functions to obtain the requisite state from the global state.