Итак, предположим, у нас уже есть этот список всех возможных строк:
allStrings :: [String]
allStrings = ...
Учитывая allStrings
, как мы можем создать еще один список всех возможных строк?
alsoAllStrings :: [String]
Давайте рассмотрим каждую возможную строку как префикс одного символа и суффикс строки
alsoAllStrings = [ c : s
Суффикс строки является либо пустой строкой, либо членом списка всех возможных строк.
| s <- "" : allStrings
Префикс, состоящий из одного символа, находится в 'a'
через 'z'
или '0'
через '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'] ]
Это называется corecursive data - данные, которые определяются в терминах самих себя.Попробуйте это в 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
определены и доступны для использования в качестве суффиксов.
Это очень похоже на распространенный хаскелизм определения чисел Фибоначчи ядро:
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
Не удивляйтесь, если corecursion займет некоторое время, чтобы обернуть вашу голову.Это стоит усилий, чтобы понять, хотя.