因此,假设我们已经有了所有可能字符串的列表:
allStrings :: [String]
allStrings = ...
Given allStrings
,我们如何创建另一个所有可能字符串的列表?
alsoAllStrings :: [String]
让我们将每个可能的字符串视为单个字符前缀和字符串后缀
alsoAllStrings = [ c : s
字符串后缀可以是空字符串,也可以是所有可能字符串列表的成员。
| s <- "" : allStrings
单字符前缀位于'a'
thru 'z'
or '0'
thru '9'
.
, c <- ['a'..'z'] ++ ['0'..'9']
]
(这是使用列表理解 - 我们也可以使用同样的方法concatMap
:
alsoAllStrings = concatMap (\s -> map (:s) $ ['a'..'z'] ++ ['0'..'9']) $ "" : allStrings
)
现在让我们回到最初的问题。我们如何找到allStrings
?
在大多数语言中,我们不能 - 这是一个无限的字符串列表,程序永远不会完成。但由于 Haskell 很懒,所以只生成尽可能多的内容就很酷了allStrings
正如我们实际使用的那样。
这让我们做的一件令人惊讶的事情是我们可以定义allStrings
按照alsoAllStrings
.
allStrings = alsoAllStrings
或者,更有可能的是,就其本身而言。
allStrings = [ c : s | s <- "" : allStrings, c <- ['a'..'z'] ++ ['0'..'9'] ]
这称为核心递归数据 - 根据其自身定义的数据。
在 ghci 中尝试一下:
ghci> let allStrings = [ c : s | s <- "": allStrings, c <- ['a'..'z'] ++ ['0'..'9'] ]
ghci> take 100 allStrings
["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9","aa","ba","ca","da","ea","fa","ga","ha","ia","ja","ka","la","ma","na","oa","pa","qa","ra","sa","ta","ua","va","wa","xa","ya","za","0a","1a","2a","3a","4a","5a","6a","7a","8a","9a","ab","bb","cb","db","eb","fb","gb","hb","ib","jb","kb","lb","mb","nb","ob","pb","qb","rb","sb","tb","ub","vb","wb","xb","yb","zb","0b","1b"]
它起作用的原因(并且不仅仅是无限循环)是它在使用自身之前定义了自身的一部分。我们包含的第一个元素allStrings
是空字符串的单字符扩展""
。所以当我们开始迭代元素时allStrings
用作后缀,前几个元素allStrings
已经定义并且可用。我们处理的后缀越多,元素就越多allStrings
已定义并可用作后缀。
它与以核心递归方式定义斐波那契数的常见 Haskellism 非常相似:
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
如果 corecursion 需要一段时间才能让您理解,请不要感到惊讶。不过,理解这一点还是值得的。