如何在Elm中记录调用图?

Mat*_*hid 5 logging elm

请帮忙,这让我绝对疯了!

如何让Elm记录调用图?

听起来很简单,不是吗?该Debug.log功能应该让这个安静.但不,尽我所能,我只是不能强迫Elm以正确的顺序记录事件.我在这里失去理智......


让我们采取这样一个简单的函数:

factorial : Int -> Int
factorial n = if n < 2 then 1 else n * factorial (n-1)
Run Code Online (Sandbox Code Playgroud)

想要做的是编写自定义trace函数,以便我可以做类似的事情

factorial n = trace ("factorial " + toString n) (if n < 2 ...)
Run Code Online (Sandbox Code Playgroud)

它会记录类似的东西

factorial 3: ENTER
factorial 2: ENTER
factorial 1: ENTER
factorial 1: 1
factorial 2: 2
factorial 3: 6
Run Code Online (Sandbox Code Playgroud)

所以你可以看到它进入每个函数,你可以看到它从每个函数返回(以及它实际返回的值).


什么行不通:

  • 明显的第一次尝试是做类似的事情

    trace : String -> x -> x
    trace label x =
      let
        _ = Debug.log label "ENTER"
        _ = Debug.log label x
      in x
    
    Run Code Online (Sandbox Code Playgroud)

    但我认为这不会奏效.由于榆树是严格的(?),x在你打电话之前就进行了评估trace.所以所有的痕迹都向后打印出来.

  • 好吧,让我们输入一个函数然后:

    trace : String -> (() -> x) -> x
    trace label fx =
      let
        _ = Debug.log label "ENTER"
        x = fx ()
        _ = Debug.log label x
      in x
    
    Run Code Online (Sandbox Code Playgroud)

    真的,看起来真的应该完美.但不知何故,这个管理打印入口和出口一起,然后是所有的下属电话之后,这显然是错误的.

  • 我特别不安的是

    let
      _ = Debug.log label "ENTER"
      x = fx ()
    in x
    
    Run Code Online (Sandbox Code Playgroud)

    打印所有输入的前进,但相同的表达

    let
      _ = Debug.log label "ENTER"
    in fx ()
    
    Run Code Online (Sandbox Code Playgroud)

    向后打印所有输入.(??!)我想这就是我试图控制纯函数式编程语言中副作用的顺序...

  • 好吧,让我们把它作为一个案例块然后:

    trace label fx =
      case Debug.log label "ENTER" of
        _ -> case Debug.log label (fx ()) of
          x -> x
    
    Run Code Online (Sandbox Code Playgroud)

    不,那会向后打印一切.那太奇怪了.如果我只是交换两个案例表达式怎么办?......不,打印输入+退出,然后是子呼叫.

  • 好吧,让我们变得坚强.Lambdas FTW!

    trace label fx = Debug.log label ((\ _ -> fx ()) (Debug.log label "ENTER"))
    
    Run Code Online (Sandbox Code Playgroud)

    所有出口都跟随所有进入.我只是交换表达式:

    trace label fx = (\ x -> (\ _ -> x) (Debug.log label "ENTER")) (Debug.log label (fx ()))
    
    Run Code Online (Sandbox Code Playgroud)

    没有骰子.这会再次打印每个呼叫组的输入+退出.

  • 嗯...

说真的,必须有办法让这个工作!>_<Plz帮助......:'{

Luk*_*ard 6

试试这个:

trace : String -> (() -> x) -> x
trace label fx =
  let
    _ = Debug.log label "ENTER"  
  in
    let
      x = fx ()
      _ = Debug.log label x
    in 
      x
Run Code Online (Sandbox Code Playgroud)

这似乎可以提供您想要的输出.

或者,由于Debug.log 返回其第二个参数,您还可以编写以下内容,该内容略短:

trace : String -> (() -> x) -> x
trace label fx =
  let
    _ = Debug.log label "ENTER"  
  in
    let
      x = fx ()
    in 
      Debug.log label x
Run Code Online (Sandbox Code Playgroud)

看一下生成的代码,似乎编译器正在重新排序let块内的声明.使用嵌套let块似乎说服编译器不重新排序声明.

如果let块中的声明没有任何依赖关系,那么编译器可以自由重新排序它们,因为它不会更改函数返回的值.此外,如果变量在let块中无序声明,则编译器会将它们排序为正确的顺序.以下面的函数为例:

silly : Int -> Int
silly x =
    let
        c = b
        b = a
        a = x
    in
        c * c
Run Code Online (Sandbox Code Playgroud)

Elm编译器不能let按照它们声明的顺序生成块中的三个赋值:如果不c知道是什么,它就无法计算b.查看为此函数生成的代码,我可以看到分配按顺序排序,以便正确计算输出值.如果你把Debug.log电话放在这个功能的中间,你会发生什么?


Cha*_*ert 4

通过使用Debug.log,您试图用纯粹的语言做一些不纯粹的事情。即使你确实让它达到了工作的程度,正如 @Luke Woodward 指出的那样,我也会犹豫是否要依赖它,因为日志输出很可能在编译器版本之间切换。

相反,我们可以构建一个精简的 Writer monad,以按照日志发生的顺序保留日志的状态表示。

type Writer w a = Writer (a, List w)

runWriter : Writer w a -> (a, List w)
runWriter (Writer x) = x

pure : a -> Writer w a
pure x = Writer (x, [])

andThen : (a -> Writer w b) -> Writer w a -> Writer w b
andThen f (Writer (x, v)) =
    let (Writer (y, v_)) = f x
    in Writer (y, v ++ v_)

log : String -> a -> Writer String a
log label x = Writer (x, [label ++ ": " ++ Debug.toString x])
Run Code Online (Sandbox Code Playgroud)

然后,您可以将其撒在阶乘函数中,这意味着该函数现在必须返回 aWriter String Int而不仅仅是 an Int

factorial : Int -> Writer String Int
factorial n = 
    let logic =
            if n < 2 then
                pure 1
            else
                factorial (n-1)
                    |> andThen (\z -> pure (n * z))
    in
    log ("factorial " ++ Debug.toString n) "ENTER"
        |> andThen (\_ -> logic)
        |> andThen (\result -> log ("factorial " ++ Debug.toString n) result)
Run Code Online (Sandbox Code Playgroud)

虽然这看起来更麻烦、更具侵入性(Elm 语法不像 Haskell 那样对 monad 友好),但这每次都会给你可预测的结果,而不必依赖于不可靠的副作用。

运行结果factorial 3 |> runWriter |> Tuple.second为:

[ "factorial 3: \"ENTER\""
, "factorial 2: \"ENTER\""
, "factorial 1: \"ENTER\""
, "factorial 1: 1"
, "factorial 2: 2"
, "factorial 3: 6"
]
Run Code Online (Sandbox Code Playgroud)

请注意,这个编写器没有经过优化(它连接列表,恶心!),但这个想法是经过尝试的并且是正确的