haskellhaskeline

How to change Tab-completed content at runtime in Haskeline?


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 = ...

Solution

  • 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
      …
      }