You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TemplateHaskell:addDependentFile文件不存在时失效,需支持重编译与Fallback

解决方案

方案1:利用GHC对不存在文件的依赖支持(推荐)

从GHC 8.6版本开始,addDependentFile允许注册不存在的文件作为编译依赖。即使目标文件当前不存在,GHC也不会报错,只会在该文件后续被创建或修改时自动触发重编译。

修改后的代码示例:

import Control.Monad.IO.Class (MonadIO (liftIO))
import Language.Haskell.TH.Syntax (Lift (liftTyped), addDependentFile)
import Language.Haskell.TH.Syntax.Compat (SpliceQ, liftSplice)
import System.Directory (doesFileExist)

myFn :: SpliceQ String
myFn = do
  let filePath = "your-target-file.txt"
      fallbackPath = "your-fallback-file.txt"
  -- 直接注册目标文件为依赖,无论其是否存在
  liftSplice (addDependentFile filePath >>= liftTyped)
  exists <- liftIO (doesFileExist filePath)
  content <- liftIO $ if exists
    then readFile filePath
    else readFile fallbackPath
  liftTyped content

方案2:兼容旧版GHC的替代方案

如果项目需要兼容GHC 8.6之前的版本,可以通过依赖目标文件所在的目录来间接监听文件创建:

import Control.Monad.IO.Class (MonadIO (liftIO))
import Language.Haskell.TH.Syntax (Lift (liftTyped), addDependentFile)
import Language.Haskell.TH.Syntax.Compat (SpliceQ, liftSplice)
import System.Directory (doesFileExist)
import System.FilePath (takeDirectory)

myFn :: SpliceQ String
myFn = do
  let filePath = "your-target-file.txt"
      fallbackPath = "your-fallback-file.txt"
      dirPath = takeDirectory filePath
  -- 依赖目标文件所在的目录,目录内任何变化(包括新文件创建)都会触发重编译
  liftSplice (addDependentFile dirPath >>= liftTyped)
  exists <- liftIO (doesFileExist filePath)
  content <- liftIO $ if exists
    then readFile filePath
    else readFile fallbackPath
  liftTyped content

这种方式的 trade-off 是:目录内任何文件的修改(包括无关文件)都会触发重编译,但能保证目标文件被创建时会重新执行TH代码。

备选:fallback时强制每次重编译

如果上述方案都不适用,想要在使用fallback文件时强制每次编译都重新检查目标文件是否存在,可以通过动态更新一个标记文件实现:

import Control.Monad.IO.Class (MonadIO (liftIO))
import Language.Haskell.TH.Syntax (Lift (liftTyped), addDependentFile)
import Language.Haskell.TH.Syntax.Compat (SpliceQ, liftSplice)
import System.Directory (doesFileExist)
import System.IO (writeFile)
import Data.Time.Clock (getCurrentTime, formatTime)
import Data.Time.Format (defaultTimeLocale)

myFn :: SpliceQ String
myFn = do
  let filePath = "your-target-file.txt"
      fallbackPath = "your-fallback-file.txt"
      markerFile = ".th-recompile-marker"
  exists <- liftIO (doesFileExist filePath)
  if exists
    then do
      liftSplice (addDependentFile filePath >>= liftTyped)
      content <- liftIO $ readFile filePath
      liftTyped content
    else do
      -- 每次fallback时更新标记文件内容,强制下次编译重跑TH
      liftIO $ do
        currentTime <- formatTime defaultTimeLocale "%s" <$> getCurrentTime
        writeFile markerFile currentTime
      liftSplice (addDependentFile markerFile >>= liftTyped)
      content <- liftIO $ readFile fallbackPath
      liftTyped content

原理是:当使用fallback时,每次都会更新标记文件的内容,GHC依赖该文件后,下次编译会检测到文件变化,从而重新执行TH代码,再次检查目标文件是否存在。


内容的提问来源于stack exchange,提问作者xbalaj

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 11:53:11