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:
- Insert a character at the cursor (so all characters under and to the right of the cursor, if any, are moved to the right).
- Move the cursor to the left (no effect if it's already at position zero)
- 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.