6

First, sorry if that title makes no sense. I am a little out of my depth here with the terminology.

So, imagine that I'm writing a text editor in Haskell. For the purposes of this question, let's consider a toy text editor that only operates on a single line of text, and has three primitive operations:

  1. Insert a character at the cursor (so all characters under and to the right of the cursor, if any, are moved to the right).
  2. Move the cursor to the left (no effect if it's already at position zero)
  3. Move the cursor to the right (no effect if it's already at the end of the line)

Obviously I am dealing with a state (the "buffer" of my text editor, and the position of its cursor) and some operations that can be applied in some sequence to modify the state. So, I'm going to implement this thing using a state monad that holds two lists, one for the characters behind the cursor and another for those after it. I will write three simple functions in the state monad to provide my three primitive operations:

data ListState a = ListState {
                     before :: [a],
                     after :: [a]
                   }

insert :: MonadState (ListState a) m => a -> m ()
insert x = do
  ListState bs as <- get
  put $ ListState bs (x:as)

moveForward :: MonadState (ListState a) m => m ()
moveForward = do
  ListState bs as <- get
  case as of
    (a:as') -> put $ ListState (a:bs) as'
    _       -> return ()

moveBackward :: MonadState (ListState a) m => m ()
moveBackward = do
  ListState bs as <- get
  case bs of
    (b:bs') -> put $ ListState bs' (b:as)
    _       -> return ()

Cool! Now I can use these three primitives to do more complicated things:

insertList :: MonadState (ListState a) m => [a] -> m ()
insertList xs = forM_ (reverse xs) insert

foo :: MonadState (ListState Char) m => m ()
foo = do
  insertList "This is a idea!"
  replicateM_ 10 moveForward
  insertList "bad "
  replicateM_ 3 moveBackward
  insertList "n't"

main = let state = ListState "" ""
           state' = execState foo state
       in  putStrLn $ reverse (before state') ++ after state'

Using my primitive DSL, I've defined a function using repeated calls to insert to insert a whole list at once, and another function foo that combines all these functions to circuitously build a string:

(mdunsmuir@altair) mdunsmuir/projects >> ./ToyDSL 
This isn't a bad idea!

I find myself following this pattern a lot: define some data type to hold a state that I want to apply a sequence of operations to, and then build out a DSL starting with the most basic atomic operations and with more complex operations defined as combinations of those primitives.

My question is pretty open-ended: For this sort of problem (imagine a more realistic text editor implemented in the same way), is this a generally valid approach? What other approaches might one use? The primary thing that I find concerning about this approach is that if you don't take a disciplined approach to the design of your DSL, you can run into a lot of unpleasant issues that I associate with imperative programming.

1 Answer 1

5

Although this can be a good idea at one level, you want to avoid passing monads around all over the place if you can. It's easier to read, write, compose, and reuse if you write your functions normally, then modify them into a monadic context as needed:

insert x (Buffer before after) = Buffer (x:before) after

insertList xs (Buffer before after) = Buffer (reverse xs ++ before) after

moveForward (Buffer before (x:after)) = Buffer (x:before) after

moveBackward (Buffer (x:before) after) = Buffer before (x:after)

modify2 f a = do {x <- get; put (f a x) } 

insertM = modify2 insert
insertListM = modify2 insertList
moveForwardM = modify moveForward
moveBackwardM = modify moveBackward

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.