Haskell: 函数可以编译吗?

Haskell: Can a function be compiled?

考虑一个简单的 Haskell Brainf*ck 解释器。看看 interpret 函数就知道了。

import Prelude hiding (Either(..))
import Control.Monad
import Data.Char (ord, chr)

-- function in question
interpret :: String -> IO ()
interpret strprog = let (prog, []) = parse strprog
                    in execBF prog


interpretFile :: FilePath -> IO ()
interpretFile fp = readFile fp >>= interpret


type BF = [BFInstr]
data BFInstr = Left | Right | Inc | Dec | Input | Output | Loop BF

type Tape = ([Integer], [Integer])

emptyTape = (repeat 0, repeat 0)

execBFTape :: Tape -> BF -> IO Tape
execBFTape = foldM doBF

execBF :: BF -> IO ()
execBF prog = do
  execBFTape emptyTape prog
  return ()

doBF :: Tape -> BFInstr -> IO Tape
doBF ((x:lefts),   rights)   Left     = return (lefts,         x:rights)
doBF (lefts,    (x:rights))  Right    = return (x:lefts,         rights)
doBF (left,     (x:rights))  Inc      = return (left,      (x+1):rights)
doBF (left,     (x:rights))  Dec      = return (left,      (x-1):rights)
doBF (left,     (_:rights))  Input    = getChar >>= \c -> return (left, fromIntegral (ord c):rights)
doBF t@(_,      (x:     _))  Output   = putChar (chr (fromIntegral x)) >> return t
doBF t@(left,   (x:     _)) (Loop bf) = if x == 0
                                        then return t
                                        else do t' <- execBFTape t bf
                                                doBF t' (Loop bf)

simpleCommands = [('<', Left),
                  ('>', Right),
                  (',', Input),
                  ('.', Output),
                  ('+', Inc),
                  ('-', Dec)]

parse :: String -> (BF, String)
parse []          = ([], [])
parse (char:prog) = case lookup char simpleCommands of
                      Just command -> let (rest, prog') = parse prog
                                      in (command : rest, prog')
                      Nothing      ->
                        case char of
                          ']' -> ([], prog)
                          '[' -> let (loop, prog')  = parse prog
                                     (rest, prog'') = parse prog'
                                 in (Loop loop:rest, prog'')
                          _   -> parse prog

所以我应用了一个函数,如 interpret "[->+<]"。这给了我一个执行给定程序的 IO () monadic 动作。它的正确类型是某个程序的 main

假设我想将此操作编译为可执行文件,也就是说,我想生成一个可执行文件,其中 interpret ... 的结果作为主要功能。当然,这个可执行文件必须包含 GHC 运行时系统(用于无限列表、整数运算等)。

问题:

  1. 我认为根本不可能只执行单子操作并将其保存为一个新文件。这是真的吗?
  2. 如何才能找到类似的解决方案? GHC Api 和 hint 有帮助吗?

编辑

抱歉,我把原问题简单化了。当然,我可以这样写一个文件:

main = interpret "..."

但这不是我们在尝试编译某些东西时通常做的事情,所以请考虑 interpretFile :: FilePath -> IO ()。让BF程序保存在文件中(helloworld.bf).

我将如何创建一个可执行文件来执行 helloworld.bf 的内容而不实际需要该文件?

$ ./MyBfCompiler helloworld.bf -o helloworld

我不确定你是在问你是如何编写一个可以将文件作为输入的编译器 helloworld.bf,还是你如何编译一个运行 Haskell 的程序 helloworld.bf.

在前一种情况下,你会想要比这更充实的东西:

import System.Environment (getArgs)

main :: IO ()
main = do
  (_:fileName:_) <- getArgs
  source <- readFile fileName
  interpret source

interpret :: String -> IO ()
interpret = undefined -- You can fill in this piddly little detail yourself.

如果您想要后者,有几种不同的选择。首先,您可以将 *.bf 文件的内容存储在一个字符串常量中(或者更好的是 Text 或严格的 ByteString),并将其传递给您的解释器函数。如果 GHC 足够乐观以在编译时完全内联和扩展该调用,我会感到惊讶,但原则上 Haskell 编译器可以。

第二个是将 Brainfuck 变成一种具有您定义的运算符的特定领域语言,这样您实际上就可以编写类似

的东西
interpret [^<,^+,^>,^.]

如果您定义 (^<) 和其他运算符,Brainfuck 命令将编译为表示 Brainfuck 程序的字节码。

在这种情况下,与第一种方法相比没有明显的好处,但是使用更结构化的语言,您可以进行优化,将源代码编译为更适合解释器执行的基于堆栈的字节码,或者生成更复杂的 AST。

您也可以将此想法表达为

interpret
  (^< ^+ ^> ^.)
  input

在这里,如果 Brainfuck 命令是具有从右到左优先级的高阶函数,并且 interpret bf input = (bf begin) input,Brainfuck 代码将简单地编译为解释器调用的函数。这最有可能变成快速的本机代码。

上一个答案

在某些情况下,编译器可以内联函数调用(GHC 中有 pragma 告诉它这样做)。如果你给闭包命名,编译器也更有可能做你想做的事,比如:

main = interpret foo

在 GHC 中,您可以通过添加

给编译器一个提示
{-# INLINE main #-}

甚至

{-# INLINE interpret #-}

您可以通过 -S 编译模块并查看源代码来检查 GHC 生成的代码。

答案基本是否定的

构造IO值的方法有很多种:

  1. 内置函数 putStrLn
  2. Monad 操作,如 return>>=

获得 IO 值后,可以通过三种方式对其进行分解:

  1. 设置main等于值
  2. unsafePerformIO
  3. 作为导出的 C 函数的 return 值

所有这些分解为将 IO a 转换为 a。没有其他方法可以检查它以了解它的作用。

类似地,您可以对函数做的唯一事情就是将它们放入变量中或调用它们(或将它们转换为 C 函数指针)。

没有其他检查函数的明智方法。

你可以做的一件事不是编译而是 linking 是让你的解释器主函数 运行 在一些外部 c 字符串上,将其构建到一个静态对象中,然后你的“编译器”可以用程序的这个 C 字符串创建一个新对象,link 你已经拥有的。


有一种部分求值理论说,如果你对应用于某些输入的解释器的部分求值器进行部分求值,那么你得到的是一个编译器,但 ghc 不是一个足够高级的部分求值器。