我试图解决一个Haskell任务,编写一个涉及扑克牌自定义数据类型的函数。该函数应该接受一张牌作为参数,并返回该牌的值,该值基于其排名(Ace -> 11,Jack/Queen/King -> 10,Numbers 2-9 -> 2-9)。这个问题基本上是需要访问数据构造函数的参数,以防卡片等级具有数值。
我找到了一个使用模式匹配的解决方案,感谢this和this thread,我还找到了如何使用case of
和模式保护(cardValueGuards
)实现它。
我仍然想知道是什么让模式匹配在技术层面上工作。我想知道在这个例子中,一种方法是否比另一种方法更好。(见下面的问题。)
data Color = Red | Black deriving (Show, Eq)
data Suit = Clubs | Diamonds | Hearts | Spades deriving (Show, Eq)
data Rank = Num Int | Jack | Queen | King | Ace deriving (Show, Eq)
data Card = Card { suit :: Suit, rank :: Rank } deriving (Show, Eq)
-- Working solutions
cardValue :: Card -> Int
cardValue card = selectNum (rank card)
where
selectNum :: Rank -> Int
selectNum Ace = 11
selectNum (Num a) = a
selectNum _ = 10
cardValueCase :: Card -> Int
cardValueCase card = case rank card of
Ace -> 11
(Num a) -> a
otherwise -> 10
cardValueGuards :: Card -> Int
cardValueGuards card
| rank card == Ace = 11
| (Num a) <- rank card = a
| otherwise = 10
cardValueGuardsLong :: Card -> Int
cardValueGuardsLong card
| rank card == Ace = 11
| rank card == (Num 2) = 2
| rank card == (Num 3) = 3
| rank card == (Num 4) = 4
| rank card == (Num 5) = 5
| rank card == (Num 6) = 6
| rank card == (Num 7) = 7
| rank card == (Num 8) = 8
| rank card == (Num 9) = 9
| otherwise = 10
下面是我以前的尝试。(我假设rank card == (Num a) = a
失败是因为使用函数(==)
需要特定的值而不是类型变量。
-- Not working
cardValueGuardsOld :: Card -> Int
cardValueGuardsOld card
| rank card == Ace = 11
| rank card == (Num a) = a
| otherwise = 10
详细说明:根据我的理解,当使用数据构造函数来构造Records的值时,Haskell创建了一个函数,该函数返回与记录名称相关联的值。因此,使用mycard = Card Spades Ace
创建一张卡片可以让您通过使用rank mycard
(返回Ace
)访问卡片的等级。对于mycard2 = Card Diamonds (Num 8)
,它将返回Num 8
。
从第二个线程关于模式匹配和守卫之间的区别来看,守卫基本上是if
-表达式,而模式匹配就像case of
。在this answer中,它说不同之处在于模式匹配只检查“是否使用给定的构造函数创建了一个值”,而模式匹配绑定了变量。
直觉上,我会理解selectNum (rank card)
后面跟着selectNum (Num a) = a
,基本上和| rank card == (Num a) = a
做同样的事情。在cardValue
和cardValueGuardsOld
中,我都得到了卡片的等级,然后将其传递给检查其值。显然有些不同,但从我目前所读到的内容来看,我仍然不明白为什么在| rank card == (Num a) = a
失败时允许模式匹配版本。
- 模式匹配和
case of
到底是如何实现的(在编译器级别上)?在cardValueGuardsOld
中,当相等性检查失败时,允许它们工作的区别是什么?它与作用域有关吗?还是因为模式匹配只检查构造函数或其他东西? - 在这种情况下,标准模式匹配/
case of
比使用guards更可取还是更不可取?如果我真的想检查数字卡只有2到9的数字,这会让警卫成为更好的选择吗?
1条答案
按热度按时间5tmbdcev1#
(我假设
rank card == (Num a) = a
失败是因为使用函数(==)
需要特定的值而不是类型变量。是的。(这里
a
是一个普通变量,而不是类型变量。)我仍然不明白为什么模式匹配版本是允许的,而
| rank card == (Num a) = a
失败。正如你所说:因为
(==)
不是特殊的语法,它是一个像许多其他函数一样的函数。编写x == y
与编写函数调用(==) x y
相同。与守卫比较此函数调用需要两个 * 表达式 *
rank card
和Num a
作为参数。第二个表达式涉及到一个变量a
,这个变量之前没有定义,所以编译器会引发一个错误。经验法则是:表达式中变量的值需要是已知的,以便计算表达式的值。(这在递归定义中部分是正确的,但让我们忽略它。相比之下,patterns 定义了它们内部变量的值。如果我们将一个值与一个模式匹配,这可能失败(构造函数不匹配)或成功,在后一种情况下,模式中的所有变量都绑定到一个值,定义它们。
因此,虽然语法
Num a
既可以用作表达式,也可以用作模式,但其语义在这两种情况下有很大的不同。模式匹配和case是如何实现的(在编译器级别)?当
cardValueGuardsOld
中的相等性检查失败时,允许它们工作的区别是什么?它与作用域有关吗?还是因为模式匹配只检查构造函数或其他东西?函数调用与模式匹配、表达式与模式之间的区别就在于此。
当你提到模式只能涉及构造函数,而不能涉及通用函数时,你也是正确的。我们不能写
因为
g
不是构造函数。我们可以写构造函数
G
。同样,对于大多数类型,函数
(==)
无论如何都是使用模式匹配实现的。因此,直接使用模式而不是调用==
是一个好主意。还有一些Haskell的高级特性(如GADT)只能使用模式。
最后,使用模式避免了一个常见的设计陷阱,称为“布尔盲”,其中布尔值被广泛使用,而不是更丰富的类型(例如,
Maybe a, Either a b
)。过多地关注布尔值通常会导致糟糕的Haskell代码。在这种情况下,标准的模式匹配/情况是比使用保护更可取还是更不可取,或者这是一个品味问题?
虽然在某些情况下,可能有一种方法可以同时使用这两种方法来解决任务,但模式匹配应该是“默认”方法。在模式匹配工作时使用
==
不是一个好主意。考虑
使用
-Wall
打开警告后,GHC可以检测到f
处理所有情况,并且不会生成任何警告。这不是
g
的情况。GHC将警告说,如果没有最终的otherwise
案例,所有的警卫都可能是假的。这很烦人另外,如果我们稍后添加一个新的构造函数呢?
现在我们在
f
上得到一个错误,这很好,因为我们需要为新的情况修复函数。相比之下,g
保持编译,并将在运行时崩溃。如果我真的想检查数字卡只有2到9的数字,这会让警卫成为更好的选择吗?
如果我们想检查
10 <= x && x <= 1000
,我们不能用990个模式来做。