英文:
How to dynamically compile and import a module using GHC API - LTS Haskell 20.20 (ghc-9.2.7)?
问题
背景
我想在Haskell中实现一个程序,该程序可以动态生成Haskell模块(hs-文件),然后编译并将其导入到生成代码的同一应用程序中。
因此,我尝试理解Stack Overflow和文档中的一些示例代码片段。但它既不能工作也不能编译。
在对给定示例的某些修改之后,我创建了以下源代码Main.hs:
import qualified GhcApiWrap as Ghw
main :: IO ()
main = Ghw.msLoadModuleAndExecute "../dyn/" "DynExample.hs" "nFromChar" 'A'
GhcApiWrap.hs:
module GhcApiWrap
(
msLoadModuleAndExecute
) where
import GHC
msLoadModuleAndExecute :: String -> String -> String -> Char -> IO ()
msLoadModuleAndExecute _ _ _ _ = do
value' <- runGhc (Just "./src/") $ do
dflags <- getSessionDynFlags
setSessionDynFlags $ dflags {
ghcLink = LinkInMemory,
ghcMode = CompManager,
objectDir = Just "../dyn/",
hiDir = Just "../dyn/"
}
target <- guessTarget "DynExample.hs" Nothing
setTargets [target]
ret <- load LoadAllTargets
case ret of
Succeeded -> do
importDecl_RdrName <- parseImportDecl "import DynExample"
setContext [IIDecl importDecl_RdrName]
value <- dynCompileExpr "DynExample.nFromChar"
return value
_ ->
return undefined
print $ value'
DynExample.hs:
module DynExample
(
nFromChar
) where
nFromChar :: Char -> Int
nFromChar _ = 33
... 这个程序能够编译但无法工作。
它的输出是:
ExprmntGhcApi-exe: Missing file: src/settings
如您所见,我注释掉了 import GHC.Paths (libdir)
,因为它不再存在。因此,我谨慎地猜测,我使用了 (Just "./src/")
代替了 (Just libdir)
。
我尝试了几个版本,但没有一个能与GHC 9.2.7一起工作。
问题
是否可以使用最新的Haskell Stack环境以及以下函数类型,以动态方式使用 DynExample.nFromChar
这种方式?
msLoadModuleAndExecute :: String -> String -> String -> Char -> ... Int
环境
目前,我正在使用带有以下配置的Stack:
resolver:
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml
... 这对应于使用GHC 9.2.7。
英文:
Background
I would like to implement a program in Haskell that can generate Haskell module (hs-File) dynamically and then compile, and import it into the same application that generated the code.
Therefore, I tried to understand some example code snippets in SO and documentation. But it doesn't work nor compiles.
After some modifications of one of the given examples, I created the following souce code, Main.hs:
import qualified GhcApiWrap as Ghw
main :: IO ()
main = Ghw.msLoadModuleAndExecute "../dyn/" "DynExample.hs" "nFromChar" 'A'
GhcApiWrap.hs:
module GhcApiWrap
(
msLoadModuleAndExecute
) where
import GHC
--import GHC.Paths (libdir)
msLoadModuleAndExecute :: String -> String -> String -> Char -> IO ()
msLoadModuleAndExecute _ _ _ _ = do
value' <- runGhc (Just "./src/") $ do
dflags <- getSessionDynFlags
setSessionDynFlags $ dflags {
ghcLink = LinkInMemory,
ghcMode = CompManager,
objectDir = Just "../dyn/",
hiDir = Just "../dyn/"
}
target <- guessTarget ("DynExample.hs") Nothing
setTargets [target]
ret <- load LoadAllTargets
case ret of
Succeeded -> do
importDecl_RdrName <- parseImportDecl $ "import DynExample"
setContext [IIDecl importDecl_RdrName]
value <- dynCompileExpr ("DynExample.nFromChar")
return value
_ ->
return undefined
print $ value'
DynExample.hs:
module DynExample
(
nFromChar
) where
nFromChar :: Char -> Int
nFromChar _ = 33
...which compiles but doesn't work.
Its ouput is:
ExprmntGhcApi-exe: Missing file: src/settings
As you can see, I commented out import GHC.Paths (libdir)
because it doesn't exist anymore. Therefore, as a humble guess I used (Just "./src/")
instead of (Just libdir)
.
I tried several versions that are "flying around", but none of them work with GHC 9.2.7.
Question
Is it, and how is it possible to use DynExample.nFromChar
dynamically this way using an up-to-date Haskell Stack environment, and having the following type of the function?
msLoadModuleAndExecute :: String -> String -> String -> Char -> ... Int
Environment
At the moment, I am using Stack with:
resolver:
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml
...which translates to usage of GHC 9.2.7.
答案1
得分: 2
所以,通过添加对ghc-paths包的依赖,解决了import GHC.Paths (libdir)
的问题。
接下来的问题是正确地为guessTarget
提供参数。从文档中可以看到:
尝试猜测字符串引用的目标是什么。这个函数实现了--make/GHCi命令行语法用于文件名。
因此,需要提供模块的路径而不仅仅是名称。
最后的问题是,dynCompileExpr
返回一个Dynamic对象,应该将其转换为您的函数类型Char -> Int
,然后进行计算。
将所有这些组合起来,我们可以得到:
module GhcApiWrap
(
msLoadModuleAndExecute
) where
import Control.Exception.Safe
import Data.Dynamic
import GHC
import GHC.Paths (libdir)
import System.FilePath
msLoadModuleAndExecute :: FilePath -> FilePath -> String -> Char -> IO Int
msLoadModuleAndExecute buildDir modulePath funcName arg = do
dynFunc <- runGhc (Just libdir) $ do
dflags <- getSessionDynFlags
setSessionDynFlags $ dflags { ghcLink = LinkInMemory
, ghcMode = CompManager
, objectDir = Just buildDir
, hiDir = Just buildDir
}
target <- guessTarget modulePath Nothing
setTargets [target]
loadStatus <- load LoadAllTargets
case loadStatus of
Succeeded -> do
let moduleName = dropExtension $ takeFileName modulePath
importDecl_RdrName <- parseImportDecl $ "import " ++ moduleName
setContext [IIDecl importDecl_RdrName]
dynCompileExpr $ moduleName ++ "." ++ funcName
Failed -> throwString $ "could not load the module: " ++ modulePath
case fromDynamic dynFunc of
Just func -> pure $ func arg
Nothing -> throwString $ funcName ++ " has type '" ++ show dynFunc ++ "' but expected type `Char -> Int'"
可以通过以下方式运行它:
import qualified GhcApiWrap as Ghw
main :: IO ()
main = print =<< Ghw.msLoadModuleAndExecute "../dyn/" "DynExample.hs" "nFromChar" 'A'
但我认为更好的做法是简化这个函数,只保留compileAndLoad
,使其更通用。
{-# LANGUAGE ScopedTypeVariables #-}
module GhcApiWrap
(
msCompileAndLoad
) where
import Control.Exception.Safe
import Data.Dynamic
import Data.Proxy
import Data.Typeable
import GHC
import GHC.Paths (libdir)
import System.FilePath
msCompileAndLoad :: forall a. Typeable a => FilePath -> FilePath -> String -> IO a
msCompileAndLoad buildDir modulePath symbolName = do
dynSymbol <- runGhc (Just libdir) $ do
dflags <- getSessionDynFlags
setSessionDynFlags $ dflags { ghcLink = LinkInMemory
, ghcMode = CompManager
, objectDir = Just buildDir
, hiDir = Just buildDir
}
target <- guessTarget modulePath Nothing
setTargets [target]
loadStatus <- load LoadAllTargets
case loadStatus of
Succeeded -> do
let moduleName = dropExtension $ takeFileName modulePath
importDecl_RdrName <- parseImportDecl $ "import " ++ moduleName
setContext [IIDecl importDecl_RdrName]
dynCompileExpr $ moduleName ++ "." ++ symbolName
Failed -> throwString $ "could not load the module: " ++ modulePath
case fromDynamic dynSymbol of
Just x -> pure x
Nothing -> throwString $ symbolName ++ " has type '" ++ show dynSymbol
++ "' but expected type '" ++ show symbolType ++ "'"
where
symbolType = typeRep (Proxy :: Proxy a)
然后我们可以使用它:
import qualified GhcApiWrap as Ghw
main :: IO ()
main = do
nFromChar <- Ghw.msCompileAndLoad "../dyn/" "DynExample.hs" "nFromChar"
print $ (nFromChar :: Char -> Int) 'A'
希望这对你有帮助。
英文:
So, issue with import GHC.Paths (libdir)
solved by adding a dependence to the ghc-paths package.
Next issue was with provide correct argument to the guessTarget
. From the doc:
> Attempts to guess what Target a string refers to. This function implements the --make/GHCi command-line syntax for filenames.
So, need provide path to the module not just the name.
And last problem is that dynCompileExpr
returns Dynamic object which should be casted to type of your function Char -> Int
and then calculate.
Combine all together we can get:
module GhcApiWrap
(
msLoadModuleAndExecute
) where
import Control.Exception.Safe
import Data.Dynamic
import GHC
import GHC.Paths (libdir)
import System.FilePath
msLoadModuleAndExecute :: FilePath -> FilePath -> String -> Char -> IO Int
msLoadModuleAndExecute buildDir modulePath funcName arg = do
dynFunc <- runGhc (Just libdir) $ do
dflags <- getSessionDynFlags
setSessionDynFlags $ dflags { ghcLink = LinkInMemory
, ghcMode = CompManager
, objectDir = Just buildDir
, hiDir = Just buildDir
}
target <- guessTarget modulePath Nothing
setTargets [target]
loadStatus <- load LoadAllTargets
case loadStatus of
Succeeded -> do
let moduleName = dropExtension $ takeFileName modulePath
importDecl_RdrName <- parseImportDecl $ "import " ++ moduleName
setContext [IIDecl importDecl_RdrName]
dynCompileExpr $ moduleName ++ "." ++ funcName
Failed -> throwString $ "could not load the module: " ++ modulePath
case fromDynamic dynFunc of
Just func -> pure $ func arg
Nothing -> throwString $ funcName ++ " has type '" ++ show dynFunc ++ " but expected type `Char -> Int'"
which can be run with:
import qualified GhcApiWrap as Ghw
main :: IO ()
main = print =<< Ghw.msLoadModuleAndExecute "../dyn/" "DynExample.hs" "nFromChar" 'A'
But I think will be better simplify this function on just compileAndLoad
that make it more generic.
{-# LANGUAGE ScopedTypeVariables #-}
module GhcApiWrap
(
msCompileAndLoad
) where
import Control.Exception.Safe
import Data.Dynamic
import Data.Proxy
import Data.Typeable
import GHC
import GHC.Paths (libdir)
import System.FilePath
msCompileAndLoad :: forall a. Typeable a => FilePath -> FilePath -> String -> IO a
msCompileAndLoad buildDir modulePath symbolName = do
dynSymbol <- runGhc (Just libdir) $ do
dflags <- getSessionDynFlags
setSessionDynFlags $ dflags { ghcLink = LinkInMemory
, ghcMode = CompManager
, objectDir = Just buildDir
, hiDir = Just buildDir
}
target <- guessTarget modulePath Nothing
setTargets [target]
loadStatus <- load LoadAllTargets
case loadStatus of
Succeeded -> do
let moduleName = dropExtension $ takeFileName modulePath
importDecl_RdrName <- parseImportDecl $ "import " ++ moduleName
setContext [IIDecl importDecl_RdrName]
dynCompileExpr $ moduleName ++ "." ++ symbolName
Failed -> throwString $ "could not load the module: " ++ modulePath
case fromDynamic dynSymbol of
Just x -> pure x
Nothing -> throwString $ symbolName ++ " has type '" ++ show dynSymbol
++ "' but expected type '" ++ show symbolType ++ "'"
where
symbolType = typeRep (Proxy :: Proxy a)
Then we can use it:
import qualified GhcApiWrap as Ghw
main :: IO ()
main = do
nFromChar <- Ghw.msCompileAndLoad "../dyn/" "DynExample.hs" "nFromChar"
print $ (nFromChar :: Char -> Int) 'A'
答案2
得分: 0
以下是您提供的内容的中文翻译:
"freestyle" 和 "K. A. Buhr" 的答案有效。
但是,我创建了一个备用版本,应该具有以下特点:
- 不抛出异常,而是通过
Either
转发错误消息 - 捕获所有异常以通过
Either
转发其错误消息 - 仅在需要时修改
dflags
- 避免使用 do 表示法
- 避免使用 case 构造
- 在合理情况下使用限定的导入
- 描述所有必需的内容,包括堆栈设置细节
- 使用模块名称来暗示文件的扩展名为“hs”
- 无 hlint 提示
我对代码进行了测试,包括以下方面:
- 函数
- 模块文件不可用
(例如,“错误:异常:找不到文件:./dyn/DynExample.hs”) - 编译器错误与模块文件相关
(例如,“错误:无法加载模块“DynExample”!”) - 与导入符号相关的类型检查错误
(例如,“错误:符号 nFromChar 的类型为“<<Char -> Int>>”,但期望类型为“Char -> Integer””)
GhcApiWrap
{-# LANGUAGE ScopedTypeVariables #-}
module GhcApiWrap
(
compileAndLoad
) where
import qualified Control.Exception.Safe as Exc
import qualified Data.Dynamic as Dyn
import qualified Data.Proxy as Px
import qualified Data.Typeable as T
import qualified GHC as Ghc
import qualified GHC.Paths as Pth
import Data.Functor ((<&>))
compileAndLoad :: forall a. T.Typeable a => String -> String -> String -> IO (Either String a)
compileAndLoad dir mdl sym = runGhcCatched >>= ethSym
where
runGhcCatched = Ghc.runGhc (Just Pth.libdir) (loadModule dir mdl sym)
`Exc.catchAny`
(\ex -> return (Left ("Exception: " ++ show ex)))
ethSym (Right dynSym) = return (ethSym' (Dyn.fromDynamic dynSym) (sErrFromDynSym dynSym))
ethSym (Left sErr) = return (Left sErr)
sErrFromDynSym dynSym = "Symbol " ++ sym ++ " has type \"" ++ show dynSym ++
"\" but expected type \"" ++ show symbolType ++ "\""
ethSym' (Just dynSym) _ = Right dynSym
ethSym' Nothing sErr = Left sErr
symbolType = T.typeRep (Px.Proxy :: Px.Proxy a)
loadModule :: Ghc.GhcMonad m => String -> String -> String -> m (Either String Dyn.Dynamic)
loadModule dir mdl sym =
Ghc.getSessionDynFlags >>=
Ghc.setSessionDynFlags >>
Ghc.guessTarget (dir ++ mdl ++ ".hs") Nothing >>=
(\target -> Ghc.setTargets [target]) >>
Ghc.load Ghc.LoadAllTargets >>=
returnFromLoadStatus
where
returnFromLoadStatus Ghc.Succeeded = dynSym <&> Right
returnFromLoadStatus Ghc.Failed = return (Left ("Could not load the module \"" ++ mdl ++ "\"!"))
dynSym = Ghc.parseImportDecl ("import " ++ mdl) >>=
(\importDecl -> Ghc.setContext [Ghc.IIDecl importDecl]) >>
Ghc.dynCompileExpr (mdl ++ "." ++ sym)
Usage
{-# LANGUAGE LambdaCase #-}
import qualified GhcApiWrap as Ghw
main :: IO ()
main = Ghw.compileAndLoad "./dyn/" "DynExample" "nFromChar" >>=
(\case
(Right nFromChar) -> putStrLn ("nFromChar 'A' := " ++ show ((nFromChar :: Char -> Integer) 'A'))
(Left sErrorMessage) -> putStrLn ("Error: " ++ sErrorMessage))
动态文件以供阅读(“DynExample.hs”)
注意:文件必须位于与目录“src”并排的目录“dyn”中,以与上面的代码(“Usage”)兼容。
module DynExample
(
nFromChar
) where
import qualified Data.Char as Chr
nFromChar :: Char -> Int
nFromChar = Chr.ord
输出
nFromChar 'A' := 65
package.yaml
...
description:...
dependencies:
- base >= 4.7 && < 5
- ghc
- ghc-paths
- safe-exceptions
ghc-options:
...
stack.yaml
...
resolver:
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml
...
希望这些翻译对您有所帮助。
英文:
The answer from freestyle and K. A. Buhr works.
However, I created an alternative version, which should have the following characteristics:
- not throwing exceptions but forwarding error messages via
Either
- catching all exceptions to forward its error messages via
Either
- not modifying the
dflags
if not needed - avoiding the do notation
- avoiding case constructions
- using qualified imports, where reasonably possible
- describing all necessities including stack setup details
- usage of the module name implying extension “hs” for the file
- hlint free
I tested the code regarding
- function
- unavailability of the module file
(e.g. "Error: Exception: can't find file: ./dyn/DynExample.hs") - compiler error regarding the module file
(e.g. "Error: Could not load the module "DynExample"!") - error regarding type check vs. imported symbol
(e.g. "Error: Symbol nFromChar has type "<<Char -> Int>>" but expected type "Char -> Integer"")
GhcApiWrap
{-# LANGUAGE ScopedTypeVariables #-}
module GhcApiWrap
(
compileAndLoad
) where
import qualified Control.Exception.Safe as Exc
import qualified Data.Dynamic as Dyn
import qualified Data.Proxy as Px
import qualified Data.Typeable as T
import qualified GHC as Ghc
import qualified GHC.Paths as Pth
import Data.Functor ((<&>))
compileAndLoad :: forall a. T.Typeable a => String -> String -> String -> IO (Either String a)
compileAndLoad dir mdl sym = runGhcCatched >>= ethSym
where
runGhcCatched = Ghc.runGhc (Just Pth.libdir) (loadModule dir mdl sym)
`Exc.catchAny`
(\ex -> return (Left ("Exception: " ++ show ex)))
ethSym (Right dynSym) = return (ethSym' (Dyn.fromDynamic dynSym) (sErrFromDynSym dynSym))
ethSym (Left sErr) = return (Left sErr)
sErrFromDynSym dynSym = "Symbol " ++ sym ++ " has type \"" ++ show dynSym ++
"\" but expected type \"" ++ show symbolType ++ "\""
ethSym' (Just dynSym) _ = Right dynSym
ethSym' Nothing sErr = Left sErr
symbolType = T.typeRep (Px.Proxy :: Px.Proxy a)
loadModule :: Ghc.GhcMonad m => String -> String -> String -> m (Either String Dyn.Dynamic)
loadModule dir mdl sym =
Ghc.getSessionDynFlags >>=
Ghc.setSessionDynFlags >>
Ghc.guessTarget (dir ++ mdl ++ ".hs") Nothing >>=
(\target -> Ghc.setTargets [target]) >>
Ghc.load Ghc.LoadAllTargets >>=
returnFromLoadStatus
where
returnFromLoadStatus Ghc.Succeeded = dynSym <&> Right
returnFromLoadStatus Ghc.Failed = return (Left ("Could not load the module \"" ++ mdl ++ "\"!"))
dynSym = Ghc.parseImportDecl ("import " ++ mdl) >>=
(\importDecl -> Ghc.setContext [Ghc.IIDecl importDecl]) >>
Ghc.dynCompileExpr (mdl ++ "." ++ sym)
Usage
{-# LANGUAGE LambdaCase #-}
import qualified GhcApiWrap as Ghw
main :: IO ()
main = Ghw.compileAndLoad "./dyn/" "DynExample" "nFromChar" >>=
(\case
(Right nFromChar) -> putStrLn ("nFromChar 'A' := " ++ show ((nFromChar :: Char -> Integer) 'A'))
(Left sErrorMassage) -> putStrLn ("Error: " ++ sErrorMassage))
Dynamic file to read ("DynExample.hs")
NOTE: The file has to be located in diretcory dyn
side by side to the directory src
, in order to be compatible with the code above ("Usage").
module DynExample
(
nFromChar
) where
import qualified Data.Char as Chr
nFromChar :: Char -> Int
nFromChar = Chr.ord
Output
nFromChar 'A' := 65
package.yaml
...
description:...
dependencies:
- base >= 4.7 && < 5
- ghc
- ghc-paths
- safe-exceptions
ghc-options:
...
stack.yaml
...
resolver:
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml
...
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论