请帮忙,这让我绝对疯了!
如何让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帮助......:'{
试试这个:
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电话放在这个功能的中间,你会发生什么?
通过使用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)
请注意,这个编写器没有经过优化(它连接列表,恶心!),但这个想法是经过尝试的并且是正确的
| 归档时间: |
|
| 查看次数: |
115 次 |
| 最近记录: |