Commits

Dmitry Grebeniuk  committed 3c36b88

4. stream2list, 5. run

  • Participants
  • Parent commits 997bb5b

Comments (0)

Files changed (1)

 Но не переживайте: энумераторы, умеющие кормить итераты чанками
 меньшего размера, чем весь поток, будут продемонстрированы ниже,
 например, в энумераторе enum_pure_nchunk или enum_{fd,file}.)
+
+
+Итераторы: 4. stream2list.
+
+  Неплохо бы, умея кормить итераты хотя бы из строки, как-нибудь
+позапускать их и посмотреть, что же им дают на вход.  Напишем
+итерат, который будет складывать всё, что ему дают, в список,
+и возвращать этот список.
+
+  В оригинальных исходниках этот итерат называется stream2list,
+хотя лучше было бы назвать его list_of_stream.  Однако, это
+несущественно.
+
+  Этот итерат будет иметь тип "iteratee 'el (list 'el)".  Он не
+возращает значение сразу, он будет работать только после анализа
+входного потока, следовательно, начнём его с комбинатора ie_cont.
+Функция, собирающая поступающие элементы, будет собирать их в
+аккумулятор в обратном порядке, анализируя поток.  Итак,
+
+    value stream2list
+     : iteratee 'el (list 'el)
+     =
+      ie_cont (step [])
+      where rec step rev_acc s =
+        match s with
+        [ Chunk c -> ...
+        | EOF _ -> ...
+        ]
+    ;
+
+  В случае, когда дают чанк, добавим все его элементы к аккумулятору
+в обратном порядке с помощью функции
+"S.append_to_list_rev : S.t 'el -> list 'el -> list 'el", затем
+запросим ещё данные вызовом комбинатора ie_contM с продолжением,
+которое будет хранить уже новый аккумулятор:
+
+    value stream2list
+     : iteratee 'el (list 'el)
+     =
+      ie_cont (step [])
+      where rec step rev_acc s =
+        match s with
+        [ Chunk c ->
+            let rev_acc = S.append_to_list_rev c rev_acc in
+            ie_contM (step rev_acc)
+        | EOF _ -> ...
+        ]
+    ;
+
+  В случае, когда дают конец потока (естественный конец файла
+либо ошибку), вернём аккумулятор и этот конец потока (для его
+обработки в следующих итератах).  Итого, получается следующий код:
+
+    value stream2list
+     : iteratee 'el (list 'el)
+     =
+      ie_cont (step [])
+      where rec step rev_acc s =
+        match s with
+        [ Chunk c ->
+            let rev_acc = S.append_to_list_rev c rev_acc in
+            ie_contM (step rev_acc)
+        | EOF _ ->
+            ie_doneM (List.rev rev_acc) s
+        ]
+    ;
+
+
+Попытка запуска: enum_string + stream2list.
+
+  Представим, что есть строка "abc", которую хотим скормить энумератом
+enum_string итерату stream2list, чтобы получить список ['a'; 'b'; 'c'].
+Чтобы из enum_string получить энумератор, необходимо применить к нему
+строку.  То есть,
+
+    enum_string "abc"  :  enumerator char 'a
+                          =  iteratee char 'a -> IO.m (iteratee char 'a)
+
+  Далее к этой функции необходимо применить сам итерат:
+
+    (enum_string "abc") stream2list  :  IO.m (iteratee char (list char))
+
+  Далее необходимо извлечь значение (итерат) для работы с ним:
+
+    (enum_string "abc") stream2list >>% fun it -> ...
+
+  Но энумераторы не завершают выполнение итерата сами по себе.  Это
+очень полезно в тех случаях, когда итерат (it) нужно докормить какими-то
+другими данными, если он не завершился ещё.  Например, из другого
+потока (из другой строки, файла, сокета).  И тут внезапно оказывается,
+что после кормления итерата stream2list строкой "abc" результат,
+итерат it, не хранит ни значения, ни ошибки (не является ни "IE_done _",
+ни "IE_cont (Some _) _").  Он оказывается равен "IE_cont None k", где
+продолжение k равно функции step из итерата stream2list, а в её
+окружении значение rev_acc равно "['c'; 'b'; 'a']".  Это нормально,
+если нужно что-то ещё добавить к этому списку, но для наших целей --
+просто посмотреть данные -- это неприемлемо.
+
+  Однако, если указать этому итерату, что "обнаружен конец потока",
+то итерат возвратит ровно то, что нам нужно: "['a'; 'b'; 'c']",
+судя по коду stream2list.  А с точки зрения пользователя создастся
+впечатление, что мы "запустили" итерат.  Поэтому функция, которая
+передаёт конец потока итерату и возвращает его результат, называется
+"run", и про неё -- следующая глава.
+
+
+Запуск: 5. run.
+
+  Итак, необходимо написать комбинатор run, который будет брать
+в качестве аргумента итерат, будет передавать ему признак конца
+потока, и возвращать результат его работы.  То есть, тип будет
+
+    value run
+     : iteratee 'el 'a -> ... что-то с 'a ...
+
+  Помним, что итераты работают в пределах базовой монады, и результат
+работы может потребовать действий ввода-вывода (что делается через
+базовую монаду), либо итерат может выдать ошибку, обработка которых
+делается опять же через базовую монаду.
+
+  Можно избавиться от типа итерата в результате, но от типа базовой
+монады не избавиться никак -- и результат, включающий в себя
+сайд-эффекты, и ошибка, возвращённая через базовую монаду, будут
+иметь тип "IO.m 'a".  Итак, тип будет
+
+    value run
+     : iteratee 'el 'a -> IO.m 'a
+
+  В случае, когда итерат уже содержит результат, достаточно будет
+вернуть этот результат через базовую монаду:
+
+    value run
+     : iteratee 'el 'a -> IO.m 'a
+     = fun it ->
+         match it with
+         [ IE_done x -> IO.return x
+         | IE_cont (Some e) _k -> IO.error e
+         | IE_cont None k ->
+             ...
+         ]
+    ;
+
+  Если же итерат не закончил работу, нужно передать ему признак
+конца файла, и итерат будет обязан вернуть то, что возвращает
+функция-продолжение итератов: значение с типом
+"IO.m (iteratee 'el 'a  *  stream 'el)":
+
+    value run
+     : iteratee 'el 'a -> IO.m 'a
+     = fun it ->
+         match it with
+         [ IE_done x -> IO.return x
+         | IE_cont (Some e) _k -> IO.error e
+         | IE_cont None k ->
+             k (EOF None) >>% fun (it, _s) ->
+             ...
+         ]
+    ;
+
+  Поток _s игнорируем, он не нужен дальше нигде.  Его некуда вернуть,
+его незачем использовать.  А вот итерат it, возвращённый продолжением,
+представляет интерес.  В теории там могло быть снова "требование
+данных" -- "IE_cont None k".  Но это было бы неудобно для практики:
+не было бы ясно, сколько раз нужно дать "EOF None" этому продолжению,
+чтобы получить от него ответ (и можно ли было бы вообще получить ответ
+от такого итерата, вот в чём вопрос).
+
+  Поэтому Олег Киселёв изначально запретил такие итераты, и постановил,
+что, по "правилам хорошего тона", если итерату передали конец потока
+(естественный конец потока или ошибку), итерат должен перейти в
+состояние "завершено": вернуть либо результат работы (как в примере
+выше, список "['a'; 'b'; 'c']"), либо ошибку (как было бы в случае,
+если бы итерат был обязан прочитать ровно n элементов потока, а ему
+передали только m, где m<n).
+
+  Поэтому лучшим вариантом было бы проверить, хранит ли итерат it
+какой-либо результат.  Если хранит, необходимо вернуть его через
+базовую монаду, как в коде выше.  Если нет, то лучшее, что мы можем
+сделать, это свалиться в рантайме с ошибкой, рассказывающей о том,
+что в функцию "run" подсунули некачественный итерат.
+
+    exception Divergent_iteratee of string;
+
+    value run
+     : iteratee 'el 'a -> IO.m 'a
+     = fun it ->
+         match it with
+         [ IE_done x -> IO.return x
+         | IE_cont (Some e) _k -> IO.error e
+         | IE_cont None k ->
+             k (EOF None) >>% fun (it, _s) ->
+             match it with
+             [ IE_done x -> IO.return x
+             | IE_cont opt_err _k ->
+                 match opt_err with
+                 [ Some e -> IO.error e
+                 | None ->
+                     (* тот самый случай, когда итерат "плохой": *)
+                     IO.error (Divergent_iteratee "run")
+                 ]
+             ]
+         ]
+    ;
+
+  (можно было бы чуть сократить функцию "run", вынеся в отдельную
+функцию логику "вернуть значение итерата средствами базовой монады",
+но не будем этого делать для простоты понимания кода.)
+
+  Теперь, имея функцию run, можем написать код, который сможет
+работать со списком символов "['a'; 'b'; 'c']", который соберёт
+итератом stream2list, если ему скормить строку "abc" через enum_string:
+
+    (enum_string "abc") stream2list >>% fun it ->
+    run it >>% fun lst ->
+    <берём список lst, работаем с ним в пределах монады,
+     убеждаемся, что там нужные элементы>
+
+  Помним про ошибки, помним про сайд-эффекты, работаем со списком
+lst только средствами базовой монады, умеренно радуемся.