Работа с бесконечными списками со строгими монадами

У меня есть функция f :: [a] -> b, которая работает с бесконечными списками (например, take 5, takeWhile (< 100) . scanl (+) 0 и т. д.). Я хочу передать этой функции значения, сгенерированные строгими монадическими действиями (например, randomIO).

Из этот вопрос, я узнал, что подход с трюками repeat и sequence не работает для строгих монад, как показано в примере ниже:

import Control.Monad.Identity

take 5 <$> sequence (repeat $ return 1) :: Identity [Int]
-- returns `Identity [1,1,1,1,1]`
-- works because Identity is non-strict

take 5 <$> sequence (repeat $ return 1) :: IO [Int]
 -- returns `*** Exception: stack overflow`
 -- does not work because IO is strict

Поэтому вместо этого я подумал об использовании функции «внутри» монадического контекста. Меня вдохновил этот пример циклического программирования и пробовал:

let loop = do
       x <- return 1
       (_, xs) <- loop
       return (take 5 xs, x:xs)
in  fst loop :: Identity [Int]
-- Overflows the stack

а также

import Control.Monad.Fix

fst <$> mfix (\(_, xs) -> do
   x <- return 1
   return (take 5 xs, x:xs)) :: Identity [Int]
-- Overflows the stack

и даже

{-# LANGUAGE RecursiveDo #-}
import System.Random

loop' = mdo
   (xs', xs) <- loop xs
   return xs'
where loop xs = do
      x <- randomIO
      return (take 5 xs, x:xs)

print $ loop'
-- Returns a list of 5 identical values

Но ничего из этого не работает. Я также попробовал подход Conduit, который не сработал даже в случае Identity:

import Conduit

runConduitPure $ yieldMany [1..] .| sinkList >>= return . take 5

Поэтому я хотел бы знать:

  1. Почему ни один из описанных выше «круговых» подходов не работает?

  2. Если существует решение этой проблемы, не связанное с unsafeInterleaveIO. (может быть iterateeс, Arrowс?)


person Douglas Vieira    schedule 11.02.2017    source источник
comment
В общем, это сложная проблема. В частности, для случайных чисел есть простой выход: randoms <$> newStdGen :: Random a => IO [a] — это бесконечный случайный список всего, что вы хотите (то есть Random).   -  person Alec    schedule 11.02.2017
comment
@Алек Спасибо за комментарий. Я использую здесь randomIO только для простоты примеров. На практике я хотел бы обрабатывать сообщения, полученные через сокеты.   -  person Douglas Vieira    schedule 11.02.2017
comment
Так что-то вроде бесконечного списка сообщений, полученных из сокета?   -  person Alec    schedule 11.02.2017
comment
@ Алек Точно. Более конкретно, вместо randomIO я использую receive sock, где sock — это монада ZeroMQ Socket z (см. hackage.haskell.org/package/zeromq4-haskell-0.6.5/docs/ )   -  person Douglas Vieira    schedule 11.02.2017
comment
@DouglasVieira в программе conduit, разве вы не должны использовать take внутри трубопровода? >>> runConduit $ sourceRandom .| takeC 5 .| sinkList :: IO [Bool] дает мне [False,False,False,False,True] Здесь sourceRandom заменяет ваш поток сообщений, но чистый случай тоже работает. На самом деле список не должен быть составлен, вы должны делать с ним все, что собирались делать внутри канала.   -  person Michael    schedule 12.02.2017
comment
Кстати, я не думаю, что ленью это можно объяснить. Maybe ленив, но take 5 <$> sequence (repeat $ Just 1) :: Maybe [Int] тоже зависает, так как если бы миллионный элемент списка был Nothing, он вернул бы Nothing. Точно так же take 5 <$> sequence (repeat [1]) :: [[Int]] будет [], если где-то в списке списков есть пустой список.   -  person Michael    schedule 12.02.2017
comment
@Michael Что касается вашего первого комментария, см. мой комментарий к ответу @duplode. По поводу вашего второго комментария, я не вижу смысла. Для монады Maybe, я думаю, проблема в том, что единственная возможная фиксированная точка в монадическом контексте (между Just a или Nothing) — это Nothing, поэтому sequenceинг бесконечного списка Just a просто зависнет навсегда. Identity a не имеет этой проблемы, потому что фиксированная точка монадического контекста уже Identity a (то же самое интуитивно применимо и для IO a, если бы не было побочных эффектов).   -  person Douglas Vieira    schedule 12.02.2017
comment
Моя точка зрения была о строгом v ленивом. Идентификация, кстати, максимально строгая, так как это новый тип. Я думаю, вам просто нужно проработать определение sequence, т.е. foldr (\m ms -> (:) <$> m <*> ms) (pure []) вместе с монадной (аппликативной) реализацией в каждом конкретном случае, чтобы увидеть, почему в Identity дела идут так гладко, а в IO (так часто) плохо. По сути, чтобы избежать sequence, у нас есть потоковые библиотеки. С точки зрения потоковых библиотек sequence mapM replicateM и т. д. для списков ввода-вывода + являются фундаментальными красными флажками.   -  person Michael    schedule 12.02.2017
comment
@Майкл Теперь я вижу, хорошая мысль! Некоторые примеры иллюстрируют упомянутое вами свойство строгости: seq (return undefined :: Identity ()) (), seq (return undefined :: Maybe ()) (), seq (return undefined :: IO ()) (). Как вы указали, случай с Identity выдает ошибку, так как он определяется как новый тип. Интересно, что случай с IO не выдает ошибки, хотя print 1 >> return () печатает 1 в терминале. В ответе Джона Л. он упоминает строгость (>>=), которая здесь играет роль.   -  person Douglas Vieira    schedule 12.02.2017
comment
С Control.Monad.Trans.State.Lazy, если я пишу a = take 5 <$> sequence (repeat $ return 1) :: StateT () Identity [Int], я могу написать >>> evalStateT a () и получаю Identity [1,1,1,1,1] с >>> runStateT a () я получаю Identity ([1,1,1,1,1], , и он делает паузу, пока пытается получить () в конце радуги - поэтому происходит переполнение стека. С Control.Monad.Trans.State.Strict оба заканчиваются переполнением стека. sequence действительно довольно токсичен для списков, хотя, конечно, его можно использовать.   -  person Michael    schedule 12.02.2017


Ответы (2)


Я использую здесь randomIO только для простоты примеров. На практике я хотел бы обрабатывать сообщения, полученные через сокеты

Это невозможно без unsafeInterleaveIO. В конце концов, проблема заключается в том, что IO действия должны быть упорядочены. Хотя порядок оценки ссылочно-прозрачных значений не имеет значения, порядок IO действий может иметь значение. Если вам нужен ленивый бесконечный список всех сообщений, полученных через сокет, вы должны априори сообщить Haskell, где в последовательности IO действий это подходит (если вы не используете unsafeInterleaveIO).

Абстракция Arrow, которую вы ищете, называется ArrowLoop, но у него тоже есть проблема с правильным ужесточением закона для строгих монад.

На первый взгляд это может выглядеть как MonadFix< /a> (проявляется через mdo или mfix) тоже является решением, но если копнуть глубже показывает, что у fixIO есть проблемы, как и у loop из ArrowLoop.

Однако иногда ограничение, согласно которому действия IO должны выполняться одно за другим, является немного чрезмерным, и именно для этого предназначено unsafeInterleaveIO. Цитирование документов

unsafeInterleaveIO позволяет лениво откладывать IO вычисления. При передаче значения типа IO a IO будет выполняться только тогда, когда требуется значение a.


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

interweavingRepeatM :: IO a -> IO [a]
interweavingRepeatM action = unsafeInterleaveIO ((:) <$> action <*> interweavingRepeatM action)

И вот работает для случайных чисел:

ghci> import System.Random
ghci> sourceOfRandomness <- interweavingRepeatM randomIO :: IO [Integer]
ghci> take 10 sourceOfRandomness
[-2002742716261662204,7803971943047671004,-8395318556488893887,-7372674153585794391,5906750628663631621,6428130029392850107,6453903217221537923,-8966011929671667536,6419977320189968675,-1842456468700051776]
person Alec    schedule 11.02.2017
comment
Большое спасибо за это объяснение. Не могли бы вы подробнее остановиться на: 1. Как решить эту проблему для общей монады, преобразованной из IO? 2. Почему подход mdo давал повторяющиеся значения? - person Douglas Vieira; 11.02.2017
comment
@DouglasVieira 1. Я не думаю, что вы можете вообще - монады предназначены для кодирования этой природы последовательности 2. mdo desugars для использования fixIO и fixIO сбивает с толку только оценивает свое действие один раз (смотря на < href="https://hackage.haskell.org/package/base-4.9.1.0/docs/src/System.IO.html#fixIO" rel="nofollow noreferrer">источник может оказаться полезным) . Даже если бы вам удалось написать что-то, что действительно должно было возвращать список, вы бы столкнулись с еще более запутанным сообщением об ошибке, связанным с MVar (или, что еще хуже, с зависшей программой). - person Alec; 11.02.2017
comment
@DouglasVieira Что касается 1. - Я вижу, что пакет, на который вы ссылаетесь, также предоставляет функции IO. Вы можете использовать их и unsafeInterleaveIO, чтобы получить свой список, а затем liftIO вернуться в нужную монаду. - person Alec; 11.02.2017

ответ Алека охватывает ваш общий вопрос. Далее следует конкретно о conduit и подобных библиотеках потоковой передачи.

Я также попробовал подход Conduit, который не сработал даже в случае Identity:

import Conduit

runConduitPure $ yieldMany [1..] .| sinkList >>= return . take 5

Хотя потоковые библиотеки обычно используются, чтобы избежать упомянутых вами трудностей (см. вступительные замечания к Pipes.Tutorial), они предполагают, что вы будете использовать их типы потоков вместо списков. Рассмотрим, например, как sinkList< /a> описан в документации Conduit:

Потребляет все значения из потока и возвращает в виде списка. Обратите внимание, что при этом все значения будут сохранены в памяти.

Это означает, что использование sinkMany сразу после yieldMany возвращает вас к исходной точке: размещение всех значений в памяти — это именно то, что делает комбинацию sequence, IO и бесконечных списков непригодной для использования. Вместо этого вам следует использовать инфраструктуру потоковой библиотеки для создания этапов вашего конвейера. Вот несколько простых примеров, использующих в основном готовые вещи из conduit и conduit-combinators:

GHCi> import Conduit
GHCi> runConduitPure $ yieldMany [1..] .| takeC 5 .| sinkList
[1,2,3,4,5]
GHCi> runConduit $ yieldMany [1..] .| takeC 5 .| printC -- try it without takeC
1
2
3
4
5
GHCi> runConduit $ yieldMany [1..] .| takeC 5 .| scanlC (+) 0 .| printC
0
1
3
6
10
15
GHCi> :{
GHCi| runConduit $ yieldMany [1..] .| takeC 5
GHCi|     .| awaitForever (\x -> liftIO (print (2*x)) >> yield x)
GHCi|     .| printC
GHCi| :}
2
1
4
2
6
3
8
4
10
5
GHCi> runConduit $ (sourceRandom :: Producer IO Int) .| takeC 5 .| printC 
1652736016140975126
5518223062916052424
-1236337270682979278
8079753510915129274
-609160753105692151

(Спасибо Michael за то, что заставил меня заметить sourceRandom -- сначала я свернул свой собственный с repeatMC randomIO.)

person duplode    schedule 12.02.2017
comment
Спасибо за разъяснение относительно Conduit. Проблема, однако, в том, что я хочу, чтобы решение работало с произвольной функцией f :: [a] -> b, которая действительно работает с (бесконечными) списками. Поэтому, как вы указали, Conduit - это не то решение, которое я ищу. Хуже того, как указал @Alec, в общем случае нет решения. Чтобы дать больше контекста, я пытался работать над простой реализацией функционального реактивного программирования, где примитивный тип Event :: Ord t => [(t, a)] представляет собой бесконечный список, упорядоченный по времени (т.е. я действительно хотел f :: Event -> b). - person Douglas Vieira; 12.02.2017
comment
Функции f, которые вы на самом деле упомянули (например, take 5, takeWhile (‹ 100). scanl (+) 0 и т. д.) — это элементарные преобразования потоков, которые действительно работают с бесконечными потоками. Вы не указали причину, по которой вы используете бесконечные списки, а не потоки, которые поддерживают почти весь API Data.List без необходимости sequence и др. Вся суть в том, что последовательность нельзя спасти «в общем случае». - person Michael; 12.02.2017