I want to write a text interface, which provides some default commands. This program supports tab completion of those commands.
This program also records user inputs and stores it in StateData
. And now I want this program to support tab completion of those user inputs. For example:
*Main > main
> read a<tab> -- press tab and no suggestions (read is a default command)
> read abcde
...
> read a<tab> -- press tab
abcde -- suggestions
Is it possible to do that without using unsafe mechanism like IORef
? Is there a way to pass updated st
from loop
(in repl
) to replSettings startState
(in repl
)?
I am new to Haskeline and thanks for your time.
repl :: StateData -> IO()
repl startState = runInputT (replSettings startState) $ loop startState
where
loop :: StateData -> InputT IO ()
loop st = do
inputL <- getInputLine "> "
case inputL of
Nothing -> return ()
Just "quit" -> outputStrLn "--Exited--" >> return ()
Just ipt -> do (opt, st') <- process ipt `runStateT` st
...
loop st'
replSettings :: StateData -> Settings IO
replSettings st =
Settings
{ complete = replCompletion st,
historyFile = Just "history.txt",
autoAddHistory = True
}
replCompletion :: StateData -> CompletionFunc IO
replCompletion st = completeWordWithPrev Nothing [' '] st (\x y -> return $ completionGenerator x y)
completionGenerator :: String -> String -> StateData -> [Completion]
completionGenerator "" c st =
commandSuggestion c (updatesSuggestions st) -- I wish to update it at run time
completionGenerator p c st = ...
IORef
isn’t unsafe; you’re already in IO
, so it’s a perfectly reasonable way to add mutable state here.
But if you want to avoid IO
, you can simply use StateT StateData IO
as the underlying monad for InputT
, and thus the completion function in Settings
. It seems you’re already trying to use StateT
anyway. Here’s a complete example that just adds every entry to a list and autocompletes them naïvely:
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State (StateT, evalStateT, get, modify)
import Data.List (isPrefixOf)
import System.Console.Haskeline
type StateData = [String]
main :: IO ()
main = repl []
repl :: StateData -> IO ()
repl startState
= flip evalStateT startState
$ runInputT replSettings loop
where
loop :: InputT (StateT StateData IO) ()
loop = do
inputL <- getInputLine "> "
case inputL of
Nothing -> pure ()
Just "quit" -> outputStrLn "--Exited--"
Just ipt -> do
-- Just add each entry to the state directly.
lift $ modify (ipt :)
loop
replSettings :: Settings (StateT StateData IO)
replSettings = Settings
{ complete = replCompletion
, historyFile = Just "history.txt"
, autoAddHistory = True
}
replCompletion :: CompletionFunc (StateT StateData IO)
replCompletion = completeWordWithPrev Nothing " " completionGenerator
completionGenerator :: String -> String -> StateT StateData IO [Completion]
completionGenerator prefix suffix = do
st <- get
-- Trivial completion that just ignores the suffix.
pure $ fmap (\ s -> Completion s s True)
$ filter (prefix `isPrefixOf`) st
The completion generator could also be written using MonadState
(from mtl
) to insulate it from being able to access IO
, and other code could likewise use this pure state while being agnostic to IO
. But otherwise, since you’re already in IO
in this code, StateT StateData IO
/ get
/ modify
are no different than ReaderT (IORef StateData) IO
/ readIORef
/ modifyIORef
.
In fact, if you put an IORef
in StateData
, supposing it’s a more complex record type in your code, the latter is a good way to make some parts of it mutable and others immutable.
data StateData = StateData
{ mutableThing :: !(IORef Thing)
, immutableStuff :: !Stuff
…
}