Изучение Haskell: круговая программа - помогите объяснить - PullRequest
9 голосов
/ 17 августа 2010

В настоящее время я изучаю книгу Дуца и Ван Эйка «Дорога к логике, математике и программированию на Хаскеле». До этой книги я никогда не сталкивался с каким-либо функциональным языком программирования, так что имейте это в виду.

Еще в начале книги приводится следующий код для теста на простоту:

ldp :: Integer -> Integer
ldp n = ldpf primes1 n

ldpf :: [Integer] -> Integer -> Integer
ldpf (p:ps) n | rem n p == 0 = p 
              | p^2 > n      = n
              | otherwise    = ldpf ps n

primes1 :: [Integer]
primes1 = 2 : filter prime [3..]

prime :: Integer -> Bool
prime n | n < 1     = error "not a positive integer"
        | n == 1    = False 
        | otherwise = ldp n == n

Существует, казалось бы, циклическое программирование, в котором ldp вызывает primes1, который вызывает prime, и снова вызывает ldp. Хотя книга предлагает это в качестве объяснения того, почему программа выполняется и завершается:

ldp вызывает primes1, список простых чисел. Это первая иллюстрация «ленивого списка». Список называется «ленивым», потому что мы вычисляем только ту часть списка, которая нам нужна для дальнейшей обработки. Чтобы определить простые числа1, нам нужен тест на простоту, но этот тест сам по себе определяется в терминах функции LD, которая, в свою очередь, относится к простым числам1. Кажется, мы бегаем по кругу. Этот круг можно сделать не порочным, избегая критерия примитивности для 2. Если задано, что 2 простое, то мы можем использовать примитивность 2 в проверке LD, что 3 простое, и так далее, и мы находимся и работает

Хотя я думаю, что понимаю это объяснение, я был бы очень признателен, если бы кто-то мог объяснить в терминах мирян:

  1. Что такое "ленивый список" и как он применяется в этом контексте?
  2. Как знание, что 2 простое, позволяет программе быть не порочной?

Любые ответы приветствуются.

Ответы [ 3 ]

9 голосов
/ 18 августа 2010

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

Мы могли бы сделать:

print $ take 1 primes1

Здесь нет проблем.Мы можем принять первое значение primes1, не обращая внимания ни на один из рекурсивного кода, он явно указан как 2. Как насчет:

print $ take 3 primes1

Давайте попробуем расширить primes1, чтобы получить некоторые значения.Чтобы эти выражения были управляемыми, я теперь игнорирую части print и take, но помните, что мы делаем эту работу только из-за них.primes1 is:

primes1 = 2 : filter prime [3..]

У нас есть первое значение, и первым шагом к нашему второму является расширение фильтра.Если бы это был строгий язык, мы бы сначала попытались расширить [3 ..] и застрять.Возможное определение фильтра:

filter f (x:xs) = if f x then x : filter f xs else filter f xs

, что дает:

primes1 = 2 : if prime 3 then 3 : filter prime [4..] else filter prime [4..]

Наш следующий шаг должен выяснить, является ли prime 3 истинным или ложным, поэтому давайте расширим его, используянаши определения prime, ldp и ldpf.

primes1 = 2 : if ldp 3 == 3 then 3 : filter prime [4..] else filter prime [4..]
primes1 = 2 : if ldpf primes1 3 == 3 then 3 : filter prime [4..] else filter prime [4..]

Теперь все становится интересным, primes1 ссылается на себя, а ldpf требуется первое значение для его вычисления.К счастью, мы можем легко получить первое значение.

primes1 = 2 : if ldpf (2 : tail primes) 3 == 3 then 3 : filter prime [4..] else filter prime [4..]

Теперь мы применяем защитные предложения в ldpf и находим 2^2 > 3, поэтому ldpf (2 : tail primes) 3 = 3.

primes1 = 2 : if 3 == 3 then 3 : filter prime [4..] else filter prime [4..]
primes1 = 2 : 3 : filter prime [4..] 

Мы сейчасесть наше второе значение.Обратите внимание, что правая часть нашей оценки никогда не становилась особенно большой и что нам никогда не требовалось очень далеко следовать рекурсивному циклу.

primes1 = 2 : 3 : if prime 4 then 4 : filter prime [5..] else filter prime [5..]
primes1 = 2 : 3 : if ldp 4 == 4 then 4 : filter prime [5..] else filter prime [5..]
primes1 = 2 : 3 : if ldpf primes1 4 == 4 then 4 : filter prime [5..] else filter prime [5..]
primes1 = 2 : 3 : if ldpf (2 : tail primes1) 4 == 4 then 4 : filter prime [5..] else filter prime [5..]

Мы используем защитное предложение rem 4 2 == 0, поэтому мы получаем 2 здесь.

primes1 = 2 : 3 : if 2 == 4 then 4 : filter prime [5..] else filter prime [5..]
primes1 = 2 : 3 : filter prime [5..]
primes1 = 2 : 3 : if prime 5 then 5 : filter prime [6..] else filter prime [6..]
primes1 = 2 : 3 : if ldp 5 == 5 then 5 : filter prime [6..] else filter prime [6..]
primes1 = 2 : 3 : if ldpf primes1 5 == 5 then 5 : filter prime [6..] else filter prime [6..]
primes1 = 2 : 3 : if ldpf (2 : tail primes) 5 == 5 then 5 : filter prime [6..] else filter prime [6..]

Нет защитных совпадений, поэтому мы повторяем:

primes1 = 2 : 3 : if ldpf (tail primes) 5 == 5 then 5 : filter prime [6..] else filter prime [6..]
primes1 = 2 : 3 : if ldpf (3 : tail (tail primes)) 5 == 5 then 5 : filter prime [6..] else filter prime [6..]

Теперь 3^2 > 5, так что выражение равно 5.

primes1 = 2 : 3 : if 5 == 5 then 5 : filter prime [6..] else filter prime [6..]
primes1 = 2 : 3 : 5 : filter prime [6..]

Нам нужно только три значенияТаким образом, оценка может остановиться.

Итак, теперь, чтобы ответить на ваши вопросы.Ленивый список - это тот, который требует от нас только оценки того, что нам нужно, и 2 помогает, потому что он гарантирует, что когда мы достигнем рекурсивного шага, у нас всегда будет достаточно рассчитанных значений.(Представьте, что вы расширили бы это выражение, если бы мы не знали 2, мы бы очень быстро застряли в цикле.)

6 голосов
/ 17 августа 2010

По порядку:

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

Я не уверен, что означает «не порочный», но я думаю, вы обнаружите, что имея значение »2 "как известное простое число обеспечивает базовый случай для рекурсии, т. Е. Обеспечивает конкретный ввод, после которого программа завершается.Написание рекурсивной функции (или даже набора взаимно рекурсивных функций) обычно включает уменьшение некоторого входного значения к этому базовому случаю на последовательных этапах применения.

Для справки, фрагменты программы этой формы обычно называют взаимно рекурсивный .Термин «круговая ссылка» - это не тот термин, который вы действительно применили бы в этом случае.

3 голосов
/ 18 августа 2010

Одной из отличительных черт Haskell является его лень. Списки являются лишь частью этой истории. По сути, Haskell никогда не выполняет вычисления any до:

  1. значение расчета требуется для вычисления чего-то еще, требуется до ...
  2. вы сказали Haskell вычислить что-то раньше, чем могло бы.

Таким образом, функция primes1 создает список значений Integer, но не производит больше, чем необходимо для полного расчета. Тем не менее, если вы определили это так:

primes1 :: [Integer]
primes1 = filter prime [2..]

у тебя будут проблемы. primes1 будет пытаться сгенерировать первое значение в его последовательности путем (косвенной) оценки prime 2, что будет оценивать ldp 2, что будет запрашивать первое значение, полученное primes1 ... presto бесконечный цикл! *

Прямо возвращая 2 в качестве первого значения последовательности, сгенерированной primes1, вы избегаете бесконечного цикла. Для каждого последующего значения, сгенерированного в последовательности, primes1 уже сгенерировало предыдущее значение, которое будет оцениваться с помощью ldp как часть расчета последующего значения.

...