【问题标题】:Easiest way to debug/visualize recursive function calls in Haskell?在 Haskell 中调试/可视化递归函数调用的最简单方法?
【发布时间】:2023-04-06 05:48:02
【问题描述】:

我正在学习 Haskell,我决定实现这个简单的算法来完成插入排序算法的一部分:

while j > 0 and A[j-1] > A[j]
    swap A[j] and A[j-1]
    j ← j - 1
end while

我是这样做的:

miniSort:: (Eq(a), Ord(a)) => Int -> [a] -> [a]
miniSort j list = if (list !! j) < (list !! (j-1)) && j >0
        then miniSort (j-1) (swapElements j (j-1) list) 
        else list

要做到这一点有点困难,但我做到了(我猜)。

在命令式编程语言中查看我可以简单执行的每个步骤

while j > 0 and A[j-1] > A[j]
    print("j is $j")
    print("swapping $A[j] with $A[j-1]")
    swap A[j] and A[j-1]
    print("swapped list: $A")
    j ← j - 1
end while
print("ended with j $j")

在 Haskell 上,通过 Writer Monad 插入日志功能要困难得多。我什至没有尝试,因为它会一团糟,然后当我想清理日志记录的东西时,它又会变得一团糟。

有没有办法查看 Haskell 中的函数调用分支?

例如:

miniSort 3 [1,2,4,3,7,8,5]

会扩展成这样的:

miniSort 3 [1,2,4,3,7,8,5] = if (3) < 4 && 3 >0
        then miniSort (2) [1,2,3,4,7,8,5]
miniSort 2 [1,2,3,4,7,8,5] = if (3) < 2 && 2 >0
        else [1,2,3,4,7,8,5]
[1,2,3,4,7,8,5]

【问题讨论】:

  • 看看trace
  • 一种解决方案(这对于初学者来说可能有点矫枉过正且难以理解,因此请持保留态度)是使您的函数单子但在单子上具有多态性,并将其写入使用辅助fix 函数的“匿名递归”样式。然后,您可以通过调整递归步骤来添加或删除“仪器”。这里有一个例子:stackoverflow.com/questions/41781460/…

标签: haskell functional-programming


【解决方案1】:

正如 Fyodor Soikin 所说,trace 将调试消息插入到任意代码中,这些代码将在您应用它的语句被评估时打印出来。

import Debug.Trace
import Text.Printf

miniSort j list
 = if trace (printf "miniSort %i %s = if %i < %i && %i>0"
                              j (show list) (list!!j) (list!!(j-1)) j)
                      $ list !! j < list !! (j-1) && j>0
    then trace (printf "then miniSort %i %s" (j-1) (show list))
           miniSort (j-1) (swapElements j (j-1) list) 
    else traceShowId list

但是,有几点需要注意:

  • 如果你发现自己想要一个像排序这样的简单算法,那你就做错了。你确实是这样的——在 Haskell 中对这样的列表进行排序是没有意义的。具体来说,索引到列表中几乎从来都不是一个好主意:它很笨拙、容易出错且速度慢。
    好的 Haskell 代码通常使用模式匹配而不是索引等。例如,

    type SortedList a = [a]
    
    insertion :: Ord a => a -> SortedList a -> SortedList a
    insertion n (x:xs)
     | n>x    = x : insertion n xs
    insertion n xs = n : xs
    
    insertSort :: Ord a => [a] -> SortedList a
    insertSort [] = []
    insertSort (x:xs) = insertion x $ insertSort xs
    

    看,任何地方都没有索引。更不用说这里可能出错了。
    (首先在 Haskell 列表上实现插入排序是否有意义当然是另一回事了!)

  • 因为(非单子)Haskell 并没有真正指定任何评估顺序,trace 经常会以意想不到的、可能混乱的顺序出现。真的,只用它来调试一个大的、已经存在的函数中间的一个细节。一般来说,彻底重构和单元测试要好得多。
    并且永远使用trace进行实际日志记录!

  • 使用 writer monad 不仅可以解决这些问题,还可以更轻松(并且更可靠)再次删除此类语句。一方面,您根本不需要真正需要这样做,因为您可以忽略日志数据,使用带有虚拟日志记录的多态单子,这将优化它而不是一开始就生成等。如果您确实删除了语句并取消编写类型,那么类型检查器将突出显示您忘记执行此操作的任何地方。

【讨论】:

  • 当严格分析器确定你的主程序从不读取时,它真的可以查看你的所有函数并删除日志吗?我希望它仍然会创建一个 thunk 来确定要记录的字符串,这可能比生成要记录的字符串更糟糕,因为它会将您的域对象保留在内存中,以便在有人要求时创建一个字符串。
  • 你说得对,措辞有点过于简单了。
【解决方案2】:

注意:这个解决方案有点矫枉过正,不适合初学者。如需实用建议,请按照 leftaroundabout 的回答。

我们可以将 minisort 定义为 monadic 函数,但它在 monad 之上是多态的。此外,我们将使用“开放递归”来定义它,这意味着函数接收自己的递归步骤作为参数,而不是直接调用自身。这会打开一个接缝,我们稍后可以在其中插入仪器:

{-# LANGUAGE ScopedTypeVariables #-}

-- some helper type synonyms
type Minisort m a = Int -> [a] -> m [a]
type Open f = f -> f

minisortAux :: (Eq a, Ord a, Monad m) => Open (Minisort m a)
minisortAux recurse j list =
    if (list !! j) < (list !! (j-1)) && j >0
        then recurse (j-1) (swapElements j (j-1) list) 
        else pure list

我们可以通过选择无操作Identity作为monad来恢复原始minisort,并使用Data.Function.fix“关闭”递归:

import Control.Monad.Identity

minisort :: (Eq a, Ord a) => Int -> [a] -> [a]
minisort j list = runIdentity $ fix minisortAux j list

但我们也可以检测函数,使其在IO 中工作并在每次迭代时打印其参数:

-- An Instrumentation transforms an open function into
-- another open function with extra behaviour.
-- Notice that instrumentations of the same type can be composed!
type Instrumentation f = Open f -> Open f

minisortIO :: forall a. (Eq a, Ord a, Show a) => Minisort IO a
minisortIO j list = fix (instrument minisortAux) j list 
  where
    instrument :: Instrumentation (Minisort IO a)
    instrument openFunction recurse j list = 
        do print $ "starting call with params " ++ show j ++ "  " ++ show list
           r <- openFunction recurse j list
           print $ "ending call with value" ++ show r
           return r

或者我们可以使用Writer来累积参数:

import Control.Monad.Writer

minisortWriter :: forall a. (Eq a, Ord a) => Minisort (Writer [(Int,[a])]) a
minisortWriter j list = fix (instrument minisortAux) j list 
  where
    instrument :: Instrumentation (Minisort (Writer [(Int,[a])]) a)
    instrument openFunction recurse j list = 
        do tell [(j,list)]
           openFunction recurse j list

除了打印或记录内容之外,我们还可以做一些事情,例如在递归过程中询问用户参数的值,甚至让用户为递归调用输入一些结果值而完全避免调用。

【讨论】:

【解决方案3】:

题外话,但你最好学习编写惯用的 Haskell 代码 而不是试图将不纯的迭代算法硬塞到 Haskell 中。

-- insert a value into a sorted list, preserving the sort
insert :: Ord a => [a] -> a -> [a]
insert [] y = [y]
insert (x:xs) y | x < y = x : insert xs y
                | otherwise = y : x : xs


-- sort a list by repeated inserting the first item of the list
-- into its proper place in the sorted remnant.
insSort :: Ord a => [a] -> [a]
insSort [] = []
insSort (x:xs) = insert (insSort xs) x

交换的整个业务就是你如何用一种更命令式的语言来实现insert;这不是你在 Haskell 中的做法。

【讨论】:

  • 确实如此。但我正在实施一个特定的算法。你认为我可以用比这更实用的方式来做吗?我认为算法的本质是没有功能的
  • 该算法是“选择一个元素,并将其插入到排序列表中”。您展示的命令式代码是实现该算法的一种方式。这不是您在 Haskell 中会做的事情(或者至少,不使用常规列表;有可变的 IORef 值可用于更准确地模拟可变值)。我认为,我在这里展示的是一种更实用的实现插入排序的方式。不同之处在于您正在构建一个新列表,而不是重用输入列表来存储输入的已排序和未排序部分。
  • 真的是命令式还是函数式?我认为这更多的是关于惰性列表与可变索引数组。如果想在 Haskell 中对可变向量进行就地排序,一些典型的命令式算法是合理的,不是吗?
  • @chepner 确实,这让我大开眼界。我很恼火我的代码看起来很有必要
  • @amalloy 当然,但我认为 OP 还没有准备好就地对可变向量进行排序。
猜你喜欢
  • 2015-05-27
  • 2012-09-10
  • 2013-11-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-29
相关资源
最近更新 更多