【问题标题】:Critique this late night, noob Haskell code批评这个深夜,菜鸟 Haskell 代码
【发布时间】:2009-04-25 06:31:03
【问题描述】:

我正在研究Real World Haskell,目前正在做第 3 章末尾的练习。

我正在采用一种不同寻常的方法:尽管我知道他们尚未涵盖的一些语言功能会对我有所帮助,但我正在尝试仅使用 来完成这些练习他们已明确涵盖的内容。 为什么?真的只是为了好玩。感觉就像是在强迫我给我的大脑一些额外的递归练习。

所以我刚刚完成了如下所示的练习:“创建一个函数,根据每个子列表的长度对列表列表进行排序。(您可能需要查看 Data.List 模块中的 sortBy 函数.)"

现在,他们提出了有关 Data.List 模块的提示。但是他们没有说在哪里可以找到参考文档,关于如何导入东西等等。所以我决定滚动我自己的排序,看看我能不能做到。我使用了冒泡排序,因为它是最简单的算法。

结果如下。我想让您的 Haskell 专家对其进行批评...但是 请记住以下警告: 如果您提出改进建议,请根据第 3 章中涵盖的语言特性进行改进真实世界的 Haskell(或者您猜想这些功能可能是什么,而无需费力查找)。我知道有很多很棒的语言特性等着我,它们可以让我把这段代码做得更好,但现在的具体挑战是利用迄今为止所涵盖的“原始”特性来做到这一点。 em>

我确信在某些情况下我会伸手去抓我的肘部,在递归和模式匹配可以为我做更多事情时使用显式控制流的情况等等。我确信代码也可以变得更短、更易读。我敢打赌,有一些我不知道的好习惯可以与我限制自己使用的原始语言功能一起使用。这些是我希望收到的提示。

这可能是任何语言中我引以为豪的最丑陋的代码(至少,我记得)。我的第一次尝试是用一种函数式语言,而不是“Hello, world”类型的东西。现在你要打败它了:)。请温柔一点,但我期待着一些深刻的见解。谢谢。

areListsEqual :: (Eq a) => [a] -> [a] -> Bool

areListsEqual [] [] = True
areListsEqual [] _  = False
areListsEqual _ []  = False

areListsEqual xs ys = (head xs == head ys)  && (areListsEqual (tail xs) (tail ys))

charlieSort :: (Eq a) => [[a]] -> [[a]]

charlieSort [] = []
charlieSort (x:xs) | null xs = [x]
charlieSort xs | (length xs) >= 2 = if(not (areListsEqual xs wip))
                    then charlieSort wip 
                    else wip
                    where
                      first = head xs
                      second = head (tail xs)
                      theRest = drop 2 xs
                      swapPairIfNeeded a b = if(length a >= length b) 
                          then [second, first]
                          else [first, second]
                      modifiedPair = swapPairIfNeeded first second
                      wip = (take 1 modifiedPair) ++ charlieSort ( (drop 1 modifiedPair) ++ theRest)

【问题讨论】:

  • 有人说这不是问题。我会尽量尊重并持开放态度对待这种观点。但我不得不问,根据什么定义它不是一个问题?您是否有一些我不知道的标准?我正在寻求改进一些代码的建议。这将帮助我在学习语言方面取得进步。是否有某些原因不是有效的 StackOverflow 问题?
  • 我认为问题中唯一无效的部分是要求人们根据随机书籍的随机章节限制他们的改进。否则我认为这没什么问题。
  • 谢谢,很高兴您同意它是有效的。就限制而言,我只是意味着人们进入 TypeClasses 和列表推导或其他任何东西都是没有意义的。我将阅读本书的其余部分并最终深入了解所有内容,因此我试图将讨论限制在基础知识上。当然,没有人需要查看这本书(我在问题中说过)......只限于基本的语言功能。
  • @apphacker,既然您继续编辑并应用 not-a-question 标签,那么解释一下您的来源如何?这对我来说似乎是一个合理的问题,你不觉得吗?
  • 关于“文档在哪里”:最简单的查找方法是使用Hoogle,例如haskell.org/hoogle/?q=sortBy

标签: sorting functional-programming haskell


【解决方案1】:

我首先会开始使用模式匹配。

areListsEqual :: Eq a => [a] -> [a] -> Bool
areListsEqual [    ] [    ] = True
areListsEqual [    ] _      = False
areListsEqual _      [    ] = False
areListsEqual (x:xs) (y:ys) = x == y && areListsEqual xs ys

请注意,当避免使用 headtail 时,它的可读性更高。

charlieSort :: Eq a => [[a]] -> [[a]]
charlieSort    [                    ] = []
charlieSort    [x                   ] = [x]
charlieSort xs@(first:second:theRest)
  | areListsEqual xs wip              = wip
  | otherwise                         = charlieSort wip
  where
  swapPairIfNeeded a b
    | length a >= length b = [second,first]
    | otherwise            = [first,second]
  modifiedPair = swapPairIfNeeded first second
  wip = take 1 modifiedPair ++ charlieSort (drop 1 modifiedPair ++ theRest)

我将if-then-else 更改为警卫,以略微提高可读性 (YMMV)。而不是检查列表是否至少有两个元素 调用length 我们使用模式匹配,这也允许我们命名 直接first,second,theRestname @ pattern 模式 将输入与pattern 匹配并且将整个输入命名为name

现在,我想避免使用takedrop 来提取这两个元素 modifiedPair,所以最后两行改为

  [shorter,longer] = swapPairIfNeeded first second
  wip = [shorter] ++ charlieSort ([longer] ++ theRest)

最后一行可以写成

  wip = shorter : charlieSort (longer : theRest)

如果您愿意。但是为什么swapPairIfNeeded 应该返回shorterlistfirstsecond 列表中的longer ?为什么不使用 对像

  swapPairIfNeeded a b
    | length a >= length b = (second,first)
    | otherwise            = (first,second)
  (shorter,longer) = swapPairIfNeeded first second

?在大多数情况下,最好将元组用于固定数量的 值(可能是不同的类型)并使用列表来表示可变数量的 值(必须是相同类型)。但这似乎很奇怪 swapPairIfNeeded 比较它的参数 ab,然后返回 firstsecond 无论如何。在这种情况下,不要让它返回ab 成对,我将完全删除swapPairIfNeeded

  (shorter,longer)
    | length first >= length second = (second,first)
    | otherwise                     = (first,second)

swapPairIfNeeded的主体“展开”到定义中 (shorter,longer).

所以现在charlieSort 的代码看起来像

charlieSort :: Eq a => [[a]] -> [[a]]
charlieSort    [                    ] = []
charlieSort    [x                   ] = [x]
charlieSort xs@(first:second:theRest)
  | areListsEqual xs wip              = wip
  | otherwise                         = charlieSort wip
  where
  (shorter,longer)
    | length first >= length second = (second,first)
    | otherwise                     = (first,second)
  wip = shorter : charlieSort (longer : theRest)

最后,我应该说charlieSort 并没有真正实现 冒泡排序,因为对charlieSort 的递归调用不仅 使列表中的一个“冒泡”传递,但也对列表进行完全排序 longer : theRest,因此在此递归调用之后必须完成的所有操作 (在返回一个“升级”之前)可能会将shorter 渗透到它的 合法的地方。

【讨论】:

  • 优秀的分数,谢谢。是的,我在 swapPairIfNeeded 中有一些“糟糕的耦合”:它没有理由“知道”a 和 b,以及 first 和 second。此外,它不需要返回列表。这两个都是早期尝试构建“wip”的“遗留物”,我认为我可以将“修改后的对”粘贴在 wip 的前面。你能帮我看看为什么“charlieSort [x] = [x]”模式不会匹配每个列表,无论它包含多少元素?
【解决方案2】:

Charlie:我将仅限于一个批评:如果可以使用模式匹配,请不要使用 headtaillength

areListsEqual [] [] = True
areListsEqual (x:xs) (y:ys) = x == y && areListsEqual xs ys
areListsEqual _ _   = False

我不能按照你的排序算法(如果你重新格式化你的问题以消除水平滚动条会很礼貌),但我会像这样重写前三行:

charlieSort []  = []
charlieSort [x] = x
charlieSort (x1:x2:xs) = if ...

(P.S.headtail的所有用法都可以用模式匹配重写,初学者应该这样做。不是length的所有用法都可以用模式匹配代替,但是常见的noob代码比如length xs == 0length xs >= 2 可以而且应该重写。)

(附注:即使是经验丰富的 Haskell 程序员也很少使用“head”。在 Glasgow Haskell 编译器中,只有不到十分之二的源代码行提到了“head”,并且观察这些提及,大约一半是字符串文字或 cmets。这大约是每 1500 行代码使用一次“head”。)

【讨论】:

  • 太好了,感谢您的建议。我一定会尝试用模式匹配替换头部/长度。问题:您的第二个 charlieSort 模式是“charlieSort [x] = x”。我认为这将是一个无可辩驳的模式。不是因为什么原因吗?
  • @Charlie:无可辩驳的模式是始终匹配的模式。模式[x] 只匹配单元素列表,因此它可以被空列表或两个或多个元素的列表反驳。
【解决方案3】:

您不需要areListsEqual 函数。您可以将列表与(==) 函数进行比较。我会使用快速排序而不是冒泡排序。这是一个我认为只使用你目前应该学到的东西的解决方案。

charlieSort :: (Eq a) => [[a]] -> [[a]]
charlieSort []     = []
charlieSort (x:xs) = charlieSort (filter (cmpLen (>) x) xs) ++ [x] ++
                     charlieSort (filter (cmpLen (<=) x) xs)
   where filter _ [] = []
         filter p (x:xs) = (if (p x) then (x:) else id) (filter p xs)
         cmpLen f x y = f (length x) (length y)

【讨论】:

  • 谢谢。收到此错误,我还没有充分了解您的解决方案,无法弄清楚如何修复:无法匹配预期类型t1 -&gt; t2 -&gt; t' against inferred type [a]' 在表达式中:(x :) 在表达式中:(如果 p x then (x :) else id) filter p xs 在 `filter' 的定义中: filter p (x : xs) = (if p x then (x :) else id) filter p xs
  • 已修复,抱歉。这是一项快速的工作。 id 函数是前奏的一部分。定义如下:id x = x
  • 非常有趣。我看到它有效。我大概理解了一半。几个问题:1)看起来 cmpLen 需要三个参数,但你只用两个来调用它。我读对了吗,如果是这样,你为什么要这样做?嘿!没关系...我只是想通了!这是咖喱。很酷。他们还没有报道,但我以前见过。您正在通过柯里化将 charlieSort 中的 x 与过滤器中的 x 进行比较,对吗? 2) 我不明白在这种情况下可能会返回什么 id,以及它如何影响下一次过滤器调用。
  • 是的,你也可以把 (cmpLen (>) x) 写成 \y -> compLen (>) x y。甚至 (\x y -> 长度 x > 长度 y)。 if 子句返回一个函数。将 x 附加到列表的函数或 id 函数。你也可以这样写: if (p x) then x : filter p xs else filter p xs.
  • 那么,在if子句返回id函数的情况下,到底是不是用到了id函数的“结果”呢?或者它只是作为“占位符”存在,因为 if 函数必须评估某些东西? “id”计算的值对最终结果有影响吗?
【解决方案4】:

我在第 8 章,所以我不是老手,但我更喜欢

areListsEqual x:xs y:ys = (x == y) && (areListsEqual xs ys)
areListsEqual [] [] = True
areListsEqual _ _ = False

似乎更符合 Haskell 风格。

同样,

charlieSort [] = []
charlieSort (x:[]) = [x]
charlieSort (x1:x2:xs) = blah blah

swapPairIfNeed 按原样工作,因为您只使用 first 和 second 作为其参数(按此顺序)调用它,但您可能的意思是

swapPairIfNeed a b = if (length a >= length b)
    then [b, a]
    else [a, b]

其实我更喜欢charlieSort的第三种情况

charlieSort (x1:x2:xs) = if not (areListsEqual x1:x2:xs wip)
                         then charlieSort wip
                         else wip
    where swapPairIfNeeded a b = if (length a >= length b)
                                 then (b, a)
                                 else (a, b)
          wip = f (swapPairIfNeeded first second)
          f (a, b) = a : (charlieSort b:xs)

我认为这一切都在第 3 章中介绍了。

现在,让我们检查一下算法。即使坚持冒泡排序,也无需在排序后检查整个列表。相反,如果需要,我们可以交换前两个元素,然后对列表的尾部进行排序。如果头部比排序的尾部的头部短,我们就完成了。

charlieSort (x1:x2:xs) = if (length a <= length (head sortedTail))
                         then a : sortedTail
                         else charlieSort (a : sortedTail)
    where sortedTail = charlieSort (b:xs)
          (a, b) = if (length x1 >= length x2)
                   then (x2, x1)
                   else (x1, x2)

【讨论】:

  • 谢谢。我真的很喜欢“charlieSort (x:[]) = [x]”,比我正在做的要干净得多。另外,有趣的是,您正在做一种“深度优先”的方法:我认为您在对头部进行排序之前先对尾部进行排序。这是有道理的,是我从未想过的。帮助我,因为现在我更清楚地意识到整个问题可以简化为:弄清楚如何对前 2 个条目进行排序;然后,一遍又一遍地使用它,直到对整个列表进行排序。酷。
  • 有趣——我刚刚重新阅读了上面的评论(我写的,一年多前)。当时,我不知道,但在 Haskell 中总是发生这样的事情,一个好的答案是“弄清楚如何对列表中的前 2 个项目执行 foo,然后弄清楚如何使用它,然后直到整个列表都是 'foo-d'"。
【解决方案5】:

您声称冒泡排序是最简单的排序算法,但这里的情况并非如此。冒泡排序非常适合数组,您可以在其中线性索引它们。对于 Haskell 的链表来说,插入排序其实更漂亮。

让我们从insert函数开始:

winsert :: [a] -> [[a]] -> [[a]]
winsert x [] = [x]
winsert x (y:ys)
    | length x < length y = x : y : ys
    | otherwise = y : winsert x ys
  • 如果列表为空,则将x放入其中
  • 如果列表不为空:
    • 如果x &lt; y,那么x属于列表的最前面
    • 否则,列表的头部是y,尾部是由x 插入到ys 的某个位置组成的。

接下来,我们有实际的排序功能:

wsort :: [[a]] -> [[a]]
wsort [] = []
wsort [x] = [x]
wsort (x:xs) = winsert x (wsort xs)
  • 如果列表为空,则返回它
  • 如果列表只有一项,则不需要排序
  • 如果列表比这个长,排序xs,然后将x插入到现在排序的xs

有趣的是,通过修改winsert 以将函数作为参数(代替length),wsort 可用于根据各种标准进行排序。尝试根据每个子列表的总和对列表列表进行排序。

【讨论】:

  • 好极了,我喜欢。我同意它很干净。同样的问题我问诺曼:我认为“wsort [x]”将是一个无可辩驳的模式(或者至少,它会匹配从 0 到无穷大的任何长度的任何列表)。显然不是,因为你们都在使用它。那为什么不呢?谢谢。
  • 相反,它只匹配具有单个元素的列表。 "wsort (x:xs)" 将匹配任何具有至少一个元素的列表。您可能会将模式匹配与类型签名混淆。 type“[x]”表示一个列表,其元素是任意类型,而pattern“[x]”表示一个具有单个元素的列表。该类型仅出现在“::”之后。
  • [x] 匹配任何只有一个元素的列表。但是,x 可以是任何类型,甚至可以是列表本身。如果 x 是这样一个子列表,则该子列表可以是任意长度。 [x] 与 [] 或 [1, 2] 不匹配。 [x] 匹配 [1](x 为 1)、[[]](x 为 [])、[[1]](x 为 [1])和 [[1, 2]](x 为 [ 1, 2]).
猜你喜欢
  • 2013-05-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多