见证一个值在 Haskell 中被评估的频率

Witnessing how frequently a value has been evaluated in Haskell

有了一点点 unsafe,您可以看到 much 的惰性值在 Haskell

中是如何计算的
import Data.IORef
import System.IO.Unsafe

data Nat = Z | S Nat
  deriving (Eq, Show, Read, Ord)

natTester :: IORef Nat -> Nat
natTester ref =
  let inf = natTester ref
   in unsafePerformIO $ do
        modifyIORef ref S
        pure $ S inf

newNatTester :: IO (Nat, IORef Nat)
newNatTester = do
  ref <- newIORef Z
  pure (natTester ref, ref)

howMuchWasEvaled :: (Nat -> b) -> IO Nat
howMuchWasEvaled f = do
  (inf, infRef) <- newNatTester
  f inf `seq` readIORef infRef

与:

ghci> howMuchWasEvaled $ \x -> x > S (S Z)
S (S (S Z))

表示只计算了infinity :: Nat的前四个构造函数。

如果 x 被使用两次,我们仍然得到所需的总评估:

> howMuchWasEvaled $ \x -> x > Z && x > S (S Z)
S (S (S Z))

这是有道理的 - 一旦我们对 x 进行了评估,我们就不必重新开始。咚咚已经被逼了

但是有一种方法可以检查构造函数被求值了吗?即,函数 magic 的行为如下:

> magic $ \x -> x > Z 
S Z
> magic $ \x -> x > Z && x > Z
S (S Z)
...

我知道这可能涉及编译器标志(可能 no-cse)、内联编译指示、非常不安全的函数等。

编辑:Carl 指出我可能对我正在寻找的内容的限制不够清楚。要求是 function 不能更改 magic 作为参数给出(尽管可以假定它的参数是惰性的)。 magic 将成为您可以使用自己的函数调用的库的一部分。

也就是说,特定于 GHC 的 hack 和只能不可靠地工作的东西绝对仍然是游戏。

如前所述,这不能在 ghc 中完成。同名的两次使用,例如您示例中的 x,将始终与 ghc 对 haskell 的评估模型的实现共享。这是为确保共享 提供关键构建块的保证。至少,让它做你想做的事需要传入多个值,每个值对应一个你想使用命名值的独立位置。

然后您必须确保在调用方,值在传递给函数之前不会意外共享。这可以做到,但它是可能需要使用 -fno-cse-fno-full-laziness 等选项的地方,具体取决于您如何实现它以及 ghc 运行 处于什么优化级别。

这里是对你在 ghci 中有效的起点的一个小修改,至少:

{-# OPTIONS_GHC -fno-full-laziness #-}

import Data.IORef
import System.IO.Unsafe

data Nat = Z | S Nat
    deriving (Eq, Show, Read, Ord)

natTester :: IORef Nat -> Nat
natTester ref =
    let inf = natTester ref
    in unsafePerformIO $ do
        modifyIORef ref S
        pure $ S inf

newNatTester :: IO ((a -> Nat), IORef Nat)
newNatTester = do
    ref <- newIORef Z
    pure (\x -> x `seq` natTester ref, ref)

howMuchWasEvaled :: ((a -> Nat) -> b) -> IO Nat
howMuchWasEvaled f = do
    (infGen, infRef) <- newNatTester
    f infGen `seq` readIORef infRef

在 ghci 中使用:

*Main> howMuchWasEvaled $ \gen -> let x = gen 1 ; y = gen 2 in x > Z && y > Z
S (S Z)
*Main> howMuchWasEvaled $ \gen -> let x = gen 1 in x > Z && x > Z
S Z

我用一个无穷大生成器代替了将单个无穷大传递给函数。生成器不关心它调用的参数是什么,只要它不是底值即可。 (seq 是为了确保函数实际使用它的参数,以防止 ghc 在参数未使用时可能进行的一些优化。)只要每次调用时都使用不同的值,ghc 就赢了' 能够把它 cse 掉,因为表达方式不同。如果与优化一起使用,完全惰性可能会干扰 natTester refnewNatTester 中的 lambda 中浮动。为防止这种情况,我添加了一个 pragma 以关闭此模块中的优化。默认情况下在 ghci 中无关紧要,因为它不使用优化。这个模块是否被编译可能很重要,所以我加入了 pragma 只是为了确定。