Haskell: 为什么在模式匹配中不允许使用++?
Haskell: Why ++ is not allowed in pattern matching?
假设我们想在Haskell中编写自己的sum
函数:
sum' :: (Num a) => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs
为什么我们不能这样做:
sum' :: (Num a) => [a] -> a
sum' [] = 0
sum' (xs++[x]) = x + sum' xs
换句话说为什么我们不能在模式匹配中使用++
?
++
不是构造函数,它只是一个普通函数。您只能匹配构造函数。
您可以使用 ViewPatterns
or PatternSynonyms
来增强模式匹配的能力(感谢@luqui)。
数据构造函数只能pattern-match,++
是函数,不是数据构造函数。
数据构造器是持久化的;像 'c':[]
这样的值无法进一步简化,因为它 是 类型 [Char]
的基本值。但是,像 "c" ++ "d"
这样的表达式可以随时替换为等效的 "cd"
,因此不能可靠地指望它出现在模式匹配中。
(您可能会争辩说 "cd"
总是可以用 "c" ++ "d"
代替,但通常在列表和分解之间没有 one-to-one 映射 ++
。为了模式匹配的目的,"cde"
等同于 "c" ++ "de"
或 "cd" ++ "e"
吗?)
您只能对构造函数进行模式匹配,不能对一般函数进行模式匹配。
从数学上讲,构造函数是一个 injective 函数:参数的每个组合都给出一个唯一值,在本例中是一个列表。因为该值是唯一的,所以该语言可以将它 再次解构为原始参数。即,当您在 :
上进行模式匹配时,您实际上使用的是函数
uncons :: [a] -> Maybe (a, [a])
检查列表是否属于您可以用 :
构造的形式(即,如果它是 non-empty),如果是,则返回头部和尾部。
++
不是单射的,例如
Prelude> [0,1] ++ [2]
[0,1,2]
Prelude> [0] ++ [1,2]
[0,1,2]
这两种表示方式都是正确的,那么应该如何再次解构列表?
然而,您可以做的是定义一个新的“虚拟”构造函数,它的作用类似于 :
,因为它始终将一个元素与列表的其余部分(如果可能)分开,但如此右边:
{-# LANGUAGE PatternSynonyms, ViewPatterns #-}
pattern (:>) :: [a] -> a -> [a]
pattern (xs:>ω) <- (unsnoc -> Just (xs,ω))
where xs:>ω = xs ++ [ω]
unsnoc :: [a] -> Maybe ([a], a)
unsnoc [] = Nothing
unsnoc [x] = Just x
unsnoc (_:xs) = unsnoc xs
然后
sum' :: Num a => [a] -> a
sum' (xs:>x) = x + sum xs
sum' [] = 0
请注意,这是非常低效的,因为 :>
pattern-synonym 实际上需要遍历整个列表,所以 sum'
具有二次而不是线性复杂度。
允许在左端和右端高效进行模式匹配的容器是Data.Sequence
,其模式为:<|
和:|>
同义词。
这是一个值得提出的问题,到目前为止它已经收到了合理的答案(只允许构造函数咕哝、咕哝单射、咕哝模糊),但仍有时间改变这一切。
我们可以说规则是什么,但是大多数关于为什么规则是它们的解释都是从 over-generalising 这个问题开始的,解决了为什么我们不能对任何旧函数进行模式匹配(咕哝序言)。这是为了忽略 ++
不是任何旧函数的事实:它是一个(空间上)线性 plugging-stuff-together 函数,由 zipper-structure 诱导列表。模式匹配就是把东西分开,实际上,根据 plugger-togetherers 和代表组件的模式变量来标记过程。它的动机是清晰的。所以我想要
lookup :: Eq k => k -> [(k, v)] -> Maybe v
lookup k (_ ++ [(k, v)] ++ _) = Just v
lookup _ _ = Nothing
不仅因为它会让我想起三十年前我实现一种函数式语言时的乐趣,其模式匹配恰好提供了这种功能。
关于它模棱两可的反对意见是合理的,但不会破坏交易。 Plugger-togetherers 与 ++
一样,仅提供有限输入的有限多次分解(如果您正在处理无限数据,那是您自己的观察),所以所涉及的最坏情况是 search,而不是 magic(发明任意函数可能丢弃的任意输入)。搜索需要某种优先排序方式,但我们的有序匹配规则也是如此。搜索也可能导致失败,但同样可以匹配。
我们有一种明智的方法来管理通过 Alternative
抽象提供替代方案(失败和选择)的计算,但我们不习惯将模式匹配视为此类计算的一种形式,这就是为什么我们仅在 expression 语言中利用 Alternative
结构。贵族,如果不切实际,例外是 match-failure in do
-notation,它调用相关的 fail
而不是必然崩溃。模式匹配是尝试计算适合 'right-hand side' 表达式求值的环境;已经处理了无法计算这样的环境,那么为什么不选择呢?
(编辑: 当然,我应该补充一点,如果你在一个模式中有不止一个有弹性的东西,你才真正需要搜索,所以建议 xs++[x]
模式不应该触发任何选择。当然,找到列表的末尾需要时间。)
假设有某种有趣的括号用于编写 Alternative
计算,例如,(|)
表示 empty
,(|a1|a2|)
表示 (|a1|) <|> (|a2|)
,以及一个普通的旧 (|f s1 .. sn|)
意思是 pure f <*> s1 .. <*> sn
。人们也可以很好地想象 (|case a of {p1 -> a1; .. pn->an}|)
根据 Alternative
组合子执行 search-patterns 的合理翻译(例如涉及 ++
)。我们可以写
lookup :: (Eq k, Alternative a) => k -> [(k, v)] -> a k
lookup k xs = (|case xs of _ ++ [(k, v)] ++ _ -> pure v|)
对于由可微函子的固定点生成的任何数据类型,我们可以获得合理的 search-patterns 语言:符号微分正是将结构元组转换为可能子结构选择的原因。好的旧 ++
只是 sublists-of-lists 示例(这令人困惑,因为 list-with-a-hole-for-a-sublist 看起来很像列表,但其他数据类型并非如此)。
有趣的是,有了 LinearTypes
的点,我们甚至可以通过它们的空洞和根来控制空洞数据,然后在恒定时间内破坏性地插入。只有当你没有注意到你在做的时候,这才是可耻的行为。
假设我们想在Haskell中编写自己的sum
函数:
sum' :: (Num a) => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs
为什么我们不能这样做:
sum' :: (Num a) => [a] -> a
sum' [] = 0
sum' (xs++[x]) = x + sum' xs
换句话说为什么我们不能在模式匹配中使用++
?
++
不是构造函数,它只是一个普通函数。您只能匹配构造函数。
您可以使用 ViewPatterns
or PatternSynonyms
来增强模式匹配的能力(感谢@luqui)。
数据构造函数只能pattern-match,++
是函数,不是数据构造函数。
数据构造器是持久化的;像 'c':[]
这样的值无法进一步简化,因为它 是 类型 [Char]
的基本值。但是,像 "c" ++ "d"
这样的表达式可以随时替换为等效的 "cd"
,因此不能可靠地指望它出现在模式匹配中。
(您可能会争辩说 "cd"
总是可以用 "c" ++ "d"
代替,但通常在列表和分解之间没有 one-to-one 映射 ++
。为了模式匹配的目的,"cde"
等同于 "c" ++ "de"
或 "cd" ++ "e"
吗?)
您只能对构造函数进行模式匹配,不能对一般函数进行模式匹配。
从数学上讲,构造函数是一个 injective 函数:参数的每个组合都给出一个唯一值,在本例中是一个列表。因为该值是唯一的,所以该语言可以将它 再次解构为原始参数。即,当您在 :
上进行模式匹配时,您实际上使用的是函数
uncons :: [a] -> Maybe (a, [a])
检查列表是否属于您可以用 :
构造的形式(即,如果它是 non-empty),如果是,则返回头部和尾部。
++
不是单射的,例如
Prelude> [0,1] ++ [2]
[0,1,2]
Prelude> [0] ++ [1,2]
[0,1,2]
这两种表示方式都是正确的,那么应该如何再次解构列表?
然而,您可以做的是定义一个新的“虚拟”构造函数,它的作用类似于 :
,因为它始终将一个元素与列表的其余部分(如果可能)分开,但如此右边:
{-# LANGUAGE PatternSynonyms, ViewPatterns #-}
pattern (:>) :: [a] -> a -> [a]
pattern (xs:>ω) <- (unsnoc -> Just (xs,ω))
where xs:>ω = xs ++ [ω]
unsnoc :: [a] -> Maybe ([a], a)
unsnoc [] = Nothing
unsnoc [x] = Just x
unsnoc (_:xs) = unsnoc xs
然后
sum' :: Num a => [a] -> a
sum' (xs:>x) = x + sum xs
sum' [] = 0
请注意,这是非常低效的,因为 :>
pattern-synonym 实际上需要遍历整个列表,所以 sum'
具有二次而不是线性复杂度。
允许在左端和右端高效进行模式匹配的容器是Data.Sequence
,其模式为:<|
和:|>
同义词。
这是一个值得提出的问题,到目前为止它已经收到了合理的答案(只允许构造函数咕哝、咕哝单射、咕哝模糊),但仍有时间改变这一切。
我们可以说规则是什么,但是大多数关于为什么规则是它们的解释都是从 over-generalising 这个问题开始的,解决了为什么我们不能对任何旧函数进行模式匹配(咕哝序言)。这是为了忽略 ++
不是任何旧函数的事实:它是一个(空间上)线性 plugging-stuff-together 函数,由 zipper-structure 诱导列表。模式匹配就是把东西分开,实际上,根据 plugger-togetherers 和代表组件的模式变量来标记过程。它的动机是清晰的。所以我想要
lookup :: Eq k => k -> [(k, v)] -> Maybe v
lookup k (_ ++ [(k, v)] ++ _) = Just v
lookup _ _ = Nothing
不仅因为它会让我想起三十年前我实现一种函数式语言时的乐趣,其模式匹配恰好提供了这种功能。
关于它模棱两可的反对意见是合理的,但不会破坏交易。 Plugger-togetherers 与 ++
一样,仅提供有限输入的有限多次分解(如果您正在处理无限数据,那是您自己的观察),所以所涉及的最坏情况是 search,而不是 magic(发明任意函数可能丢弃的任意输入)。搜索需要某种优先排序方式,但我们的有序匹配规则也是如此。搜索也可能导致失败,但同样可以匹配。
我们有一种明智的方法来管理通过 Alternative
抽象提供替代方案(失败和选择)的计算,但我们不习惯将模式匹配视为此类计算的一种形式,这就是为什么我们仅在 expression 语言中利用 Alternative
结构。贵族,如果不切实际,例外是 match-failure in do
-notation,它调用相关的 fail
而不是必然崩溃。模式匹配是尝试计算适合 'right-hand side' 表达式求值的环境;已经处理了无法计算这样的环境,那么为什么不选择呢?
(编辑: 当然,我应该补充一点,如果你在一个模式中有不止一个有弹性的东西,你才真正需要搜索,所以建议 xs++[x]
模式不应该触发任何选择。当然,找到列表的末尾需要时间。)
假设有某种有趣的括号用于编写 Alternative
计算,例如,(|)
表示 empty
,(|a1|a2|)
表示 (|a1|) <|> (|a2|)
,以及一个普通的旧 (|f s1 .. sn|)
意思是 pure f <*> s1 .. <*> sn
。人们也可以很好地想象 (|case a of {p1 -> a1; .. pn->an}|)
根据 Alternative
组合子执行 search-patterns 的合理翻译(例如涉及 ++
)。我们可以写
lookup :: (Eq k, Alternative a) => k -> [(k, v)] -> a k
lookup k xs = (|case xs of _ ++ [(k, v)] ++ _ -> pure v|)
对于由可微函子的固定点生成的任何数据类型,我们可以获得合理的 search-patterns 语言:符号微分正是将结构元组转换为可能子结构选择的原因。好的旧 ++
只是 sublists-of-lists 示例(这令人困惑,因为 list-with-a-hole-for-a-sublist 看起来很像列表,但其他数据类型并非如此)。
有趣的是,有了 LinearTypes
的点,我们甚至可以通过它们的空洞和根来控制空洞数据,然后在恒定时间内破坏性地插入。只有当你没有注意到你在做的时候,这才是可耻的行为。