Безопасно ли чередовать ручное прохождение состояния realWorld # с произвольной монадой? - PullRequest
0 голосов
/ 14 декабря 2018

Рассмотрим эту функцию, которая генерирует список для произвольного Monad:

generateListM :: Monad m => Int -> (Int -> m a) -> m [a]
generateListM sz f = go 0
  where go i | i < sz = do x <- f i
                           xs <- go (i + 1)
                           return (x:xs)
             | otherwise = pure []

Возможно, реализация не идеальна, но она представлена ​​здесь исключительно для демонстрации желаемого эффекта, что довольно просто.,Например, если монада является списком, получите список списков:

λ> generateListM 3 (\i -> [0 :: Int64 .. fromIntegral i])
[[0,0,0],[0,0,1],[0,0,2],[0,1,0],[0,1,1],[0,1,2]]

Что я хотел бы сделать, это добиться того же эффекта, но для ByteArray вместо списка.Оказывается, это гораздо сложнее, чем я думал, когда впервые наткнулся на эту проблему.Конечная цель - использовать этот генератор для реализации mapM в massiv , но это не главное.

Подход, который требует наименьших усилий, заключается в использовании функции generateM из vector package при выполнении небольшого ручного преобразования.Но, как оказалось, есть способ добиться увеличения производительности по меньшей мере в два раза с помощью этого небольшого хитрого способа обработки маркера состояния вручную и чередования его с монадой:

{-# LANGUAGE MagicHash           #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UnboxedTuples       #-}
import           Data.Primitive.ByteArray
import           Data.Primitive.Types
import qualified Data.Vector.Primitive    as VP
import           GHC.Int
import           GHC.Magic
import           GHC.Prim

-- | Can't `return` unlifted types, so we need a wrapper for the state and MutableByteArray
data MutableByteArrayState s = MutableByteArrayState !(State# s) !(MutableByteArray# s)

generatePrimM :: forall m a . (Prim a, Monad m) => Int -> (Int -> m a) -> m (VP.Vector a)
generatePrimM (I# sz#) f =
  runRW# $ \s0# -> do
    let go i# = do
          case i# <# sz# of
            0# ->
              case newByteArray# (sz# *# sizeOf# (undefined :: a)) (noDuplicate# s0#) of
                (# s1#, mba# #) -> return (MutableByteArrayState s1# mba#)
            _ -> do
              res <- f (I# i#)
              MutableByteArrayState si# mba# <- go (i# +# 1#)
              return (MutableByteArrayState (writeByteArray# mba# i# res si#) mba#)
    MutableByteArrayState s# mba# <- go 0#
    case unsafeFreezeByteArray# mba# s# of
      (# _, ba# #) -> return (VP.Vector 0 (I# sz#) (ByteArray ba#))

Мы можем использовать еготаким же образом, как и раньше, за исключением того, что теперь мы получим примитив Vector, который поддерживается ByteArray, что мне действительно нужно:

λ> generatePrimM 3 (\i -> [0 :: Int64 .. fromIntegral i])
[[0,0,0],[0,0,1],[0,0,2],[0,1,0],[0,1,1],[0,1,2]]

Это, кажется, работает отлично, выполняетхорошо для ghc версий 8.0 и 8.2, за исключением того, что в 8.4 и 8.6 есть регрессия, но эта проблема является ортогональной.

Наконец, я перехожу к актуальному вопросу.Этот подход действительно безопасен?Есть ли какой-нибудь крайний случай, о котором я не знаю, который может укусить меня позже?Любые другие предложения или мнения приветствуются также в отношении вышеуказанной функции.

PS.m не нужно ограничивать Monad, Applicative будет работать просто отлично, но пример немного яснее, когда он представлен с синтаксисом do.

1 Ответ

0 голосов
/ 24 января 2019

TLDR; Из того, что я до сих пор собирал, кажется, что это безопасный способ генерировать примитив Vector способом, который я первоначально предложил.Более того, использование noDuplicate# на самом деле не является необходимым, поскольку все операции являются идемпотентными, и порядок операций не будет влиять на полученный массив (ы).

Раскрытие информации Прошло больше года с тех пор, как я впервые подумал об этой проблеме.Только в прошлом месяце я попытался вернуться к нему.Причина, по которой я это говорю, заключается в том, что, проверяя пакет примитив , я заметил новый модуль Data.Primitive.PrimArray.Как упомянуто в комментариях @chi, на самом деле нет необходимости опускаться до низкоуровневых примитивов, чтобы найти решение, поскольку оно может уже существовать.Который содержит именно функцию generatePrimArrayA , которая была именно тем, что я искал (немного упрощенная копия исходного кода):

newtype STA a = STA {_runSTA :: forall s. MutableByteArray# s -> ST s (PrimArray a)}

runSTA :: forall a. Prim a => Int -> STA a -> PrimArray a
runSTA !sz =
  \(STA m) -> runST $ newPrimArray sz >>= \(ar :: MutablePrimArray s a) -> m (unMutablePrimArray ar)

generatePrimArrayA :: (Applicative f, Prim a) => Int -> (Int -> f a) -> f (PrimArray a)
generatePrimArrayA len f =
  let go !i
        | i == len = pure $ STA $ \mary -> unsafeFreezePrimArray (MutablePrimArray mary)
        | otherwise =
          liftA2
            (\b (STA m) -> STA $ \mary -> writePrimArray (MutablePrimArray mary) i b >> m mary)
            (f i)
            (go (i + 1))
   in runSTA len <$> go 0

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

generatePrimArrayA :: forall f a. (Applicative f, Prim a) => Int -> (Int -> f a) -> f (PrimArray a)
generatePrimArrayA !(I# n#) f =
  let go i# = case i# <# n# of
                0# -> pure $ \mary s# ->
                        case unsafeFreezeByteArray# mary s# of
                          (# s'#, arr'# #) -> (# s'#, PrimArray arr'# #)
                _ -> liftA2
                     (\b m ->
                        \mary s ->
                          case writeByteArray# mary i# b s of
                            s'# -> m mary s'#)
                     (f (I# i#))
                     (go (i# +# 1#))
   in (\m -> runRW# $ \s0# ->
                case newByteArray# (n# *# sizeOf# (undefined :: a)) s0# of
                  (# s'#, arr# #) -> case m arr# s'# of
                                       (# _, a #) -> a)
      <$> go 0#

Вот моя версия с поправкой на Applicative вместо Monad:

generatePrimM :: forall m a . (Prim a, Applicative m) => Int -> (Int -> m a) -> m (PrimArray a)
generatePrimM (I# sz#) f =
  let go i# = case i# <# sz# of
                0# -> runRW# $ \s0# ->
                      case newByteArray# (sz# *# sizeOf# (undefined :: a)) s0# of
                        (# s1#, mba# #) -> pure (MutableByteArrayState s1# mba#)
                _  -> liftA2
                      (\b (MutableByteArrayState si# mba#) ->
                         MutableByteArrayState (writeByteArray# mba# i# b si#) mba#)
                      (f (I# i#))
                      (go (i# +# 1#))
   in (\(MutableByteArrayState s# mba#) ->
         case unsafeFreezeByteArray# mba# s# of
           (# _, ba# #) -> PrimArray ba#) <$>
      (go 0#)

Функционально и с точки зрения производительности они очень близки друг к другу, и в конце они оба дадут абсолютно одинаковый ответ.Разница в том, что внутренний цикл go производит в конце.Последний вернет аппликатив, содержащий замыкание, которое может создать MutableByteArray# s, которые позже будут заморожены.В то время как первый имеет цикл, который возвращает аппликатив, содержащий действие, которое создаст замороженные ByteArray# с, как только ему предоставляется действие, которое может создать MutableByteArray#.

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

...