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 运行时系统(用于无限列表、整数运算等)。
问题:
- 我认为根本不可能只执行单子操作并将其保存为一个新文件。这是真的吗?
- 如何才能找到类似的解决方案? 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
值的方法有很多种:
- 内置函数
putStrLn
- Monad 操作,如
return
或 >>=
获得 IO 值后,可以通过三种方式对其进行分解:
- 设置
main
等于值
unsafePerformIO
- 作为导出的 C 函数的 return 值
所有这些分解为将 IO a
转换为 a
。没有其他方法可以检查它以了解它的作用。
类似地,您可以对函数做的唯一事情就是将它们放入变量中或调用它们(或将它们转换为 C 函数指针)。
没有其他检查函数的明智方法。
你可以做的一件事不是编译而是 linking 是让你的解释器主函数 运行 在一些外部 c 字符串上,将其构建到一个静态对象中,然后你的“编译器”可以用程序的这个 C 字符串创建一个新对象,link 你已经拥有的。
有一种部分求值理论说,如果你对应用于某些输入的解释器的部分求值器进行部分求值,那么你得到的是一个编译器,但 ghc 不是一个足够高级的部分求值器。
考虑一个简单的 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 运行时系统(用于无限列表、整数运算等)。
问题:
- 我认为根本不可能只执行单子操作并将其保存为一个新文件。这是真的吗?
- 如何才能找到类似的解决方案? 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
值的方法有很多种:
- 内置函数
putStrLn
- Monad 操作,如
return
或>>=
获得 IO 值后,可以通过三种方式对其进行分解:
- 设置
main
等于值 unsafePerformIO
- 作为导出的 C 函数的 return 值
所有这些分解为将 IO a
转换为 a
。没有其他方法可以检查它以了解它的作用。
类似地,您可以对函数做的唯一事情就是将它们放入变量中或调用它们(或将它们转换为 C 函数指针)。
没有其他检查函数的明智方法。
你可以做的一件事不是编译而是 linking 是让你的解释器主函数 运行 在一些外部 c 字符串上,将其构建到一个静态对象中,然后你的“编译器”可以用程序的这个 C 字符串创建一个新对象,link 你已经拥有的。
有一种部分求值理论说,如果你对应用于某些输入的解释器的部分求值器进行部分求值,那么你得到的是一个编译器,但 ghc 不是一个足够高级的部分求值器。