如何在运行时在 Haskeline 中更改 Tab 完成的内容?

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

我想写一个文本界面,提供一些默认命令。该程序支持这些命令的制表符补全。

该程序还记录用户输入并将其存储在 StateData 中。现在我希望这个程序支持这些用户输入的制表符完成。例如:

*Main > main
> read a<tab> -- press tab and no suggestions (read is a default command)
> read abcde
...
> read a<tab> -- press tab
abcde         -- suggestions

是否可以在不使用像 IORef 这样的不安全机制的情况下做到这一点?有没有办法将更新的 stloop(在 repl 中)传递到 replSettings startState(在 repl 中)?

我是 Haskeline 的新手,感谢您的宝贵时间。

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 并非不安全;您已经在 IO,因此在此处添加可变状态是一种非常合理的方式。

但是如果你想避免 IO,你可以简单地使用 StateT StateData IO 作为 InputT 的底层 monad,因此 Settings 中的完成函数。看来您已经在尝试使用 StateT 了。这是一个完整的示例,它只是将每个条目添加到列表中并天真地自动完成它们:

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

完成生成器也可以使用 MonadState(来自 mtl)编写,以使其无法访问 IO,其他代码同样可以使用此纯状态,同时对 IO 不可知论。但除此之外,由于您在此代码中已经处于 IO,因此 StateT StateData IO / get / modifyReaderT (IORef StateData) IO / readIORef 没有区别/ modifyIORef.

事实上,如果你把一个IORef 放在 StateData中,假设它是一个更复杂的记录类型在你的代码中,后者是一个很好的选择使它的某些部分可变而其他部分不可变的方法。

data StateData = StateData
  { mutableThing   :: !(IORef Thing)
  , immutableStuff :: !Stuff
  …
  }