Финал: load, CLI на optparse-applicative, REPL на haskeline
Язык готов, делаем из него инструмент. Спец-форма
loadи поискstd.lspпоLISHP_STD_PATHи рабочей директории,runFilesс общим контекстом между файлами, CLI наoptparse-applicative(run --evalи список файлов), REPL наhaskelineс мультилайн-вводом по незакрытой скобке, историей, автодополнением пути в:load, командами:help,:quit,:historyи ANSI-баннером. В конце ретроспектива проекта и дорожная карта расширений (TCO, continuations, гигиена макросов) как открытое задание.
Идея
Язык готов, теперь сделаем из него инструмент. И это тоже дизайн языка, а не обёртка поверх. Главная мысль урока в том, что read и eval, написанные для проекта, переиспользуются в обоих режимах: и когда ты запускаешь файл целиком (batch), и когда вводишь по одной форме в интерактивной консоли (REPL). Один движок, две поверхности.
load и поиск std.lsp
Спец-форма load читает файл и вычисляет его формы в текущем окружении (поэтому загруженные определения остаются доступны). Это последняя ветка evalSpecial:
Load -> with1 $ \pe -> do
pv <- eval env pe
case pv of
Str path -> do
contents <- liftIO (readFile path)
case readProgram contents of
Left err -> throwError (ParseFailure err)
Right exprs -> foldM (\_ e -> eval env e) Nil exprs -- вычислить все формы
_ -> throwError (TypeError "load expects a string path argument")
На том же readProgram плюс evalAll стоит загрузка std.lsp при старте сессии. Окружение сессии это пустое окружение, поверх которого накатили стандартную библиотеку:
-- окружение сессии: пустое, затем std.lsp сверху
newReplEnv :: IO Env
newReplEnv = do
env <- newEnv
loadStd env
pure env
-- где искать std.lsp, по порядку: переменная окружения, рабочая папка, ~/.lishp
stdSearchPaths :: IO [FilePath]
stdSearchPaths = do
override <- lookupEnv "LISHP_STD_PATH"
home <- lookupEnv "HOME"
pure $ concat
[ maybe [] pure override
, ["std/std.lsp", "std.lsp"]
, maybe [] (\h -> [h ++ "/.lishp/std.lsp"]) home
]
Запуск файлов это runFiles: один общий env на весь список, чтобы поздние файлы видели определения ранних. Первая ошибка останавливает прогон с ненулевым кодом возврата:
runFiles :: [FilePath] -> IO ()
runFiles paths = do
env <- newReplEnv -- общий контекст со std
go env paths
where
go _ [] = pure ()
go env (path : rest) = do
contents <- readFile path
case readProgram contents of
Left err -> fail' ("parse error: " ++ err)
Right exprs -> evalAll env exprs >>= either (fail' . show) (const (go env rest))
CLI на optparse-applicative
Командную строку собираем декларативно на optparse-applicative. Красота в том, что парсер опций это Applicative, ровно как парсеры из урока 16: маленькие парсеры флагов комбинируются в большой. Две подкоманды: repl (по умолчанию) и run:
import Options.Applicative
data Command = Repl | Run RunOptions
data RunOptions = RunOptions { runEval :: Maybe String, runFilesArg :: [FilePath] }
runOptions :: Parser RunOptions
runOptions =
RunOptions
<$> optional (strOption (long "eval" <> short 'e' <> metavar "EXPR"
<> help "Вычислить выражение из строки"))
<*> many (argument str (metavar "FILES..." <> help "Файлы (общий контекст)"))
commandParser :: Parser Command
commandParser =
subparser
( command "repl" (info (pure Repl) (progDesc "Интерактивный REPL"))
<> command "run" (info (Run <$> runOptions) (progDesc "Запустить код"))
)
<|> pure Repl -- без подкоманды: REPL
<$> и <*> тут собирают RunOptions из двух парсеров полей, точно так же, как (,) <$> p <*> q собирал пару в парсере. Диспетчер выбирает, что делать:
dispatch :: Command -> IO ()
dispatch Repl = runRepl
dispatch (Run (RunOptions (Just expr) _)) = runEvalExpr expr -- run --eval "..."
dispatch (Run (RunOptions Nothing files)) = runFiles files -- run file1 file2
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.