hint
This library defines an Interpreter monad within which you can interpret
strings like "[1,2] ++ [3]"
into values like [1,2,3]
. You can easily
exchange data between your compiled program and your interpreted program, as
long as the data has a Typeable
instance.
You can choose which modules should be in scope while evaluating these
expressions, you can browse the contents of those modules, and you can ask for
the type of the identifiers you're browsing.
Example
{-# LANGUAGE LambdaCase, ScopedTypeVariables, TypeApplications #-}
import Control.Exception (throwIO)
import Control.Monad (when)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Writer (execWriterT, tell)
import Data.Foldable (for_)
import Data.List (isPrefixOf)
import Data.Typeable (Typeable)
import qualified Language.Haskell.Interpreter as Hint
-- |
-- Interpret expressions into values:
--
-- >>> eval @[Int] "[1,2] ++ [3]"
-- Right [1,2,3]
--
-- Send values from your compiled program to your interpreted program by
-- interpreting a function:
--
-- >>> Right f <- eval @(Int -> [Int]) "\\x -> [1..x]"
-- >>> f 5
-- [1,2,3,4,5]
eval :: forall t. Typeable t
=> String -> IO (Either Hint.InterpreterError t)
eval s = Hint.runInterpreter $ do
Hint.setImports ["Prelude"]
Hint.interpret s (Hint.as :: t)
-- |
-- >>> :{
-- do Right contents <- browse "Prelude"
-- for_ contents $ \(identifier, tp) -> do
-- when ("put" `isPrefixOf` identifier) $ do
-- putStrLn $ identifier ++ " :: " ++ tp
-- :}
-- putChar :: Char -> IO ()
-- putStr :: String -> IO ()
-- putStrLn :: String -> IO ()
browse :: Hint.ModuleName -> IO (Either Hint.InterpreterError [(String, String)])
browse moduleName = Hint.runInterpreter $ do
Hint.setImports ["Prelude", "Data.Typeable", moduleName]
exports <- Hint.getModuleExports moduleName
execWriterT $ do
for_ exports $ \case
Hint.Fun identifier -> do
tp <- lift $ Hint.typeOf identifier
tell [(identifier, tp)]
_ -> pure () -- skip datatypes and typeclasses
Check example.hs for a longer example (it must be run
from hint's base directory).
Limitations
Importing a module from the current package is not supported. It might look
like it works on one day and then segfault the next day. You have been warned.
To work around this limitation, move those modules to a separate package. Now
the part of your code which calls hint and the code interpreted by hint can
both import that module.
It is not possible to exchange a value whose type involves an implicit kind
parameter.
This includes type-level lists. To work around this limitation, define a
newtype wrapper which wraps the type you
want.
It is possible to run the interpreter inside a thread, but on GHC 8.8 and
below, you can't run two instances of the interpreter simultaneously.
GHC must be installed on the system on which the compiled executable is
running. There is a workaround for this but it's not trivial.
The packages used by the interpreted code must be installed in a package
database, and hint needs to be told about that package database at runtime.
The most common use case for package databases is for the interpreted code to
have access to the same packages as the compiled code (but not compiled code
itself). The easiest way to accomplish this is via a
GHC environment file,
and the easiest way to generate a GHC environment file is
via cabal.
Compile your code using cabal build --write-ghc-environment-files=always
;
this will create a file named .ghc.environment.<something>
in the current
directory. At runtime, hint will look for that file in the current directory.
For more advanced use cases, you can use
unsafeRunInterpreterWithArgs
to pass arguments to the underlying ghc library, such as
-package-db
to specify a path to a package database, or
-package-env
to specify a path to a GHC environment file.