声明一个空列表,但实际上它并不为空?Haskell

4

我是一个Haskell新手,正在尝试一些东西。我创建了一个有保障的递归函数。请看下面的函数:

filterAge :: [Person] -> [String]
filterAge (x:xs)
 | (x:xs) == []                        = []
 | (getAge x) < 30 || (getAge x) > 40  = [] ++ filterAge xs
 | otherwise                           = [getName x] ++ filterAge xs

我有一个数据集,其中包括10个人,我在这个方法中使用它。当我尝试这个函数时,它返回了所有正确的人,但之后它出现了一个非全面模式错误:

["Lise","Jaap","Elle","Ebba"*** Exception: D:\...:(44,1)-(47,77): Non-exhaustive patterns in function filterAge

我发现它从未到达第一个保护程序。所以我进行了一些尝试,并发现了一些非常奇怪的事情(在我看来):

*Main> let (x:xs) = []
*Main> (x:xs) == []
False

现在我的主要问题是:为什么(x:xs) == []返回False?
如果有更好的方法可以帮我完成这个函数,那就太棒了,但这并不是非常重要。
提前感谢!
编辑
感谢Willem Van Onsem和Lambda.xy.x,他们很快回答了我的问题。这导致了以下完美运行的函数:
filterAge :: [Person] -> [String]
filterAge []                           = []
filterAge (x:xs)
 | (getAge x) < 30 || (getAge x) > 40  = [] ++ filterAge xs
 | otherwise                           = [getName x] ++ filterAge xs

但是如果你想得到最好的版本,你需要查看Willem Van Onsem的答案。


5
(x:xs) == [] 永远不可能成功,因为 (x:xs) 是至少含有一个元素的列表。 - Willem Van Onsem
谢谢,这很有帮助。所以我应该绕过它吗?不过我不确定如何做到这一点,因为我的函数中需要(x:xs)。 - Snake
1
为了进一步解释,列表有两个构造函数:[]_ : _,具有最外层构造函数[]的列表永远不能将:作为最外层构造函数。 - lambda.xy.x
2
你已经在 filterAge (x:xs) 的定义中进行了模式匹配,并在那里排除了空列表。最简单的解决方法是在上面添加一个定义 filterAge []。或者你可以定义 filterAge xs 并在任意列表上进行匹配。 - lambda.xy.x
@ lambda.xy.x 那很好用!我完全忽略了这种可能性,虽然我以前用过一点,但没有想到可以用在这个函数上。非常感谢! - Snake
我相信这个错误,即认为x:xs可以为空,在SO上已经出现了几次。我们应该选择一个规范问题(可能是这个),并将未来的出现视为重复。 - chi
2个回答

10

列表被定义为:

data [] a = [] | a : [a]

因此,列表有两个构造器:[]表示空列表,(x:xs)表示具有一个元素和一个可以存储任意数量(零个或多个)其余元素的尾部的构造器。

因此,(x:xs)是至少有一个元素x的列表。 xs可以是一个空列表(因为它的类型是[a]),但x的类型是a,所以它是列表的"头部"。您的let语句使用模式匹配工作,由于空列表无法匹配(x:xs),因此它将始终失败。

另一个影响是您的第一个保护条件永远不会触发。为了解决这个问题,您应该为空列表实现一个单独的情况。像这样:

filterAge :: [Person] -> [String]
<b>filterAge []     = []</b> -- empty list case
filterAge (x:xs) -- first guard dropped
    | (getAge x) < 30 || (getAge x) > 40  = [] ++ filterAge xs
    | otherwise                           = [getName x] ++ filterAge xs
请注意,在第二个子句中,我们删除了第一个守卫,因为我们知道它总是会失败的,因此检查这个守卫可能只会增加 CPU 周期成本。
仍然有一些部分可以进行优化:
1.我们调用了两次 getAge,这是无用的,我们可以使用 where 子句来优化它; 2.[] ++ somelist 等同于 somelist:在空列表之后附加将导致该列表; 3.[element] ++ somelist 等同于 element : somelist,因为现在我们直接使用列表构造函数。
因此,我们的 filterAge 可以重写为:
filterAge :: [Person] -> [String]
filterAge []     = [] -- empty list case
filterAge (x:xs) | <b>age</b> < 30 || <b>age</b> > 40  = <b>filterAge xs</b>
                 | otherwise             = <b>getName x : filterAge xs</b>
    <b>where age = getAge x</b>
请注意,如果您使用-Wincomplete-patterns标志(对于不完整的模式进行警告)编译(或启动解释器),Haskell将自动警告您的功能定义不完整,并且存在输入模式未定义子句。

1
非常感谢您的回答!它不仅回答了我的问题,还帮助我分析和改进了我的代码,并解释了为什么这样做会更好。 - Snake

4
回答你的“主要”问题,
*Main> let (x:xs) = [] -- (1)
意思是,当需要x或者xs的值时,匹配(x:xs)和[],并使用所得到的变量。这个匹配永远不会成功,任何尝试都将导致模式匹配失败错误。
那么为什么没有错误呢?
*Main> (x:xs) == [] -- (2)
现在这意味着,尝试比较(x:xs)和[]。两个都是列表;比较列表涉及模式匹配顶部结构,然后递归地比较组件,在成功时返回False,在失败时(实际上不失败)返回False。因此,尝试匹配(_:_)和[],失败了,导致立即返回False值作为比较的结果。
请注意,从未请求绑定到x和xs的值;因此,在(1)中的(x:xs)和[]的匹配从未被触发,因此没有错误。

谢谢您的解释,您的答案让我更加理解了。 - Snake

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接