Haskell 中的模式匹配异常计时

Pattern match exception timing in Haskell

考虑这个(相当精简的)函数来捕捉模式匹配的失败。

module Handle where
import Control.Exception
import System.IO.Unsafe

wrap :: (a -> b) -> a -> b
wrap f x =
  unsafePerformIO (catch (return $! (f x)) handler)
  where
    handler :: PatternMatchFail -> b
    handler _ = error "caught"

我已将它放在一个单独的模块中,以防止它被内联到后面的示例中。这是使用包装器的示例。

module Main where
import Handle

f [x] = x

g [x] y = x + y

h [x] = \ y -> x + y

main = do putStrLn (show (wrap h [] 4))

让我们用 GHCi 试试看。正如预期的那样,在简单示例中,wrap 或者 returns f x 的值,或者如果模式失败,它会捕获错误:

mike@spinnaker:~$ ghci example.hs
GHCi, version 8.4.4: http://www.haskell.org/ghc/  :? for help
[1 of 2] Compiling Handle           ( Handle.hs, interpreted )
[2 of 2] Compiling Main             ( example.hs, interpreted )
Ok, two modules loaded.
*Main> wrap f [3]
3
*Main> wrap f []
*** Exception: caught

但是现在考虑函数gh,它们应该是等价的。 wrap 函数不适用于 g,导致在顶层捕获失败,但它确实适用于 h.

*Main> wrap g [3] 4
7
*Main> wrap g [] 4
*** Exception: example.hs:6:1-15: Non-exhaustive patterns in function g

*Main> wrap h [] 4
*** Exception: caught

现在让我们试试 GHC 本身。结果是,在关闭优化的情况下,捕获了来自 h 的错误;但在 -O 下,优化器可能会将 h 变成 g,并且错误不再被捕获。

mike@spinnaker:~$ ghc -O --make -o example example.hs
mike@spinnaker:~$ example
example: example.hs:8:1-20: Non-exhaustive patterns in function h

我可以很好地想象优化器将嵌套函数 h 变成像 g 这样的一次有两个参数的函数,并将 g 应用于单个参数产生一个被认为是头部正常形式的对象,直到它接收到它的第二个参数,所以模式匹配失败被延迟,直到我们超出 wrap 的范围。我的问题是:这个解释大体上正确吗?如果我们想要一致的行为,我们该如何实现?

(示例是从高阶编程语言的解释器中提取的,其中部分函数作为原语安装,我们想要一种处理原语失败的统一方法,无论它们是否提供高阶结果与否:根据底层 monad 的不同而不同。)

这是一个可行的方案。在示例中,参数 [x] 表示解释器中原语的参数列表,而 y 是正在使用的 monad 的人工制品(可能 M a = Mem -> (a, Mem) )。 [我正在使用 monad,但没有将它们包装在 类 类型中。每个人都有自己的!]

首先,wrap 的一个版本会打印函数名称和有问题的参数列表。注意res参数是应该返回的结果——CBN的意思是不需要在wrap里面形成申请f args,可以从外面传入。

module Handle where
import Control.Exception
import System.IO.Unsafe

wrap :: Show a => String -> a -> b -> b
wrap f args res =
  unsafePerformIO (catch (return $! res) handler)
  where
    handler :: PatternMatchFail -> b
    handler _ = error ("caught " ++ f ++ " " ++ show args)

现在是一个例子,包含一个参数和两个参数的函数。辅助函数 wrap2 对解释器的每个版本都有一个定义,而 fgh 是一大类原语的成员。

module Main where
import Handle

f [x] = x
ff arg = wrap "f" arg (f arg)

wrap2 :: Show a => String -> (a -> b -> c) -> a -> b -> c
wrap2 name fun arg y = wrap name arg (fun arg y)

g [x] y = x + y
gg = wrap2 "g" g

h [x] = \ y -> x + y
hh = wrap2 "h" h

main = do putStrLn (show (hh [] 4))

我们的目标是能够通过对每个基元的一行定义来定义像这样的部分基元。

将 lambda 移出 case 表达式的转换,以及影响 IO monad 的相关转换,可以通过为 GHC 提供标志 -fpedantic-bottoms-fno-state-hack 来禁用。这似乎是解决这个困难的最佳解决方案,而不是试图猜测每种情况下需要什么包装函数。