Преобразователи потока из книги Function Programming in Scala (FPiS) и Clojure преобразователи очень похожи. Они представляют собой обобщение идеи наличия «машины» (шаговой функции) для преобразования входного потока в выходной поток. Датчики FPiS называются Process
es. Рич Хики также использует термин процесс< /em> во вступительном докладе о преобразователях в Clojure.
Истоки
Конструкция датчиков FPiS основана на машинах Мили. Говорят, что машины Мили имеют:
transition function T : (S, I) -> S
output function G : (S, I) -> O
Эти функции могут быть объединены вместе, чтобы сформировать:
step: (S, I) -> (S, O)
Здесь легко увидеть, что пошаговая функция работает с текущим состоянием машины и следующим входным элементом для создания следующего состояния машины и выходного элемента.
Один из комбинаторов от FPiS использует такую ступенчатую функцию:
trait Process[I, O] {
...
def loop[S, I, O](z: S)(f: (I,S) => (O,S)): Process[I, O]
...
}
Эта функция loop
по существу представляет собой засеянное левое сокращение, о котором Рики говорит на этом слайде.
Независимость от контекста
Оба могут использоваться во многих различных контекстах (таких как списки, потоки, каналы и т. д.).
В преобразователях FPiS тип процесса:
trait Process[I, O]
Все, о чем он знает, это его элементы ввода и элементы вывода.
В Clojure аналогичная история. Хики называет это "полностью разъединенным". .
Сочинение
Оба типа преобразователей могут быть составлены.
FPiS использует оператор «pipe».
map(labelHeavy) |> filter(_.nonFood)
Clojure использует comp
(comp
(filtering non-food?)
(mapping label-heavy))
Представление
В Кложуре:
reducer: (whatever, input) -> whatever
transducer: reducer -> reducer
В ФПиС:
// The main type is
trait Process[I, O]
// Many combinators have the type
Process[I, O] ⇒ Process[I, O]
Однако представление FPiS — это не просто скрытая функция. Это case-класс (алгебраический тип данных) с 3 вариантами: Await, Emit и Halt.
case class Await[I,O](recv: Option[I] => Process[I,O])
case class Emit[I,O](head: O, tail: Process[I,O]
case class Halt[I,O]() extends Process[I,O]
- Await играет роль функции reducer->reducer из Clojure.
- Halt играет роль
reduced
в Clojure.
- Emit стоит вместо вызова функции следующего шага в Clojure.
Раннее прекращение
Оба поддерживают досрочное прекращение. Clojure делает это, используя специальное значение reduced
, которое можно проверить с помощью предиката reduced?
.
FPiS использует более статически типизированный подход, процесс может находиться в одном из 3 состояний: «ожидание», «выдача» или «остановка». Когда «пошаговая функция» возвращает процесс в состоянии «Остановка», функция обработки знает, что нужно остановиться.
Эффективность
В некоторых моментах они снова похожи. Оба типа преобразователей управляются спросом и не создают промежуточных коллекций. Тем не менее, я полагаю, что преобразователи FPiS не так эффективны, когда они конвейеризированы/составлены, поскольку внутреннее представление больше, чем "просто набор вызовов функций", как выразился Хикки. Я только догадываюсь об эффективности/производительности.
Посмотрите на fs2
(ранее scalaz-stream
) возможно более производительную библиотеку, основанную на проектирование преобразователей в FPiS.
Пример
Вот пример filter
в обеих реализациях:
Clojure, из слайдов выступления Хики:
(defn filter
([pred]
(fn [rf]
(fn
([] (rf))
([result] (rf result))
([result input]
(if (prod input)
(rf result input)
result)))))
([pred coll]
(sequence (filter red) coll)))
В FPiS есть один способ реализовать это:
def filter[I](f: I ⇒ Boolean): Process[I, I] =
await(i ⇒ if (f(i)) emit(i, filter(f))
else filter(f))
Как видите, здесь filter
состоит из других комбинаторов, таких как await
и emit
.
Безопасность
Есть несколько мест, где вы должны быть осторожны при реализации преобразователей Clojure. Кажется, это компромисс дизайна в пользу эффективности. Однако этот недостаток, по-видимому, касается в основном производителей библиотек, а не конечных пользователей/потребителей.
- Если преобразователь получает значение
reduced
из вызова вложенного шага, он никогда не должен снова вызывать эту функцию шага с вводом.
- Преобразователи, которым требуется состояние, должны создавать уникальное состояние и не могут быть совмещены.
- Все ступенчатые функции должны иметь вариант арности-1, который не принимает входные данные.
- Операция завершения преобразователя должна вызвать вложенную операцию завершения ровно один раз и вернуть то, что она возвращает.
Конструкция датчика от FPiS способствует правильности и простоте использования. Состав конвейера и flatMap
операций обеспечивают быстрое выполнение действий по завершению и правильную обработку ошибок. Эти проблемы не являются бременем для разработчиков преобразователей. Тем не менее, я полагаю, что библиотека может быть не такой эффективной, как Clojure.
Резюме
Преобразователи Clojure и FPiS имеют:
- подобное происхождение
- возможность использования в разных контекстах (список, потоки, каналы, файловый/сетевой ввод-вывод, результаты базы данных)
- по требованию / досрочное прекращение
- доработка/завершение (для сохранности ресурсов)
- вкусно :)
Они несколько отличаются по своему основному представлению. Преобразователи в стиле Clojure, кажется, отдают предпочтение эффективности, тогда как преобразователи FPiS отдают предпочтение правильности и композиционности.
person
Steven Shaw
schedule
14.05.2016