Manage effects in DSLs (part 2)

Hi, this is the second post in the exploration of the design of Nomyx. We will try to solve again the problem of managing effects in DSLs, this time using type classes (see part 1 to see how to solve it using a polymorphic type parameter). As a reminder, the problem is to separate semantically the instructions that have an “effect” in a DSL, from those who don’t. Of course, an effect-less instruction could be run in an effect-full context, but not the opposite! How to encode this semantic?

Preliminaries:

> {-# LANGUAGE RankNTypes #-}
> {-# LANGUAGE FlexibleInstances #-}
> module Main where
> import Control.Monad.State
> import Control.Monad.Reader

We define a class Nomex, which will hold effectless instances of a Nomex language.

> class Monad m => Nomex m where
>   readAccount :: m Int

We also define a class NomexEffect, for effectfull versions of the language. Note that the effectful version contains the effectless one.

> class Nomex m => NomexEffect m where
>   writeAccount :: Int -> m ()
>   setVictory   :: (forall n. Nomex n => n Bool) -> m ()

Here is the state of the game. We can set a victory rule, that is run will tell us if we won. We can also have money on our account!

> data Game = Game { victory :: (forall m. Nomex m => m Bool)
>                  , account :: Int
>                  }

Here is where is becomes interesting. Our language is dedicated to read (for the effectless part) and modify (for the effectful part) the game state. So here is the instance of the language to read the game state:

> instance Nomex (Reader Game) where
>   readAccount = asks account

And here is it for stateful computation. We define an instance of NomexEffect for State Game:

> instance NomexEffect (State Game) where
>   writeAccount n = modify $ \game -> game { account = n }
>   setVictory   v = modify $ \game -> game { victory = v }

We define it also for effectless computations:

> instance Nomex (State Game) where
>   readAccount = liftEval readAccount 
> liftEval :: Reader Game a -> State Game a
> liftEval r = get >>= return . runReader r 

We are now able to define effectful computations, mixing effectful and effectless instructions:

> incrAccount :: NomexEffect m => m ()
> incrAccount = do
>    a <- readAccount 
>    writeAccount (a + 101)

We can also safely define expressions that must not yield any effect. Here is our victory condition. Note that effectful instructions are not accepted, which is what we wanted!

> victoryCondition :: Nomex m => m Bool
> victoryCondition = do
>    i <- readAccount
>    --writeAccount 100 --This would not compile (good!)
>    return (i > 100)
> winOnBigMoney :: NomexEffect m => m ()
> winOnBigMoney = setVictory victoryCondition 

All this allows us to define a pure function able to determine if we won the game:

> isVictory :: Game -> Bool
> isVictory g = runReader (victory g) g

We can now play! We first define how to win the game. The condition will be stored in the game state as an expression. We then increment the bank account.

> play = do
>    winOnBigMoney
>    incrAccount

Let’s check that everything worked correctly:

> initGame = Game (return False) 0
> main = putStrLn $ show $ isVictory $ execState play initGame

Running the program will display the value “True”: We won!

#Conclusion

So finally, there are really 3 solutions to the problem of representing the semantic of effects/no effects (effect-less instructions can be run in effect-full context, but not the opposite). We can encode this semantic at (click on the links for full solutions):

At value level, the semantic is encoded by a DSL instruction ‘NoEff’ that wraps an effect-less instruction into an effect-full one:

NoEff        :: Nomex NoEffect a -> Nomex Effect a

At type level, the semantic is encoded by using a polymorphic type parameter that can take two concrete type, ‘Effect’ or ‘NoEffect’:

ReadAccount  :: Nomex r Int

At typeclass level, the semantic is encoded by the hierarchy of classes:

class NomexNoEffect m => NomexEffect m where...

Here are their pros and cons, in my opinion:

Value level solution:

Pros:

Cons:

Type level solution:

Pros:

Cons:

Type class solution:

Pros:

Cons:

Special thanks to the Haskell community who helped me. Especially the persons in this Reddit post and this mailing list thread.

Comments