Случайный поворот быстрой сортировки в Haskell - PullRequest
4 голосов
/ 08 марта 2011

Возможно ли реализовать быструю сортировку в Haskell (с RANDOM-PIVOT), которая все еще имеет простую подпись Ord a => [a]->[a]?

Я начинаю понимать монады, и на данный момент явид интерпретации монад как нечто вроде «шаблона команды», который прекрасно работает для ввода-вывода.

Итак, я понимаю, что функция, которая возвращает случайное число, должна фактически возвращать монадическое значение, такое как IO, потому что в противном случае она нарушит прозрачность ссылок.Я также понимаю, что не должно быть никакого способа «извлечь» случайное целое число из возвращенного монадического значения, потому что в противном случае это опять-таки нарушило бы прозрачность ссылок.

Но, тем не менее, я все же думаю, что оно должноможно реализовать «чистую» функцию быстрой сортировки [a]->[a], даже если она использует случайный поворот, потому что она прозрачна по ссылкам.С моей точки зрения, случайный стержень - это просто деталь реализации, и он не должен изменять сигнатуру функции

OBS: меня не интересует конкретная проблема быстрой сортировки (поэтому я не хочузвучит грубо, но я не ищу «использовать слияние» или «случайный поворот не повышает производительность на практике» ответы на некоторые вопросы) Мне на самом деле интересно, какреализовать «чистую» функцию, которая использует «нечистые» функции внутри нее, в таких случаях, как быстрая сортировка, где я могу заверить, что функция на самом деле является чистой.

Быстрая сортировка - только хороший пример.

Ответы [ 4 ]

7 голосов
/ 09 марта 2011

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

карта a <карта b, если номинальная стоимость меньше, но если вы должны были оценить логические значения: </p>

  4 spades < 4 hearts (false)
  4 hearts < 4 spades (false)
  4 hearts = 4 spades (false)

В этом случае выбор точек поворота определитокончательный заказ карт.Точно так же

для такой функции, как

a = get random integer  
b = a + 3
print b 

, определяется с помощью a.Если вы случайно выбираете что-то, то ваши вычисления являются или могут быть недетерминированными.

4 голосов
/ 09 марта 2011

ОК, проверьте это.

Выберите части, скопированные из хэшируемой упаковки , и прагмы Voodoo Magic Language

{-# LANGUAGE FlexibleInstances, UndecidableInstances, NoMonomorphismRestriction, OverlappingInstances #-}

import System.Random (mkStdGen, next, split)
import Data.List (foldl')
import Data.Bits (shiftL, xor)

class Hashable a where
    hash :: a -> Int

instance (Integral a) => Hashable a where
    hash = fromIntegral

instance Hashable Char where
    hash = fromEnum

instance (Hashable a) => Hashable [a] where
    hash = foldl' combine 0 . map hash

-- ask the authors of the hashable package about this if interested
combine h1 h2 = (h1 + h1 `shiftL` 5) `xor` h2

ОК, теперь мы можем взять список чего угодно Hashable и превратить его в Int. Я предоставил здесь экземпляры Char и Integral a, все больше и больше экземпляров находятся в хэшируемом пакете, который также позволяет солить и прочее.

Это все только для того, чтобы мы могли создать генератор чисел.

genFromHashable = mkStdGen . hash

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

qSortByGen _ _ [] = []
qSortByGen g f xs = qSortByGen g'' f l ++ mid ++ qSortByGen g''' f r
    where (l, mid, r) = partition (`f` pivot) xs
          pivot = xs !! (pivotLoc `mod` length xs)
          (pivotLoc, g') = next g
          (g'', g''') = split g'

partition f = foldl' step ([],[],[])
    where step (l,mid,r) x = case f x of
              LT -> (x:l,mid,r)
              EQ -> (l,x:mid,r)
              GT -> (l,mid,x:r)

Библиотечные функции : next захватывает Int из генератора и производит новый генератор. split разветвляет генератор на два разных генератора.

Мои функции : partition использует f :: a -> Ordering, чтобы разбить список на три списка. Если вы знаете сгибы, это должно быть совершенно ясно. (Обратите внимание, что он не сохраняет первоначальный порядок элементов в подсписках; он меняет их. Использование сгибателя может исправить это, если это будет проблемой.) qSortByGen работает так же, как я говорил ранее: обратитесь к генератору за сводной информацией, разбить список, разбить генератор для использования в двух рекурсивных вызовах, рекурсивно отсортировать левую и правую стороны и объединить все вместе.

Удобные функции легко составить здесь

qSortBy f xs = qSortByGen (genFromHashable xs) f xs
qSort = qSortBy compare

Обратите внимание на подпись окончательной функции.

ghci> :t qSort
qSort :: (Ord a, Hashable a) => [a] -> [a]

Тип в списке должен содержать как Hashable, так и Ord. Это «чистая» функция, о которой вы просили, с одним логическим добавленным требованием. Более общие функции менее ограничены в своих требованиях.

ghci> :t qSortBy
qSortBy :: (Hashable a) => (a -> a -> Ordering) -> [a] -> [a]
ghci> :t qSortByGen
qSortByGen
  :: (System.Random.RandomGen t) =>
     t -> (a -> a -> Ordering) -> [a] -> [a]

Заключительные замечания

qSort будет вести себя одинаково для всех входов. «Случайный» выбор сводки есть. на самом деле детерминированный . Но он затеняется хэшированием списка и последующим заполнением генератора случайных чисел, что делает его «случайным» для меня. ;)

qSort также работает только для списков длиной менее maxBound :: Int, что, по словам ghci, составляет 9 223 372 036 854 775 807. Я думал, что будет проблема с отрицательными индексами, но в моем специальном тестировании я еще не сталкивался с этим.


Или, вы можете просто жить с монадой IO для "истинной" случайности.

qSortIO xs = do g <- getStdGen -- add getStdGen to your imports
                return $ qSortByGen g compare xs


ghci> :t qSortIO
qSortIO :: (Ord a) => [a] -> IO [a]
ghci> qSortIO "Hello world"
" Hdellloorw"
ghci> qSort "Hello world"
" Hdellloorw"
3 голосов
/ 08 марта 2011

В тех случаях, когда вы знаете, что функция прозрачна по ссылкам, но не можете доказать это компилятору, вы можете использовать функцию unsafePerformIO :: IO a -> a из модуля Data.Unsafe.

ДляНапример, вы можете использовать unsafePerformIO, чтобы получить начальное случайное состояние, а затем делать что-либо, используя только это состояние.

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

2 голосов
/ 08 марта 2011

Haskell предоставляет монаду ST для выполнения нереференциально прозрачных действий с ссылочно прозрачным результатом.

Обратите внимание, что не обеспечивает ссылочную прозрачность;это просто гарантирует, что потенциально нереференциально прозрачное временное состояние не может просочиться.Ничто не может помешать вам вернуть обработанные чистые входные данные, которые были преобразованы невоспроизводимым способом.Лучше всего реализовать одну и ту же вещь как в ST, так и в чистом виде и использовать QuickCheck для сравнения их на случайных входах.

...