Как рекурсия встретила базовый случай Хаскелла - PullRequest
8 голосов
/ 21 марта 2019

Я пытаюсь понять этот фрагмент кода, который возвращает все возможные комбинации [a], переданные ему:

-- Infinite list of all combinations for a given value domain
allCombinations :: [a] -> [[a]]
allCombinations []     = [[]]
allCombinations values = [] : concatMap (\w -> map (:w) values)
                                        (allCombinations values)

Здесь я попробовал этот пример ввода:

ghci> take 7 (allCombinations [True,False])
[[],[True],[False],[True,True],[False,True],[True,False],[False,False]]

Здесь мне не кажется понятным, что рекурсия в конечном итоге останавливается и возвращает [ [ ] ], потому что функция allCombinations определенно не имеет указателя, который перемещается по списку при каждом вызове и когда он соответствует базовому случаю [ ], он возвращает [ [ ] ]. По моему мнению, она будет вызывать allCombinations функцию бесконечной и никогда не остановится сама по себе. Или, может быть, я что-то упустил?

С другой стороны, take возвращает только первые 7 элементы из final list после того, как все вычисления выполнены путем возврата после завершения рекурсивных вызовов. Так на самом деле, как рекурсия встретила базовый случай здесь?

Во-вторых, какова цель concatMap здесь, здесь мы также могли бы использовать функцию Map, чтобы просто применить функцию к списку, а внутри функции мы могли бы упорядочить список? Что на самом деле concatMap делает здесь. Из определения он concatMap говорит нам, что сначала сопоставляет функцию, затем объединяет списки, где, как я вижу, мы уже делаем это внутри функции здесь?

Любой ценный вклад будет оценен?

Ответы [ 3 ]

8 голосов
/ 21 марта 2019

Краткий ответ: никогда не будет соответствовать базовому случаю.

Однако, это не нужно. Базовый случай чаще всего необходим для остановки рекурсии, однако здесь вы хотите вернуть бесконечный список, поэтому останавливать его не нужно.

С другой стороны, эта функция сломалась бы, если вы попытаетесь взять более 1 элемента из allCombination [] - взгляните на ответ @ robin, чтобы лучше понять, почему. Это единственная причина, по которой вы видите здесь базовый случай.

Принцип работы основной функции заключается в том, что она начинается с пустого списка, а затем добавляет в начале каждый элемент списка аргументов. (:w) делает это рекурсивно. Однако одна только эта лямбда вернула бы бесконечно вложенный список. I.e: [],[[True],[False]],[[[True,True],[True,False] и т. Д. Concatmap удаляет внешний список на каждом шаге, и, поскольку он вызывается рекурсивно, он возвращает только один список списков в конце. Это может быть сложная концепция для понимания, поэтому ищите другой пример использования concatMap и попытайтесь понять, как они работают и почему одного map будет недостаточно.

Это, очевидно, работает только из-за ленивой оценки Haskell. Точно так же вы знаете, что в foldr вам нужно передать ему базовый случай, однако, когда ваша функция должна принимать только бесконечные списки, вы можете иметь undefined в качестве базового случая, чтобы сделать более ясным, что конечные списки не должны использоваться. Например, foldr f undefined может использоваться вместо foldr f []

4 голосов
/ 22 марта 2019

Это не базовый случай, а специальный случай, и это не рекурсия, а corecursion , (*) который никогда не останавливается.

Возможно, будет легче следовать следующей переформулировке:

allCombs :: [t] -> [[t]]
--        [1,2] -> [[]] ++ [1:[],2:[]] ++ [1:[1],2:[1],1:[2],2:[2]] ++ ...
allCombs vals = concat . iterate (cons vals) $ [[]]
    where
    cons :: [t] -> [[t]] -> [[t]]
    cons vals combs = concat [ [v : comb | v    <- vals]
                               |           comb <- combs ]

-- iterate   :: (a     -> a    ) -> a     -> [a]
-- cons vals ::  [[t]] -> [[t]]
-- iterate (cons vals)           :: [[t]] -> [[[t]]]
-- concat    ::                              [[ a ]] -> [ a ]
-- concat . iterate (cons vals)                      :: [[t]]

Выглядит иначе, делает то же самое. Не только дает те же результаты, но фактически делает то же самое для их получения. (*) concat - это то же самое concat, вам просто нужно наклонить Пройдите немного, чтобы увидеть это.

Это также показывает, почему здесь нужен concat. Каждый step = cons vals создает новую серию комбинаций, длина которых увеличивается на 1 в каждом step приложении, и concat склеивает их все вместе в один список результатов.

Длина каждой партии - это длина предыдущей партии, умноженная на n, где n - это длина vals. Это также показывает необходимость особого случая vals == [], т.е. случая n == 0: 0*x == 0, поэтому длина каждой новой партии равна 0, поэтому попытка получить еще одно значение из результатов никогда не приведет к результат, то есть войти в бесконечный цикл. Говорят, что в этот момент функция становится непроизводительной .

Кстати, cons почти такой же, как

                   == concat [ [v : comb | comb <- combs]
                               |           v    <- vals  ]
                   == liftA2 (:) vals combs

liftA2 :: Applicative f => (a -> b -> r) -> f a -> f b -> f r

Так что, если вам не важен внутренний порядок результатов каждого шага (но обратите внимание на важное предупреждение в нижней части поста), это можно просто кодировать как

allCombsA :: [t] -> [[t]]
--         [1,2] -> [[]] ++ [1:[],2:[]] ++ [1:[1],1:[2],2:[1],2:[2]] ++ ...
allCombsA   []   =  [[]]
allCombsA  vals  =  concat . iterate (liftA2 (:) vals) $ [[]]

(*) на самом деле, это относится к немного измененной версии,

allCombsRes vals = res
             where res = [] : concatMap (\w -> map (: w) vals)
                              res
-- or:
allCombsRes vals = fix $ ([] :) . concatMap (\w -> map (: w) vals)
--  where
--  fix g = x where x = g x     -- in Data.Function

Или в псевдокоде:

 Produce a sequence of values `res` by
      FIRST producing `[]`, AND THEN
      from each produced value `w` in `res`, 
          produce a batch of new values `[v : w | v <- vals]`
          and splice them into the output sequence
               (by using  `concat`)

Таким образом, список res создается ядром, начиная с его начальной точки, [], производя следующие его элементы на основе предыдущих (-ых) - либо пакетами, как в версии на основе iterate, или один за другим, как здесь, принимая вход с помощью обратного указателя в результаты, ранее произведенные (принимая его вывод в качестве входных данных, как говорится - что, конечно, немного обманчиво, так как мы принимаем его с более медленной скоростью, чем производим, иначе процесс перестанет быть производительным , как уже упоминалось выше.

Но. Иногда может быть выгодно производить ввод через рекурсивные вызовы, создавая во время выполнения последовательность функций, каждая из которых передает свой вывод по цепочке вызывающей стороне. Тем не менее, поток данных идет вверх, в отличие от обычной рекурсии, которая сначала идет вниз к базовому случаю.

Только что упомянутое преимущество связано с сохранением памяти. Corecursive allCombsRes как бы хранит обратный указатель в последовательности, которую он сам производит, и поэтому последовательность не может быть собрана мусором на лету.

Но цепочка производителей потоков, неявно созданная вашей исходной версией во время выполнения, означает, что каждый из них может собираться мусором на лету, так как n = length vals новые элементы создаются из каждого последующего элемента таким образом, общий процесс становится эквивалентным просто k = ceiling $ logBase n i вложенных циклов каждый с O (1) пробелом для получения i -го элемента последовательности .

Это намного лучше, чем O (n) требования к памяти для corecursive / value-recursive allCombsRes, который фактически сохраняет обратный указатель на свой выходной сигнал при i/n позиция. И на практике логарифмическое пространство, скорее всего, будет рассматриваться как более или менее O (1) пространство.

Это преимущество имеет место только с порядком генерации, как в вашей версии, т.е. как в cons vals, а не liftA2 (:) vals, который должен возвращаться к началу своей входной последовательности combs (для каждого нового v в vals), что, таким образом, должно быть сохранено, поэтому мы можем с уверенностью сказать, что формулировка в вашем вопросе довольно изобретательна.

И если мы после бессмысленной переформулировки - как бессмысленно может время от времени быть светящейся - это

allCombsY values =  _Y $ ([] :) . concatMap (\w -> map (: w) values)
    where
    _Y g = g (_Y g)      -- no-sharing fixpoint combinator

Таким образом, код гораздо проще понять в fix -использующей формулировке, и тогда мы просто переключаем fix с семантически эквивалентным _Y для эффективности, получая (эквивалент) исходного кода из вопроса.

Приведенные выше утверждения о поведении требований к пространству легко проверяются .Я еще этого не сделал.

См. Также:

4 голосов
/ 21 марта 2019

@ Лоренцо уже объяснил ключевой момент - что рекурсия на самом деле никогда не заканчивается, и поэтому это создает бесконечный список, из которого вы все равно можете взять любое конечное число элементов из-за лени Хаскелла.Но я думаю, что будет полезно рассказать немного подробнее об этой конкретной функции и о том, как она работает.

Во-первых, [] : в начале определения говорит вам, что первый элемент будет всегда быть [].Это, конечно, единственный способ создать список из 0 элементов из элементов values.Остальная часть списка - concatMap (\w -> map (:w) values) (allCombinations values).

concatMap f, поскольку вы просто наблюдаете композицию concat . (map f): она применяет данную функцию к каждому элементу списка и объединяет результаты вместе.Здесь функция (\w -> map (:w) values) берет список и создает список списков, добавляя каждый элемент values к этому списку.Например, если values == [1,2], то:

(\w -> map (:w) values) [1,2] == [[1,1,2], [2,1,2]]

, если мы map будем работать со списком списков, таким как

[[], [1], [2]]

, то получим (все еще с values as [1,2]):

[[[1], [2]], [[1,1], [2,1]], [[1,2], [2,2]]] 

Это, конечно, список списков, но тогда к нам приходит на помощь часть concat concatMap, сглаживающая самый внешний слой, ив результате получается список списков:

[[1], [2], [1,1], [2,1], [1,2], [2,2]]     

Я надеюсь, вы могли заметить, что список списков, с которого я начинал, не был произвольным.[[], [1], [2]] - список всех комбинаций размера 0 или 1 из начального списка [1,2].На самом деле это первые три элемента allCombinations [1,2].

. Вспомните, что все, что мы знаем «наверняка», глядя на определение, это то, что первым элементом этого списка будет [].А остальная часть списка - concatMap (\w -> map (:w) [1,2]) (allCombinations [1,2]).Следующим шагом будет расширение рекурсивной части этого как [] : concatMap (\w -> map (:w) [1,2]) (allCombinations [1,2]).Внешний concatMap тогда может видеть, что заголовок списка, над которым он отображается, - [] - создание списка, начинающегося с [1], [2] и продолжающегося с результатами добавления 1, а затем 2 к другим элементам - независимо от того,они есть.Но мы только что увидели, что следующие 2 элемента на самом деле [1] и [2].В итоге мы получим

allCombinations [1,2] == [] : [1] : [2] : concatMap (\w -> map (:w) values) [1,2] (tail (allCombinations [1,2]))

(tail строго не вызывается в процессе оценки, вместо этого это делается с помощью сопоставления с образцом - я пытаюсь объяснить больше словами, чем явной подтасовкой через равенства),

И, глядя на это, мы знаем, что хвост - [1] : [2] : concatMap ....Ключевым моментом является то, что на каждом этапе процесса мы точно знаем, что представляют собой первые несколько элементов списка - и это все списки из 0 элементов со значениями, взятыми из values, за которыми следуют все 1-списки элементов с этими значениями, затем все списки из 2 элементов и т. д.После того, как вы начали, процесс должен продолжаться, потому что функция, переданная в concatMap, гарантирует, что мы просто получим списки, полученные при взятии каждого сгенерированного списка, и добавив каждый элемент values в начало.

Если вы все еще смущены этим, посмотрите, как вычислить числа Фибоначчи в Хаскеле.Классический способ получить бесконечный список всех чисел Фибоначчи:

fib = 1 : 1 : zipWith (+) fib (tail fib)

Это немного проще для понимания, чем пример allCombinations, но основывается, по сути, на одном и том же - определение списка исключительно всам по себе, но с использованием ленивых вычислений для постепенного создания необходимого списка в соответствии с простым правилом.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...