Haskell, GHC 8: модуль динамической загрузки/импорта

Мне нужно что-то вроде

-- Main.hs
module Main where

main :: IO ()
main = do
  <import Plugin>
  print Plugin.computation

С плагином вроде

-- Plugin.hs
module Plugin where

computation :: Int
computation = 4

Однако мне нужен плагин для компиляции рядом с основным приложением. Их необходимо развернуть вместе. Только импорт (а не компиляция) модуля должен происходить динамически.

Я нашел динамически загружать скомпилированный модуль Haskell - GHC 7.6 по пути, и он отлично работает с GHC 8.0.2, за исключением того факта, что для него требуется исходный файл плагина, находящегося в текущем рабочем каталоге при выполнении приложения.


Изменить (07.12.2017)

Можно ли загрузить модуль из строки вместо файла с помощью API GHC? http://hackage.haskell.org/package/ghc-8.2.1/docs/GHC.html#t:Target предполагает, что это возможно, но в документации много дыр, и я не могу найти способ на самом деле сделать это. Если это можно сделать, я могу использовать file-embed, чтобы включить исходный файл плагина в скомпилированный двоичный файл. Пример:

module Main where

-- Dynamic loading of modules
import GHC
import GHC.Paths ( libdir )
import DynFlags
import Unsafe.Coerce

import Data.Time.Clock (getCurrentTime)
import StringBuffer

pluginModuleNameStr :: String
pluginModuleNameStr = "MyPlugin"

pluginSourceStr :: String
pluginSourceStr = unlines
  [ "module MyPlugin where"
  , "computation :: Int"
  , "computation = 4"
  ]

pluginModuleName :: ModuleName
pluginModuleName = mkModuleName pluginModuleNameStr

pluginSource :: StringBuffer
pluginSource = stringToStringBuffer pluginSourceStr

main :: IO ()
main = do
    currentTime <- getCurrentTime
    defaultErrorHandler defaultFatalMessager defaultFlushOut $ do
      result <- runGhc (Just libdir) $ do
        dflags <- getSessionDynFlags
        setSessionDynFlags dflags
        let target = Target { targetId = TargetModule $ pluginModuleName
                            , targetAllowObjCode = True
                            , targetContents = Just ( pluginSource
                                                    , currentTime
                                                    )
                            }
        setTargets [target]
        r <- load LoadAllTargets
        case r of
          Failed    -> error "Compilation failed"
          Succeeded -> do
            setContext [IIDecl $ simpleImportDecl pluginModuleName]
            result <- compileExpr ("MyPlugin.computation")
            let result' = unsafeCoerce result :: Int
            return result'
      print result

Это, однако, приводит к

<command-line>: panic! (the 'impossible' happened)
  (GHC version 8.0.2 for x86_64-apple-darwin):
    module ‘MyPlugin’ is a package module

Изменить (08.12.2017)

Я могу скомпилировать "плагин" непосредственно в финальный двоичный файл, написав источник в временном файле, а затем загрузив его, как в связанном сообщении (Динамически загружать скомпилированный модуль Haskell - GHC 7.6). Однако это плохо работает, если плагин импортирует пакеты из Hackage:

module Main where

import Control.Monad.IO.Class (liftIO)
import DynFlags
import GHC
import GHC.Paths (libdir)
import System.Directory (getTemporaryDirectory, removePathForcibly)
import Unsafe.Coerce (unsafeCoerce)

pluginModuleNameStr :: String
pluginModuleNameStr = "MyPlugin"

pluginSourceStr :: String
pluginSourceStr = unlines
  [ "module MyPlugin where"
  , "import Data.Aeson"
  , "computation :: Int"
  , "computation = 4"
  ]

writeTempFile :: IO FilePath
writeTempFile = do
  dir <- getTemporaryDirectory
  let file = dir ++ "/" ++ pluginModuleNameStr ++ ".hs"
  writeFile file pluginSourceStr
  return file

main :: IO ()
main = do
  moduleFile <- writeTempFile
  defaultErrorHandler defaultFatalMessager defaultFlushOut $ do
    result <- runGhc (Just libdir) $ do
      dflags <- getSessionDynFlags
      setSessionDynFlags dflags
      target <- guessTarget moduleFile Nothing
      setTargets [target]
      r <- load LoadAllTargets
      liftIO $ removePathForcibly moduleFile
      case r of
        Failed -> error "Compilation failed"
        Succeeded -> do
          setContext [IIDecl $ simpleImportDecl $ mkModuleName pluginModuleNameStr]
          result <- compileExpr "MyPlugin.computation"
          let result' = unsafeCoerce result :: Int
          return result'
    print result

Есть ли способ загрузить пакеты, если, например, MyPlugin содержит инструкцию import Data.Aeson? Если я добавлю его в строку плагина, он не выполнит

/var/folders/t2/hp9y8x6s6rs7zg21hdzvhbf40000gn/T/MyPlugin.hs:2:1: error:
    Failed to load interface for ‘Data.Aeson’
    Perhaps you meant Data.Version (from base-4.9.1.0)
    Use -v to see a list of the files searched for.
haskell-loader-exe: panic! (the 'impossible' happened)
  (GHC version 8.0.2 for x86_64-apple-darwin):
  Compilation failed
CallStack (from HasCallStack):
  error, called at app/Main.hs:40:19 in main:Main

Причиной моего запроса является поддержка базы данных. Мы используем Persistent для доступа к базе данных, а динамический импорт необходим для поддержки нескольких баз данных (MySQL, PostgreSQL и SQLite), но при этом конечный пользователь может установить только одну из трех баз данных серверов (другими словами: не требуется, чтобы пользователь устанавливал все из них, если они используют, например, PostgreSQL). Модуль, специфичный для базы данных, должен загружаться только тогда, когда пользователь на самом деле настраивает основное приложение для использования этого модуля.

Если я не import Database.Persist.MySQL, то приложение не требует установки MySQL. В противном случае приложение терпит неудачу, например,

dyld: Library not loaded: 
/usr/local/opt/mysql/lib/libmysqlclient.20.dylib

на macOS.

Ответ 1

Файл с соответствующим именем модуля должен существовать по внешнему виду - не похоже, что такое содержимое файла.

В Linux я могу даже сделать это символической ссылкой на /dev/null, и все работает, но символическая ссылка на нее не делает.