Haskell 新手将列表分成两半的麻烦

Posted

技术标签:

【中文标题】Haskell 新手将列表分成两半的麻烦【英文标题】:Haskell Novice Trouble with Splitting a List in Half 【发布时间】:2014-11-23 15:04:13 【问题描述】:

这是我尝试编写一个函数,将偶数长度的列表分成相等的两半。

halve :: [a] -> ([a], [a])
halve x 
   | even len = (take half x, drop half x)
   | otherwise = error "Cannnot halve a list of odd length"
   where
      len = length x
      half = len / 2

我收到以下错误:

 No instance for (Fractional Int) arising from a use of ‘/’
    In the expression: len / 2
    In an equation for ‘half’: half = len / 2
    In an equation for ‘halve’:

我不明白这个错误,但我怀疑 Haskell 需要提前被告知 len 是你可以除以 2 的东西。那么,我该如何纠正这个例子?我的代码在惯用的haskell附近吗?如果有其他关于我的代码的 cmets,我将不胜感激。

【问题讨论】:

halve a = go a [] a; go [] b c = (reverse b, c); go a b (c:d) = go (drop 2 a) (c:b) d 是 Lisp 民间传说中的一个著名技巧。 另见***.com/questions/13784671/simple-haskell-splitlist 【参考方案1】:

/ 用于当您很高兴得到非整数答案时。由于您已经检查了数字的偶数,您可以愉快地使用div 的整数除法:

halve :: [a] -> ([a], [a]) 
halve x | even len = (take half x, drop half x) 
        | otherwise = error "Cannnot halve a list of odd length" 
   where len = length x 
         half = len `div` 2

(我个人很乐意不准确地将奇数长度列表减半并避免错误消息,但这取决于您。)

这种区别由类型表示:

(/) :: Fractional a => a -> a -> a
div :: Integral a => a -> a -> a 

所以你只能在类型支持非整数除法时使用/,当它是整数类型时你只能使用div。这样你就不会误以为你在做一种除法,而实际上你在做另一种。

干得好,顺便说一句,你想得很好。

“没有实例...”

实际上,“No instance for ...”错误消息几乎总是因为类型错误。当我以错误的顺序放置论点时,我最常得到它。在这种情况下,当您的类型为Int 时,您已经使用了其他类型的除法。

它说“无实例”,因为您尝试使用的函数适用于一类类型,但您提供的数据类型不在该类(的实例)中。编译器将此视为缺少的实例声明,通常这只是一个错误并且完全是错误的类型。我很少打算将某些东西作为类的实例然后忘记,而我更经常将参数放在错误的位置。

【讨论】:

谢谢。我注意到 Haskell 的错误对于新手来说并不容易理解。随着时间的推移会变得更好吗? @ArmenTsirunyan 是的,一旦你习惯了类型系统,它们就会变得更容易理解。【参考方案2】:

请注意,列表减半可以在结构上完成,无需任何索引。并行地对列表进行两次遍历,一次以另一次的速度前进。当快的触底时,慢的已经到了一半。

halve :: [a] -> ([a], [a])
halve xs = go xs xs
  where
    go xs []  = ([],xs)
    go (x:xs) [_] = ([x],xs)
    go (x:xs) (_:_:ys) = let (first,last) = go xs ys in (x:first, last)

go 中的第二个子句是奇数长度列表的“决胜局”。照原样,代码将奇数放在上半场的末尾。如果您愿意,只需将右侧更改为 ([],x:xs)

这个想法是 Danvy 和 Goldberg (http://www.brics.dk/RS/05/3/BRICS-RS-05-3.pdf) 确定的“There And Back Again”模式的一半关键。

【讨论】:

这看起来像是 Will Ness 在评论中建议的方法的一个版本,但您的方法更懒惰——第一个列表的元素逐渐可用。不错。 哦,没听懂。好吧,它已经脱离了民间传说并进入了具有该来源的文献 :) 但是,在另一个,老实说不是故意的苦涩的情况下,请注意,我发现 Jubobs 的答案在我发布我的答案后 2 小时就长了一个尾巴。这很常见吗?与 wiki 相比,这不是违背问答网站的目的吗? Jubob 的加法和你的答案完全不同,所以我不会担心。 @Kris 我不记得我更新答案时的确切想法,但我的意图不是抄袭你的。【参考方案3】:

length函数的类型签名是[a] -> Int;这告诉你length 返回一个Int

此外,/ 运算符(类型签名Fractional a => a -> a -> a)仅与具有Fractional 实例的类型兼容;因为no Fractional instance exists for Int,你不能写类似的东西

length x / 2

使用div函数(类型签名Integral a => a -> a -> a)执行整数除法:

div len 2

此外,还有一些方法可以改进halve 的实现。

    您可以使用splitAt half x 而不是(take half x, drop half x),它只需要通过x 一次。

    这是一个更自然(这就是纸牌玩家所做的!)和更高效(无需计算长度)的实现:

    halve :: [a] -> ([a], [a])
    halve []  = ([], [])
    halve [x] = ([x], [])  -- necessary case for input lists that contains an odd number of elements
    halve (x : y : zs) = (x : xs, y : ys)
      where
        (xs, ys) = halve zs
    

【讨论】:

【参考方案4】:

takedrop 需要 Int 类型的参数。 (/) 执行小数除法,因此其结果不能是 Int 类型。改用div 来执行整数除法:

halve :: [a] -> ([a], [a])
halve x 
   | even len = (take half x, drop half x)
   | otherwise = error "Cannnot halve a list of odd length"
   where
      len = length x
      half = len `div` 2

【讨论】:

以上是关于Haskell 新手将列表分成两半的麻烦的主要内容,如果未能解决你的问题,请参考以下文章

将节点列表分成两半

将列表拆分为较小的列表(分成两半)

将屏幕分成两半并将图像视图放在两半的两半上

将字符串分成两半

将文本分成两半,但在最近的句子

Haskell:将文件中的每一行插入到列表中