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】:take
和 drop
需要 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 新手将列表分成两半的麻烦的主要内容,如果未能解决你的问题,请参考以下文章