18

有谁知道适合用于编写 shell 脚本的快速启动的 Haskell 解释器?在我的旧笔记本电脑上使用 Hugs 运行“hello world”需要 400 毫秒,而在我当前的 Thinkpad X300 上需要 300 毫秒。这对于瞬时响应来说太慢了。使用 GHCi 的时间类似。

函数式语言不必很慢:Objective Caml 和 Moscow ML 都在 1 毫秒或更短的时间内运行 hello world。

澄清:我是 GHC 的重度用户,我知道如何使用 GHCi。我知道所有关于编译以快速完成任务的知识。解析成本应该完全不相关:如果 ML 和 OCaml 的启动速度可以比 GHCi 快 300 倍,那么还有改进的空间。

我在寻找

  • 脚本编写的便利性:一个源文件,没有二进制代码,相同的代码在所有平台上运行
  • 与其他解释器相当的性能,包括快速启动和执行简单程序,如

    module Main where
    main = print 33
    

不是在为更严肃的程序寻找编译性能。重点是看看 Haskell 是否对脚本有用。

4

5 回答 5

31

为什么不创建一个脚本前端来编译脚本,如果它以前没有或者编译的版本已经过时了。

这是基本的想法,这段代码可以改进很多——搜索路径而不是假设所有内容都在同一个目录中,更好地处理其他文件扩展名,等等。而且我在 haskell 编码(ghc-compiled-script。 hs):

import Control.Monad
import System
import System.Directory
import System.IO
import System.Posix.Files
import System.Posix.Process
import System.Process

getMTime f = getFileStatus f >>= return . modificationTime

main = do
  scr : args <- getArgs
  let cscr = takeWhile (/= '.') scr

  scrExists <- doesFileExist scr
  cscrExists <- doesFileExist cscr
  compile <- if scrExists && cscrExists
               then do
                 scrMTime <- getMTime scr
                 cscrMTime <- getMTime cscr
                 return $ cscrMTime <= scrMTime
               else
                   return True

  when compile $ do
         r <- system $ "ghc --make " ++ scr
         case r of
           ExitFailure i -> do
                   hPutStrLn stderr $
                            "'ghc --make " ++ scr ++ "' failed: " ++ show i
                   exitFailure
           ExitSuccess -> return ()

  executeFile cscr False args Nothing

现在我们可以创建这样的脚本(hs-echo.hs):

#! ghc-compiled-script

import Data.List
import System
import System.Environment

main = do
  args <- getArgs
  putStrLn $ foldl (++) "" $ intersperse " " args

现在运行它:

$ time hs-echo.hs "Hello, world\!"     
[1 of 1] Compiling Main             ( hs-echo.hs, hs-echo.o )
Linking hs-echo ...
Hello, world!
hs-echo.hs "Hello, world!"  0.83s user 0.21s system 97% cpu 1.062 total

$ time hs-echo.hs "Hello, world, again\!"
Hello, world, again!
hs-echo.hs "Hello, world, again!"  0.01s user 0.00s system 60% cpu 0.022 total
于 2010-07-24T18:00:35.030 回答
8

usingghc -e几乎等同于调用ghci. 我相信 GHCrunhaskell在运行之前会将代码编译为临时可执行文件,而不是像ghc -e/那样解释它ghci,但我不是 100% 确定。

$ time echo 'Hello, world!'
Hello, world!

real    0m0.021s
user    0m0.000s
sys     0m0.000s
$ time ghc -e 'putStrLn "Hello, world!"'
Hello, world!

real    0m0.401s
user    0m0.031s
sys     0m0.015s
$ echo 'main = putStrLn "Hello, world!"' > hw.hs
$ time runhaskell hw.hs
Hello, world!

real    0m0.335s
user    0m0.015s
sys     0m0.015s
$ time ghc --make hw
[1 of 1] Compiling Main             ( hw.hs, hw.o )
Linking hw ...

real    0m0.855s
user    0m0.015s
sys     0m0.015s
$ time ./hw
Hello, world!

real    0m0.037s
user    0m0.015s
sys     0m0.000s

在运行它们之前简单地编译所有“脚本”有多难?

编辑

啊,为多种架构提供二进制文件确实很痛苦。我以前也走过那条路,但不是很有趣……

可悲的是,我认为不可能使任何 Haskell 编译器的启动开销更好。该语言的声明性意味着有必要在尝试对任何内容进行类型检查之前先阅读整个程序,更不用说执行了,然后您要么遭受严格分析的代价,要么遭受不必要的懒惰和重击。

流行的“脚本”语言(shell、Perl、Python 等)和基于 ML 的语言只需要一次通过......好吧,ML 需要一个静态类型检查通过,而 Perl 有这个有趣的 5-pass 方案(带有其中两个反向运行);无论哪种方式,程序化意味着编译器/解释器可以更轻松地将程序的各个部分组装在一起。

简而言之,我认为不可能比这更好。我还没有测试过 Hugs 或 GHCi 是否具有更快的启动速度,但与非 Haskell 语言之间的差异仍然很大。

于 2009-04-03T17:32:59.327 回答
5

如果您真的关心速度,您将受到每次启动时重新解析代码的阻碍。Haskell 不需要从解释器中运行,使用GHC编译它,你应该会获得出色的性能。

于 2009-04-03T06:49:14.680 回答
4

这个问题有两个部分:

  • 你关心性能
  • 你想要脚本

如果您关心性能,唯一严肃的选择是 GHC,它非常非常快:http ://shootout.alioth.debian.org/u64q/benchmark.php?test=all&lang=all

如果你想为 Unix 脚本编写一些简单的东西,我会使用 GHCi。它比 Hugs 快大约 30 倍,而且还支持所有关于 hackage 的库。

所以现在安装 GHC(并免费获得 GHCi)。

于 2009-04-03T16:24:11.410 回答
3

拥有一个 ghci 守护进程和一个获取脚本路径和位置的馈送脚本怎么样?与已经运行的 ghci 进程通信以在正确的目录中加载和执行脚本并将输出通过管道传回馈送脚本以用于标准输出?

不幸的是,我不知道如何写这样的东西,但从 :l 在 ghci 中的速度来看,它似乎真的很快。看起来 runhaskell 的大部分成本都在启动 ghci,而不是解析和运行脚本。

编辑:经过一番尝试,我发现Hint 包(GHC API 的包装器)在这里非常有用。下面的代码将加载传入的模块名称(这里假设在同一目录中)并执行 main 函数。现在剩下的就是让它成为一个守护进程,并让它接受管道或套接字上的脚本。

import Language.Haskell.Interpreter
import Control.Monad

run = runInterpreter . test

test :: String -> Interpreter ()
test mname = 
  do
    loadModules [mname ++ ".hs"]
    setTopLevelModules [mname]
    res <- interpret "main" (as :: IO())
    liftIO res

编辑2:就stdout/err/in而言,使用这个特定的GHC技巧看起来可以将客户端程序的std重定向到feeder程序,然后到守护程序连接的一些命名管道(也许)一直到,然后将守护程序的标准输出返回到馈线程序正在侦听的另一个命名管道。伪示例:

grep ... | feeder my_script.hs | xargs ...
            |   ^---------------- <
            V                      |
         named pipe -> daemon -> named pipe

在这里,馈线将是一个小型编译的线束程序,只需将标准重定向到守护程序然后返回守护程序,并将脚本的名称和位置提供给守护程序。

于 2010-11-20T07:02:38.453 回答