Подтвердить что ты не робот

Вычисление простых чисел в Scala: как работает этот код?

Итак, я потратил часы, пытаясь выяснить, как именно этот код производит простые числа.

lazy val ps: Stream[Int] = 2 #:: Stream.from(3).filter(i =>
   ps.takeWhile{j => j * j <= i}.forall{ k => i % k > 0});

Я использовал несколько printlns и т.д., но ничего не сделал яснее.

Вот что я думаю, что код делает:

/**
 * [2,3] 
 * 
 * takeWhile 2*2 <= 3 
 * takeWhile 2*2 <= 4 found match
 *      (4 % [2,3] > 1) return false.
 * takeWhile 2*2 <= 5 found match
 *      (5 % [2,3] > 1) return true 
 *          Add 5 to the list
 * takeWhile 2*2 <= 6 found match
 *      (6 % [2,3,5] > 1) return false
 * takeWhile 2*2 <= 7
 *      (7 % [2,3,5] > 1) return true
 *          Add 7 to the list
 */

Но если я изменил j*j в списке на 2 * 2, который, как я предполагал, будет работать точно так же, он вызывает ошибку stackoverflow.

Мне явно не хватает чего-то фундаментального здесь, и я действительно мог бы использовать кого-то, объясняющего это мне, как будто мне было пять лет.

Любая помощь будет принята с благодарностью.

4b9b3361

Ответ 1

Я не уверен, что поиск процедурного/императивного объяснения - лучший способ получить понимание здесь. Потоки основаны на функциональном программировании, и их лучше понять с этой точки зрения. Ключевыми аспектами данного определения являются:

  • Это лениво. Помимо первого элемента потока, ничего не вычисляется до тех пор, пока вы его не попросите. Если вы никогда не просите о 5-м премьер-счете, он никогда не будет вычислен.

  • Рекурсивный. Список простых чисел определяется в терминах самого себя.

  • Это бесконечно. Потоки имеют интересное свойство (потому что они ленивы), что они могут представлять последовательность с бесконечным числом элементов. Stream.from(3) - пример этого: он представляет список [3, 4, 5,...].

Посмотрим, можем ли мы понять, почему ваше определение вычисляет последовательность простых чисел.

Определение начинается с 2 #:: .... Это просто говорит о том, что первое число в последовательности 2 - достаточно просто до сих пор.

Следующая часть определяет остальные простые числа. Мы можем начать со всех чисел подсчета, начиная с 3 (Stream.from(3)), но нам, очевидно, нужно отфильтровать пучок этих чисел (т.е. Все композиты). Поэтому рассмотрим каждое число i. Если i не кратно меньшему простому числу, то i является простым. То есть i является простым, если для всех простых чисел k меньше i, i % k > 0. В Scala мы могли бы выразить это как

nums.filter(i => ps.takeWhile(k => k < i).forall(k => i % k > 0))

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

nums.filter(i => ps.takeWhile(k => k * k <= i).forall(k => i % k > 0))

Итак, мы получили ваше определение.

Теперь, если вам удалось попробовать первое определение (с k < i), вы бы обнаружили, что он не работает. Почему нет? Это связано с тем, что это рекурсивное определение.

Предположим, мы пытаемся решить, что произойдет после 2 в последовательности. Определение говорит нам сначала определить, принадлежит ли 3. Для этого рассмотрим список простых чисел до первого, больше или равно 3 (takeWhile(k => k < i)). Первый штрих равен 2, что меньше 3 - пока что так хорошо. Но мы еще не знаем второго праймера, поэтому нам нужно его вычислить. Хорошо, поэтому нам нужно сначала увидеть, принадлежит ли 3... BOOM!

* Очень легко видеть, что если число n является составным, тогда квадрат одного из его факторов должен быть меньше или равен n. Если n является составным, то по определению n == a * b, где 1 < a <= b < n (мы можем гарантировать a <= b только путем соответствующего обозначения двух факторов). Из a <= b следует, что a^2 <= a * b, поэтому следует, что a^2 <= n.

Ответ 2

Ваши объяснения в основном верны, вы допустили только две ошибки:

takeWhile не включает последний проверенный элемент:

scala> List(1,2,3).takeWhile(_<2)
res1: List[Int] = List(1)

Вы предполагаете, что ps всегда содержит только два и три, но поскольку Stream ленив, к нему можно добавить новые элементы. Фактически каждый раз, когда новый штрих найден, он добавляется к ps, и на следующем шаге takeWhile рассмотрит этот новый добавленный элемент. Здесь важно помнить, что хвост Stream вычисляется только тогда, когда он необходим, поэтому takeWhile не может видеть его до того, как forall будет оценено как true.

Помните об этих двух вещах, и вы должны придумать это:

ps = [2]
i = 3
  takeWhile
    2*2 <= 3 -> false
  forall on []
    -> true
ps = [2,3]
i = 4
  takeWhile
    2*2 <= 4 -> true
    3*3 <= 4 -> false
  forall on [2]
    4%2 > 0 -> false
ps = [2,3]
i = 5
  takeWhile
    2*2 <= 5 -> true
    3*3 <= 5 -> false
  forall on [2]
    5%2 > 0 -> true
ps = [2,3,5]
i = 6
...

Хотя эти шаги описывают поведение кода, это не совсем правильно, потому что не только добавление элементов в Stream является ленивым, но и каждой операцией на нем. Это означает, что когда вы вызываете xs.takeWhile(f) не все значения до момента, когда f является ложным, вычисляются сразу - они вычисляются, когда forall хочет их видеть (поскольку это единственная функция здесь, которая должна смотреть на все элементы до того, как он определенно может привести к истинному, для false он может прервать раньше). Здесь порядок вычислений, когда лень считается повсюду (пример касается только 9):

ps = [2,3,5,7]
i = 9
  takeWhile on 2
    2*2 <= 9 -> true
  forall on 2
    9%2 > 0 -> true
  takeWhile on 3
    3*3 <= 9 -> true
  forall on 3
    9%3 > 0 -> false
ps = [2,3,5,7]
i = 10
...

Поскольку forall прерывается, когда он вычисляет значение false, takeWhile не вычисляет оставшиеся возможные элементы.

Ответ 3

Этот код проще (для меня, по крайней мере) читать с некоторыми переменными, переименованными наводящим на размышления, как

lazy val ps: Stream[Int] = 2 #:: Stream.from(3).filter(i =>
   ps.takeWhile{p => p * p <= i}.forall{ p => i % p > 0});

Это читается слева направо, естественно, как

простые числа 2, а числа i от 3 до, что все простые числа p, квадрат которых не превышает i, не делят i равномерно (т.е. без какого-либо ненулевого остатка).

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

Единственная потенциальная проблема после этого - это время доступа к потоку ps по мере его определения. Как первый шаг, представьте себе, что у нас есть только какой-то поток простых чисел, предоставленный нам откуда-то, волшебным образом. Затем, посмотрев правду определения, убедитесь, что время доступа в порядке, то есть мы никогда не пытаемся получить доступ к областям ps до их определения; что сделало бы определение застрявшим, непродуктивным.

Я помню, где-то читал (не помню, где) что-то вроде следующего - разговор между учеником и волшебником,

  • ученик: какие числа являются первичными?
  • wizard: ну, знаете ли вы, какой номер является первым премьер?
  • s: да, это 2.
  • w: okay (быстро записывает 2 на листе бумаги). А как насчет следующего?
  • s: ну, следующий кандидат - 3. нам нужно проверить, делится ли он на любое число, квадрат которого не превышает его, но я еще не знаю, что такое простые числа!
  • w: не волнуйся, я отдам их тебе. Это волшебство, которое я знаю; В конце концов, я волшебник.
  • s: хорошо, так что такое первое простое число?
  • w: (смотрит на лист бумаги) 2.
  • s: отлично, поэтому его квадрат уже больше 3... ЭЙ, ты обманул!.....

Здесь псевдокод 1 перевод вашего кода, прочитанный частично справа налево, с некоторыми переменными, снова переименованными для ясности (используя p для "prime" ):

ps = 2 : filter (\i-> all (\p->rem i p > 0) (takeWhile (\p->p^2 <= i) ps)) [3..]

который также является

ps = 2 : [i | i <- [3..], and [rem i p > 0 | p <- takeWhile (\p->p^2 <= i) ps]]

который выглядит более визуально, используя список понятий. and проверяет, что все записи в списке логических элементов True (читайте | как "для", <- как "взяты из", , как "такие, что" и (\p-> ...) как "лямбда p" ).

Итак, вы видите, ps - это ленивый список из 2, а затем чисел i, взятых из потока [3,4,5,...], такого, что для всех p, взятых из ps такой, что p^2 <= i, верно, что i % p > 0. Это фактически оптимальный алгоритм пробного деления.:)

Здесь, конечно, тонкость: список ps открыт. Мы используем его, поскольку он "сглаживается" (это, конечно, потому что он ленив). Когда p берутся из ps, это потенциально может быть случай, когда мы закончим его конец, и в этом случае у нас будет неоплачиваемый расчет на наших руках ( "черная дыра" ). Это так происходит:) (и нужно & frasl; может быть доказано математически), что это невозможно с вышеприведенным определением. Таким образом, 2 помещается в ps безоговорочно, поэтому в нем есть что-то в этом роде.

Но если мы попытаемся "упростить",

bad = 2 : [i | i <- [3..], and [rem i p > 0 | p <- takeWhile (\p->p < i) bad]]

он перестает работать после создания только одного числа, 2: при рассмотрении 3 в качестве кандидата takeWhile (\p->p < 3) bad требует следующего номера в bad после 2, но там еще нет никаких чисел. Он "прыгает впереди себя".

Это "исправлено" с помощью

bad = 2 : [i | i <- [3..], and [rem i p > 0 | p <- [2..(i-1)] ]]

но это намного более медленный алгоритм деления, очень далекий от оптимального.

-

1 (Haskell на самом деле, это просто проще для меня таким образом:))