I am trying to design embedded language, where operations can raise certain flags depending on values. I foresee operation on scalar values as well as on vectors (e.g. map, fold, etc.) My idea is to use Writer Monad to keep track of flags. Simplified example, where actual type is "Int" and flag is raised if any of argument is 0.
import Control.Monad.Identity
import Control.Monad.Writer
import Data.Monoid
type WInt = Writer Any Int
bplus :: Int -> Int -> WInt
bplus a b =
do
tell (Any (a == 0 || b == 0)) ;
return (a+b)
wbplus :: WInt -> WInt -> WInt
wbplus wa wb =
do
a <- wa ;
b <- wb ;
tell (Any (a == 0 || b == 0)) ;
return (a+b)
ex0 = runWriter (bplus 1 2)
ex1 = runWriter (bplus 0 2)
ex2 = runWriter (wbplus (return 1) (return 2))
ex3 = runWriter (wbplus (return 0) (return 2))
ex4 = runWriter (wbplus (wbplus (return 1) (return 2)) (return 2))
ex5 = runWriter (wbplus (wbplus (return 0) (return 2)) (return 2))
ex6 = runWriter (wbplus (wbplus (return 1) (return 2)) (return 0))
I am little unsure what is the best way to implement this. Some questions:
Should I define all operations like I did for
bplus
or like forwbplus
. Laters makes composition easier, it seems. But to usefoldM
binary operator should have typeInt -> Int -> WInt
.What would be the appropriate type for lists:
Writer Any [Int]
or[Wint]
?
Any suggestions or thoughts are appreciated.
You can derive
bplus
fromwbplus
and vice versa using the appropriate monadic operations:They are inverses of each other, evident from the type signatures of their compositions:
Now you can define
wbplus = apM2 bplus
orbplus = pureM2 wbplus
. There's no definite answer which one is better, use your taste and judgement. TemplateHaskell goes with thewbplus
approach and defines all operations to work with values in theQ
monad. See Language.Haskell.TH.Lib.Regarding
[m a]
vsm [a]
, you can only go in one direction (viasequence :: Monad m => [m a] -> m [a]
). Would you ever want to go in the opposite direction? Do you care about individual values having their own flags or would you rather annotate the computation as a whole with flags?The real question is, what is your mental model for this? However, let's think about some consequences of each design choice.
If you choose to represent each value as
Writer Any a
and have all operations work with it, you can start with anewtype
:Now you can define instances of standard type classes for your values:
For an EDSL this gives a huge advantage: terseness and syntactic support from the compiler. You can now write
getValue (42 + 0)
instead ofwbplus (pure 42) (pure 0)
.If, instead, you don't think about flags as a part of your values and rather see them as an external effect, it's better to go with the alternative approach. But rather than write something like
Writer Any [Int]
, use corresponding classes frommtl
:MonadWriter Any m => m [Int]
. This way, if you later find out that you need to use other effects, you can easily add them to some (but not all) operations. For example, you might want to raise an error in case of division by zero:Now you can use
plusF
anddivZ
together within one monad, although they have different effects. If you'll later find yourself in need to integrate with some external library, this flexibility will come in handy.Now, I didn't give it too much thought, but perhaps you could combine those approaches using something like
newtype Value m a = Value { getValue :: m a }
. Good luck exploring the design space :)