Вычисление простых чисел в 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.

Я, очевидно, упускаю что-то фундаментальное здесь, и может действительно пусть кто-нибудь объяснит мне это, как будто мне пять лет.

любая помощь была бы весьма признательна.

3 ответов


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

  1. это лень. Кроме первого элемента в потоке, ничего не вычисляется, пока вы не попросите его. Если вы никогда не попросите 5th prime, это никогда не будет вычисленный.

  2. это рекурсивные. Список простых чисел определяется сам по себе.

  3. это бесконечный. У потоков есть интересное свойство (потому что они ленивы), что они могут представлять последовательность с бесконечным количеством элементов. 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 ... Бум!

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


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

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 is false вычисляются при один раз-они вычисляются, когда forall хочет их увидеть (потому что это единственная функция здесь, которая должна смотреть на все элементы, прежде чем она определенно может привести к true, для 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 не вычисляет оставшиеся возможные элементы.


этот код легче (для меня, по крайней мере) читать с некоторыми переменными, переименованными намеком, as

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

это слева направо вполне естественно, а

простых are 2, и эти цифры i С 3, что все на простых p площадь которого не превышает i, do не разделить i равномерно (т. е. без какого-либо ненулевого остатка).

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

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

я помню где-то читал (не помню где) что-то вроде следующего-разговор между студентом и мастера

  • студент: какие числа являются простыми?
  • :
ну, вы знаете, какое число является первым простым?

  • s: да, это 2.
  • w: хорошо (быстро записывает 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-> ...) как "лямда - of p").

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

    здесь, конечно, есть тонкость: список ps открытый финал. Мы используем его как "плоть" (это, конечно, потому, что он ленив). Когда ps взяты из ps, потенциально это может быть случай, когда мы пробегаем мимо его конца, и в этом случае у нас на руках будет не заканчивающийся расчет (a"черная дыра"). Так уж получилось :) (и нужно ⁄ можно доказать математически), что это невозможно с вышеуказанным определением. Итак, 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 (Хаскелл на самом деле, мне так проще:))