代码之家  ›  专栏  ›  技术社区  ›  MathematicalOrchid

如何在榆树中记录调用图?

  •  4
  • MathematicalOrchid  · 技术社区  · 7 年前

    请帮帮我,这绝对是我的动力 精神错乱!

    如何使elm日志成为调用图?

    听起来很简单,不是吗?这个 Debug.log 函数应该使这个安静容易。但不,尽我所能,我只是不能强迫榆树以正确的顺序记录事件。我在这里疯了…


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

    factorial : Int -> Int
    factorial n = if n < 2 then 1 else n * factorial (n-1)
    

    我什么 希望 要做的就是写一个自定义 trace 这样我可以做类似的事情

    factorial n = trace ("factorial " + toString n) (if n < 2 ...)
    

    它会记录下

    factorial 3: ENTER
    factorial 2: ENTER
    factorial 1: ENTER
    factorial 1: 1
    factorial 2: 2
    factorial 3: 6
    

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


    什么不起作用:

    • 显而易见的第一次尝试是

      trace : String -> x -> x
      trace label x =
        let
          _ = Debug.log label "ENTER"
          _ = Debug.log label x
        in x
      

      但我认为这不可能奏效。因为榆树是严格的(?),请 x 在你打电话之前就被评估过了 追踪 . 所以所有的痕迹都是向后打印出来的。

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

      trace : String -> (() -> x) -> x
      trace label fx =
        let
          _ = Debug.log label "ENTER"
          x = fx ()
          _ = Debug.log label x
        in x
      

      这真的,真的看起来应该是完美的。但不知怎的,这会打印出入口和出口 在一起 然后是所有的下属调用,这显然是错误的。

    • 我特别不安的是

      let
        _ = Debug.log label "ENTER"
        x = fx ()
      in x
      

      向前打印所有输入,但表达式相同

      let
        _ = Debug.log label "ENTER"
      in fx ()
      

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

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

      trace label fx =
        case Debug.log label "ENTER" of
          _ -> case Debug.log label (fx ()) of
            x -> x
      

      不,它把所有的东西都印回原处。好吧,真奇怪。如果我只是交换两个case表达式呢?…不,它一起打印Enter+Exit,然后再打印子调用。

    • 好吧,我们来做核心肌群。拉姆达斯!

      trace label fx = Debug.log label ((\ _ -> fx ()) (Debug.log label "ENTER"))
      

      它占据了所有的出口,然后是所有的入口。我将交换表达式:

      trace label fx = (\ x -> (\ _ -> x) (Debug.log label "ENTER")) (Debug.log label (fx ()))
      

      没有骰子。再次打印每个通话组的Enter+Exit。

    • 嗯…

    说真的,那里 必须是 一个让这个工作的方法! >_< 请帮助… :'{

    2 回复  |  直到 7 年前
        1
  •  4
  •   Chad Gilbert    7 年前

    通过使用 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])
    

    然后可以将它洒在阶乘函数中,这意味着函数现在必须返回 Writer String Int 而不仅仅是 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)
    

    虽然这看起来更麻烦和侵入性(ELM语法不像haskell那么简单友好),但这每次都会给您带来可预测的结果,而不必依赖不可靠的副作用。

    跑步的结果 factorial 3 |> runWriter |> Tuple.second 是:

    [ "factorial 3: \"ENTER\""
    , "factorial 2: \"ENTER\""
    , "factorial 1: \"ENTER\""
    , "factorial 1: 1"
    , "factorial 2: 2"
    , "factorial 3: 6"
    ]
    

    请注意,此编写器没有优化(它连接列表,糟糕!)但这个想法是经过实践的

        2
  •  6
  •   Luke Woodward    7 年前

    试试这个:

    trace : String -> (() -> x) -> x
    trace label fx =
      let
        _ = Debug.log label "ENTER"  
      in
        let
          x = fx ()
          _ = Debug.log label x
        in 
          x
    

    这似乎给了你想要的输出。

    或者,因为 Debug.log 返回其第二个参数,您还可以编写以下稍微短一些的参数:

    trace : String -> (() -> x) -> x
    trace label fx =
      let
        _ = Debug.log label "ENTER"  
      in
        let
          x = fx ()
        in 
          Debug.log label x
    

    看看生成的代码,编译器似乎在重新排序声明 let 阻碍。使用嵌套 块似乎说服编译器不要对声明重新排序。

    如果声明在 块没有任何依赖项,则编译器可以自由地对它们重新排序,因为它不会更改函数返回的值。此外,如果变量在 块,编译器将按正确的顺序对它们进行排序。以下面的函数为例:

    silly : Int -> Int
    silly x =
        let
            c = b
            b = a
            a = x
        in
            c * c
    

    ELM编译器无法在 块的顺序与声明的顺序相同:它无法计算 c 先不知道是什么 b 是。查看为这个函数生成的代码,我可以看到分配按顺序排序,以便正确计算输出值。如果你把 调试日志 在此函数中间调用?

    推荐文章