模式绑定:为什么允许递归模式?
Pattern binding: why are recursive patterns allowed?
Prelude> let (Just x) = Just (x - 1) in x
*** Exception: <<loop>>
为什么编译器接受该声明?它可以在语法上告诉它绑定到 loop(?)
GHC 有点知道:
Prelude> :set -XBangPatterns
Prelude> let !(Just x) = Just (x - 1) in x
Recursive bang-pattern or unboxed-tuple bindings aren't allowed:
[Exception
和 GHCi 7.10.2 中的编译器失败消息。在 GHC 8.10 中没有有用的信息,只是循环。]
递归模式绑定是否有一些合理的用途?我认为对于模式绑定声明,lhs 的自由变量应该与 rhs(?)
的自由变量分离
'pattern-binding' 我的意思是,根据 Haskell 语言报告 2010,第 4.4.3.2 节,特别是从数据 constructor/at 最外层范围开始的 lhs。
(我希望能有比 lhs 上的单一 var 更令人兴奋的东西。这仍然算作 'pattern-binding',请参阅评论。)
当然对于函数声明,编译器必须允许递归,因为它无法从语法上判断函数是否会循环:
Prelude> let { f 0 = 0;
f x = f (x - 1) } -- will terminate (for positive inputs)
in f 5
lhs 上的裸变量在我看来更像是 niladic 函数而不是实际模式。
数据可以像 Haskell 中的函数一样惰性。因此,肯定有递归绑定的用例。例如,考虑 fix
:
的定义
fix :: (a -> a) -> a
fix f = let x = f x in x
twoOnes :: [Int]
twoOnes = take 2 $ fix (1 :)
观察到,由于 let 绑定的右侧在左侧是非严格的,因此绑定可以产生结果。
考虑到一些同样愚蠢的 Num
实例,即使您最初的“显然很愚蠢”的表达也可能会终止:
instance Num () where
fromInteger x = ()
x - y = ()
loop :: ()
loop = let (Just x) = Just (x - 1) in x
显然对于这个特定类型不是特别有用,但是 GHC 不负责决定哪些合法表达式有用,并且通过更多的工作,可以想象更多有用的 Num 实例可能会在这里终止。
有关将递归模式与构造函数一起使用而不仅仅是单个变量的有用代码示例,请参阅广度优先重新标记函数:
data Tree a
= Leaf a
| Node (Tree a) (Tree a)
deriving Show
bfl :: forall a b. [b] -> Tree a -> Tree b
bfl l t = result
where
go :: Tree a -> [[b]] -> ([[b]], Tree b)
go (Node l r) (xs:xss0) =
let (xss1, l') = go l xss0
(xss2, r') = go r xss1
in (xs:xss2, Node l' r')
go (Leaf _) ((x:xs):xss) = (xs:xss, Leaf x)
(xss, result) = go t (l:xss)
在最后一行,我们使用元组 (xss, result)
,它是通过将 xss
本身传递给 go
辅助函数来定义的。这是将树的每个级别的结果列表传递到下一个级别所必需的。
Prelude> let (Just x) = Just (x - 1) in x
*** Exception: <<loop>>
为什么编译器接受该声明?它可以在语法上告诉它绑定到 loop(?)
GHC 有点知道:
Prelude> :set -XBangPatterns
Prelude> let !(Just x) = Just (x - 1) in x
Recursive bang-pattern or unboxed-tuple bindings aren't allowed:
[Exception
和 GHCi 7.10.2 中的编译器失败消息。在 GHC 8.10 中没有有用的信息,只是循环。]
递归模式绑定是否有一些合理的用途?我认为对于模式绑定声明,lhs 的自由变量应该与 rhs(?)
的自由变量分离'pattern-binding' 我的意思是,根据 Haskell 语言报告 2010,第 4.4.3.2 节,特别是从数据 constructor/at 最外层范围开始的 lhs。
(我希望能有比 lhs 上的单一 var 更令人兴奋的东西。这仍然算作 'pattern-binding',请参阅评论。)
当然对于函数声明,编译器必须允许递归,因为它无法从语法上判断函数是否会循环:
Prelude> let { f 0 = 0;
f x = f (x - 1) } -- will terminate (for positive inputs)
in f 5
lhs 上的裸变量在我看来更像是 niladic 函数而不是实际模式。
数据可以像 Haskell 中的函数一样惰性。因此,肯定有递归绑定的用例。例如,考虑 fix
:
fix :: (a -> a) -> a
fix f = let x = f x in x
twoOnes :: [Int]
twoOnes = take 2 $ fix (1 :)
观察到,由于 let 绑定的右侧在左侧是非严格的,因此绑定可以产生结果。
考虑到一些同样愚蠢的 Num
实例,即使您最初的“显然很愚蠢”的表达也可能会终止:
instance Num () where
fromInteger x = ()
x - y = ()
loop :: ()
loop = let (Just x) = Just (x - 1) in x
显然对于这个特定类型不是特别有用,但是 GHC 不负责决定哪些合法表达式有用,并且通过更多的工作,可以想象更多有用的 Num 实例可能会在这里终止。
有关将递归模式与构造函数一起使用而不仅仅是单个变量的有用代码示例,请参阅广度优先重新标记函数:
data Tree a
= Leaf a
| Node (Tree a) (Tree a)
deriving Show
bfl :: forall a b. [b] -> Tree a -> Tree b
bfl l t = result
where
go :: Tree a -> [[b]] -> ([[b]], Tree b)
go (Node l r) (xs:xss0) =
let (xss1, l') = go l xss0
(xss2, r') = go r xss1
in (xs:xss2, Node l' r')
go (Leaf _) ((x:xs):xss) = (xs:xss, Leaf x)
(xss, result) = go t (l:xss)
在最后一行,我们使用元组 (xss, result)
,它是通过将 xss
本身传递给 go
辅助函数来定义的。这是将树的每个级别的结果列表传递到下一个级别所必需的。