Haskell --› F#: сито Тернера

Я читал о различных алгоритмах просеивания, когда наткнулся на своего рода улучшенную версию решета Эратосфена, называемую решетом Эйлера. Согласно Википедии существует реализация немного другая версия идеи (называемая решетом Тернера) в Haskell.

Теперь я пытаюсь понять, что именно делает приведенный фрагмент кода, и я думаю, что у меня это получилось, но теперь я хотел перевести код на F # и понятия не имею, с чего начать. Меня больше всего беспокоит то, что, похоже, не существует функции для «вычитания» двух последовательностей.

Вот код:

import Data.OrdList (minus)

primes = euler [2..]
euler (p : xs) = p : euler (xs `minus` map (*p) (p : xs))

Как это реализовать на F#? Это вообще возможно?


person chrischu    schedule 24.02.2010    source источник
comment
Возможно, также представляет интерес: fsharpnews.blogspot.com/2010/02/sieve -of-eratosthenes.html   -  person kvb    schedule 24.02.2010
comment
... Или сито Аткина: blog.hoomla .se/post/Сито-оф-Аткина-в-Ф.aspx   -  person Johan Kullbom    schedule 24.02.2010
comment
Приведенные выше 2 ссылки - это не решето Тернера (или Эйлера), а решето Эратосфена. Хотя они довольно быстры до ограниченного диапазона простых чисел по сравнению с наивными реализациями решет, обе не намного быстрее, чем функциональные версии, несмотря на то, что они императивны. Версия Йохана Калбуна даже не так быстра, как он думает, поскольку он умножает простые числа на 10 миллионов, а не на первые 10 миллионов простых чисел, и первая ссылка разрывается примерно на первых 30 миллионах простых чисел. Попробуйте stackoverflow.com/a/17668738/549617 для функциональной версии с той же скоростью, что и приведенные выше ссылки.   -  person GordonBGood    schedule 01.11.2013


Ответы (4)


Если вы хотите вычислять такие вещи, как слияния/разности бесконечных списков, как это делает Haskell, на ум приходит тип LazyList (находящийся в пакете питания F#).

Это делает код очень подробным, как в приведенном ниже переводе:

#r "FSharp.PowerPack.dll"

//A lazy stream of numbers, starting from x
let rec numsFrom x = LazyList.consDelayed x (fun () -> numsFrom (x+1))

//subtracts L2 from L1, where L1 and L2 are both sorted(!) lazy lists
let rec lazyDiff L1 L2 =
    match L1,L2 with
        | LazyList.Cons(x1,xs1),LazyList.Cons(x2,xs2) when x1<x2 ->
            LazyList.consDelayed x1 (fun () -> lazyDiff xs1 L2)
        | LazyList.Cons(x1,xs1),LazyList.Cons(x2,xs2) when x1=x2 ->
            lazyDiff xs1 L2
        | LazyList.Cons(x1,xs1),LazyList.Cons(x2,xs2) when x1>x2 ->
            lazyDiff L1 xs2
        | _ -> failwith "Should not occur with infinite lists!"

let rec euler = function
    | LazyList.Cons(p,xs) as LL ->
        let remaining = lazyDiff xs (LazyList.map ((*) p) LL)
        LazyList.consDelayed p (fun () -> euler remaining)
    | _ -> failwith "Should be unpossible with infinite lists!"

let primes = euler (numsFrom 2)

с участием

> primes |> Seq.take 15 |> Seq.toList;;
val it : int list = [2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37; 41; 43; 47]

Обратите внимание, что я добавил два предложения failwith, чтобы компилятор не жаловался на неполное совпадение, даже если мы знаем, что все списки в вычислении (лениво) бесконечны.

person cfern    schedule 24.02.2010

Вы можете сделать это с помощью seq. И поскольку вы сделали minus, euler сам по себе такой же, как в Haskell.

let rec minus xs ys =
  seq {
    match Seq.isEmpty xs, Seq.isEmpty ys with
    | true,_ | _,true -> yield! xs
    | _ ->
       let x,y = Seq.head xs, Seq.head ys
       let xs',ys' = Seq.skip 1 xs, Seq.skip 1 ys
       match compare x y with
       | 0 -> yield! minus xs' ys'
       | 1 -> yield! minus xs ys'
       | _ -> yield x; yield! minus xs' ys
  }

let rec euler s =
  seq {
    let p = Seq.head s
    yield p
    yield! minus (Seq.skip 1 s) (Seq.map ((*) p) s) |> euler
  }

let primes = Seq.initInfinite ((+) 2) |> euler
person ssp    schedule 24.02.2010
comment
К сожалению, рекурсивные последовательности с использованием Seq.skip очень неэффективны. Хотя это элегантное решение, оно работает намного хуже, чем cfern. - person kvb; 24.02.2010
comment
Похоже, это связано с тем, что последовательности не имеют встроенной памяти, а использование Seq.cache, предложенное @toyvo, не работает, потому что вызывает переполнение стека, как и каждая новая последовательность, заканчивающаяся символом |› Seq .cache (хотя до этого момента он был намного быстрее) из-за того, что рекурсивные функции больше не являются хвостовыми рекурсивными. Преимущество использования LazyList для этого алгоритма заключается в том, что они поддерживают встроенную мемоизацию, и поэтому рекурсия хвостового вызова все еще работает. - person GordonBGood; 30.06.2013

С последовательностями вы становитесь намного более конкурентоспособным, сохраняя последовательность:

let rec euler s =
    seq {
        let s = Seq.cache s
        let p = Seq.head s
        yield p
        yield! minus (Seq.skip 1 s) (Seq.map ((*) p) s) |> euler
    }
person t0yv0    schedule 03.03.2010
comment
К сожалению, использование Seq.cache означает более быстрое, чем обычно, переполнение стека, поскольку новый кеш создается для каждого рекурсивного вызова/цикла функции Эйлера, что невозможно облегчить, поскольку, как написано, функция Эйлера получает совершенно другую последовательность каждый раз. вызов, возвращаемый функцией минус. - person GordonBGood; 21.06.2013

Во-первых, необходимо сказать, что решето Эйлера для простых чисел не является «улучшенной версией решета Эратосфена», поскольку его производительность во всех смыслах намного хуже любой версии решета Эратосфена: Haskell Wiki по алгоритмам простых чисел — Эйлер

Далее следует сказать, что код @cfem, использующий LazyList, является точным, хотя и многословным переводом версии решета Эйлера, которую вы дали, хотя ему не хватает небольшого улучшения просеивания только нечетных чисел по ссылке выше.

Следует отметить, что на самом деле нет никакого смысла в реализации решета Эйлера, так как это сложнее и медленнее, чем поиск простых чисел с помощью оптимизации пробного деления (TDO), поскольку деление выполняется только на найденные простые числа до квадратного корня из число-кандидат проверено на простое число согласно: Haskell Wiki по алгоритмам простых чисел — TDO.

Это сито Trial Division Optimized (TDO) может быть реализовано на F# с использованием LazyList (со ссылкой на FSharp.PowerPack.dll) следующим образом:

let primesTDO() =
  let rec oddprimes =
    let rec oddprimesFrom n =
      if oddprimes |> Seq.takeWhile (fun p -> p * p <= n) |> Seq.forall (fun p -> (n % p) <> 0)
      then LazyList.consDelayed n (fun() -> oddprimesFrom (n + 2))
      else oddprimesFrom (n + 2)
    LazyList.consDelayed 3 (fun() -> oddprimesFrom 5)
  LazyList.consDelayed 2 (fun () -> oddprimes)

Это может быть реализовано с использованием последовательностей в той же форме, что и:

let primesTDOS() =
  let rec oddprimes =
    let rec oddprimesFrom n =
      if oddprimes |> Seq.takeWhile (fun p -> p * p <= n) |> Seq.forall (fun p -> (n % p) <> 0)
      then seq { yield n; yield! oddprimesFrom (n + 2) }
      else oddprimesFrom (n + 2)
    seq { yield 3; yield! (oddprimesFrom 5) } |> Seq.cache
  seq { yield 2; yield! oddprimes }

Версия последовательности немного быстрее, чем версия LazyList, потому что она позволяет избежать некоторых накладных расходов при вызове, поскольку LazyList основан на кэшированных последовательностях. Оба используют внутренний объект, который представляет кэшированную копию простых чисел, найденных до сих пор, автоматически кэшируемых в случае LazyList и Seq.cache в случае последовательностей. Любой может найти первые 100 000 простых чисел примерно за две секунды.

Теперь решето Эйлера может иметь оптимизацию просеивания нечетных чисел и быть выражено с помощью LazyList следующим образом, с исключением одного случая совпадения из-за того, что параметры входного списка бесконечны, а сопоставление упрощено, как ну, я добавил оператор '^', чтобы сделать код более читабельным:

let primesE = //uses LazyList's from referenced FSharp.PowerPack.dll version 4.0.0.1
  let inline (^) a ll = LazyList.cons a (LazyList.delayed ll) //a consd function for readability
  let rec eulers xs =
    //subtracts ys from xs, where xs and ys are both sorted(!) infinite lazy lists
    let rec (-) xs ys =
      let x,xs',ys' = LazyList.head xs,LazyList.tail xs,LazyList.tail ys
      match compare x ( LazyList.head ys) with
        | 0 -> xs' - ys' // x == y
        | 1 -> xs - ys' // x > y
        | _ -> x^(fun()->(xs' - ys)) // must be x < y
    let p = LazyList.head xs
    p^(fun() -> (((LazyList.tail xs) - (LazyList.map ((*) p) xs)) |> eulers))
  let rec everyothernumFrom x = x^(fun() -> (everyothernumFrom (x + 2)))
  2^(fun() -> ((everyothernumFrom 3) |> eulers))

Однако следует отметить, что время вычисления 1899-го простого числа (16381) составляет около 0,2 и 0,16 секунды для простых чисел TDO и PrimesTDOS соответственно, в то время как при использовании этого простого числа E для ужасной производительности решета Эйлера при более чем 2,5 секунды в десять раз больше времени даже для этого небольшого диапазона. В дополнение к ужасной производительности, PrimeE не может даже вычислить простые числа до 3000 из-за еще большего использования памяти, поскольку он записывает быстро растущее количество отложенных функций выполнения с увеличением найденных простых чисел.

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

Таким образом, решето Эйлера, реализованное на любом языке, включая F#, является всего лишь интересным интеллектуальным упражнением и не имеет практического применения, поскольку оно намного медленнее и потребляет гораздо больше памяти, чем почти любое другое разумно оптимизированное решето.

person GordonBGood    schedule 21.06.2013
comment
ваш вывод может не выполняться в C/C++ для непрерывных сит на основе массивов (т.е. ограниченных). - person Will Ness; 21.06.2013
comment
также было бы интересно посмотреть, как слияние деревьев Eratosthenes работает в F #, или может ли оно быть реализовано вообще... (или слияние на скользящем сегменте массива...). Эратосфен должен быть быстрее ТД, может стоит попробовать. :) - person Will Ness; 21.06.2013
comment
@ Уилл Несс относительно вашего первого комментария, я не думаю, что можно императивно реализовать решето Эйлера, кроме как для ограниченного диапазона, и это становится таким же, как решето Эратосфена (SoE), потому что по определению решето Эйлера отменяет составные части всех оставшихся числа, начинающиеся с квадрата найденных простых чисел, что приведет к определению SoE, состоящему в том, что составные части будут сокращаться на кратные найденным промежуткам простых чисел: ссылка. Неэффективность решета Эйлера заключается в несвязанном определении. - person GordonBGood; 28.06.2013
comment
@Will Ness, о реализации гораздо более быстрых версий простых сит на основе слияния деревьев и карт, я работаю над этим и обнаружил, что нет ничего, что Haskell может сделать, чего не может F #, даже используя чистый функциональный код, хотя, возможно более многословно. SoE действительно будет быстрее, чем TD для больших диапазонов простых чисел, поскольку эти версии TD начинают выдыхаться примерно при 100 000 простых чисел и определенно при 1 000 000 простых чисел из-за времени O (n ^ 1,5). Unbounded Euler занимает время O(2) или хуже (из-за большого использования памяти), и ни одна истинная реализация SoE не будет хуже, чем TD со многими лучшими. - person GordonBGood; 28.06.2013
comment
да, неограниченное Эйлера ~ n^2 или хуже; кстати вот что-то похожее. :) На самом деле, есть есть более медленные реализации SoEr, чем даже неоптимальные TD (также этот). Пока он генерирует кратные путем подсчета, он истинен в моей книге. :) - person Will Ness; 28.06.2013
comment
@Will Ness, ваша первая ссылка (что-то похожее) ведет к версии EoSr (рекурсивной?) Ричарда Берда, которая действительно является алгоритмом EoS, но работает примерно на O (n ^ 1,5), как и оптимизированный TD. Я согласен с тем, что первая из медленных (EoS?) ссылок является истинным EoSr, поскольку она помечает все числа из найденного простого числа с приращением простого числа, продвигаясь вверх по оставшемуся списку один за другим, но это медленно по причинам, как указано. Алгоритм Python на самом деле ограничен, но все еще очень медленный, как вы указываете в своем ответе. Но мы отвлеклись, так как этот вопрос касается решета Эйлера. - person GordonBGood; 29.06.2013
comment
правильно; просто небольшое исправление, это не версия Ричарда Берда; Он намного проще, он не пытается избежать создания повторяющихся множителей, как и сито Эра. Но эта версия похожа на решето Эйлера в том, что они оба избегают генерации повторяющихся кратных — т. е. они генерируют/удаляют 45 только один раз (как 3*15), а Эр и Бёрд — дважды (как 9+36 и 25+20). :) - person Will Ness; 29.06.2013
comment
@ Уилл Несс, я исправлен, но теперь замечаю дополнительный код для устранения повторных составов, что также делает сито Эйлера. Проблема в том, что он добавляет алгоритму большую рекурсивность, чтобы уменьшить фактическое количество операций, выполняемых почти совсем. Как указано в Haskell: - person GordonBGood; 29.06.2013
comment
продолжение ... Haskell: primesEU = 2 : эйлеры [3,5..] где эйлеры (p:xs) = p : эйлеры (xs minus map (p*) (p:xs)) -- эратос (p:xs ) = p : eratos (xs minus [pp, pp+2*p..]) Можно видеть, что можно изменить строку преобразования карты в приведенном выше коде на: - person GordonBGood; 29.06.2013
comment
продолжение -- p^(fun() -› (((LazyList.tail xs) - (numFromBy (p*p) (p+p))) |› eulers)) с let rec numFromBy pi = p^(fun( ) -> (everyothernumFrom (x + i))), чтобы иметь гораздо лучшую производительность при использовании EoS, хотя это и плохо по сравнению с использованием некоторых алгоритмов. - person GordonBGood; 29.06.2013
comment
@Will Ness, дальнейшие обсуждения функциональных версий SoEr (r = recursive, что означает добавочные версии Site of Erotasthenes), вероятно, должны быть по другому вопросу в stackoverflow об алгоритмах EoS в F#. - person GordonBGood; 01.07.2013
comment
Я использовал SoEr для решета Эратосфена, так как E также может быть Эйлером. :) Кстати, map (p*) (p:xs) против [p*p,p*p+2*p..] - это как раз разница между Эйлером и Эратосфеном. Спасибо за ссылку. :) - person Will Ness; 01.07.2013