Каковы сходства и различия между преобразователями Scala и преобразователями Clojure?

Пол Кьюзано и Рунар Оли написал фантастическую книгу Функциональное программирование на Scala. В нем упоминается малоизвестная в Scala-сообществе концепция — Transducers.

Scala Transducers в книге Functional Programming In Scala

В сообществе Clojure — преобразователи получают немного больше нажмите.

У меня такой вопрос: Каковы сходства и различия между преобразователями Scala **(из книги «Функциональное программирование в Scala») и преобразователями Clojure?**

Предположения:

я знаю, что

  1. Преобразователи — это обычное выражение из их концепции в электротехнике.

  2. Существует ранее существовавшая концепция в компьютерных науках, называемая преобразователем конечного состояния.

  3. Существует прецедент в биологии и психологии, в котором используется слово трансдукция

  4. уже есть история других технических книг. как SICP используя слово преобразователи.


person hawkeye    schedule 07.01.2015    source источник


Ответы (2)


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

  • Они очень разные
  • Они похожи только тем, что оба относятся к тому, как один человек превращает один набор в другой.

Что я могу сказать о Scala Transducers:

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

Stream[A] -> Stream[B]

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

Вот и все; довольно просто на самом деле.

Преобразователи Clojure:

Clojure преобразователь — это функция, которая преобразует одну редукционную функцию в другую. Функция сокращения может использоваться с reduce. То есть, если бы у Clojure были подписи, у него была бы подпись

(x, a) -> x

В английском языке, учитывая некоторую начальную коллекцию x и «следующую вещь» a в сокращаемой коллекции, наша функция сокращения возвращает «следующую итерацию создаваемой коллекции».

Итак, если это сигнатура редукционной функции, преобразователь имеет сигнатуру

((x, a) -> x) -> ((x, b) -> x)

Причина, по которой преобразователи были добавлены в Clojure, заключается в том, что с добавлением каналов core.async Рич Хикки и его друзья обнаружили сами повторно реализуют все стандартные функции сбора для работы с каналами (map, filter, take и т. д.). Р.Х. подумал, что здесь нет лучшего способа, и принялся за работу, думая о том, как разобрать логика этих различных функций обработки коллекций из механики имеющихся типов коллекций. Объяснение того, как именно преобразователи это делают, я думаю, выходит за рамки этого вопроса, поэтому я вернусь к сути. Но если вам интересно, есть много литературы по этому вопросу, которую легко найти и изучить.

Итак, как эти вещи связаны?

Понятно, что это очень разные понятия, но вот как я вижу их связь:

В то время как преобразователи Scala — это функции обработки коллекций для потоков (в отличие от других коллекций Scala), преобразователи Clojure на самом деле представляют собой механизм унификации реализации функций обработки коллекций для различных типов коллекций. Таким образом, можно было бы выразиться так: если бы в Scala было понятие преобразователей Clojure, то понятие преобразователей Scala могло бы быть реализовано в терминах понятия преобразователей Clojure, которые представляют собой более абстрактные/общие функции обработки, повторно используемые для нескольких типов коллекций.

person metasoarous    schedule 08.01.2015

Преобразователи потока из книги Function Programming in Scala (FPiS) и Clojure преобразователи очень похожи. Они представляют собой обобщение идеи наличия «машины» (шаговой функции) для преобразования входного потока в выходной поток. Датчики FPiS называются Processes. Рич Хики также использует термин процесс< /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