Подтвердить что ты не робот

Идиоматический способ представления типа суммы (Либо a) в Clojure

Под ред. Теперь мой вопрос: какие идиоматические конструкции Clojure обычно используются вместо типов сумм в языках статических типов? Консенсус до сих пор: использовать протоколы, если поведение может быть унифицировано, использовать помеченные пары/карты в противном случае, поставить необходимые утверждения в предварительном и послесловии.

Clojure предоставляет множество способов выражения типов продуктов: векторов, карт, записей..., но как вы представляете типы сумм , также известных как помеченные союзы и варианты записей? Что-то вроде Either a b в Haskell или Either[+A, +B] в Scala.

Первое, что приходит мне в голову, это карта со специальным тегом: {:tag :left :value a}, но тогда весь код будет загрязнен условностями на (:tag value) и обрабатывать специальные случаи, если он не существует... То, что я хотел бы гарантировать, состоит в том, что :tag всегда существует, и он может принимать только одно из указанных значений, а соответствующее значение всегда одного и того же типа/поведения и не может быть nil, и есть простой способ увидеть, что я позаботился обо всех случаях в коде.

Я могу представить макрос в строках defrecord, но для типов сумм:

; it creates a special record type and some helper functions
(defvariant Either
   left Foo
   right :bar)
; user.Either

(def x (left (Foo. "foo")))   ;; factory functions for every variant
; #user.Either{:variant :left :value #user.Foo{:name "foo"}}
(def y (right (Foo. "bar")))  ;; factory functions check types
; SomeException...
(def y (right ^{:type :bar} ()))
; #user.Either{:variant :right :value ()}

(variants x) ;; list of all possible options is intrinsic to the value
; [:left :right]

Существует ли такая вещь? (Ответ: нет).

4b9b3361

Ответ 1

как вы представляете типы сумм, также известные как помеченные союзы и   варианты записей? Что-то вроде Either a b в Haskell или   Either[+A, +B] в Scala.

Either имеет два применения: для возврата значения одного из двух типов или для вернуть два значения того же типа, которые должны иметь разные значения семантика на основе тега.

Первое использование важно только при использовании системы статического типа. Either в основном является минимальным возможным решением, учитывая ограничения системы типа Haskell. С системой динамического типа, вы можете возвращать значения любого типа, который вы хотите. Either не требуется.

Второе использование значимо, но может быть выполнено довольно просто двумя (или более) способами:

  • {:tag :left :value 123} {:tag :right :value "hello"}
  • {:left 123} {:right "hello"}

Что я хотел бы гарантировать, так это: tag всегда есть, и он может   принимать только одно из указанных значений, а соответствующее значение   последовательно одного и того же типа/поведения и не может быть nil, и там   это простой способ увидеть, что я позаботился обо всех случаях в коде.

Если вы хотите обеспечить это статически, Clojure, вероятно, не Твой язык. Причина проста: выражения не имеют типов до времени выполнения - пока они не вернут значение.

Причина, по которой макрос не будет работать, заключается в том, что при увеличении времени макросов вы не имеют значений времени выполнения и, следовательно, типов времени выполнения. У тебя есть компиляционные конструкции, такие как символы, атомы, пресечения и т.д. Вы может eval их, но использование eval считается плохой практикой для количество причин.

Однако во время выполнения мы можем неплохо работать.

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

Моя стратегия будет состоять в том, чтобы преобразовать все, что обычно статично (в Haskell) во время выполнения. Пусть написано код.

;; let us define a union "type" (static type to runtime value)
(def either-string-number {:left java.lang.String :right java.lang.Number})

;; a constructor for a given type
(defn mk-value-of-union [union-type tag value]  
  (assert (union-type tag)) ; tag is valid  
  (assert (instance? (union-type tag) value)) ; value is of correct type  
  (assert value)  
  {:tag tag :value value :union-type union-type}) 

;; "conditional" to ensure that all the cases are handled  
;; take a value and a map of tags to functions of one argument
;; if calls the function mapped to the appropriate tag
(defn union-case-fn [union-value tag-fn]
  ;; assert that we handle all cases
  (assert (= (set (keys tag-fn))
             (set (keys (:union-type union-value)))))
  ((tag-fn (:tag union-value)) (:value union-value)))

;; extra points for wrapping this in a macro

;; example
(def j (mk-value-of-union either-string-number :right 2))

(union-case-fn j {:left #(println "left: " %) :right #(println "right: " %)})
=> right: 2

(union-case-fn j {:left #(println "left: " %)})
=> AssertionError Assert failed: (= (set (keys tag-fn)) (set (keys (:union-type union-value))))

В этом коде используются следующие идиоматические конструкции Clojure:

  • Программирование, управляемое данными: создать структуру данных, которая представляет "тип". Это значение является неизменным и первоклассным, и у вас есть весь язык, доступный для реализации с ним логики. Это то, что я не верю, что Haskell может: манипулировать типами во время выполнения.
  • Использование карт для представления значений.
  • Программирование более высокого порядка: передача карты fns в другую функцию.

Можно использовать протоколы, если вы используете Either для полиморфизма. В противном случае, если вы заинтересованы в теге, что-то вроде формы {:tag :left :value 123} является самым идиоматическим. Вы часто увидите что-то вроде этого:

;; let say we have a function that may generate an error or succeed
(defn somefunction []
  ...
  (if (some error condition)
    {:status :error :message "Really bad error occurred."}
    {:status :success :result [1 2 3]}))

;; then you can check the status
(let [r (somefunction)]
  (case (:status r)
    :error
    (println "Error: " (:message r))
    :success
    (do-something-else (:result r))
    ;; default
    (println "Don't know what to do!")))

Ответ 2

В общем случае типы сумм в динамически типизированных языках представлены как:

  • (например, тип продукта с тегом, представляющим конструктор)
  • анализ случая на теге во время выполнения для отправки

В статически типизированном языке большинство значений различаются по типам - это означает, что вам не нужно выполнять анализ тегов во время выполнения, чтобы узнать, есть ли у вас Either или Maybe, поэтому вы просто смотрите на тег знать, является ли это Left или Right.

В динамически типизированном параметре сначала необходимо выполнить анализ типа времени выполнения (чтобы узнать, какое у вас значение), а затем анализ случая конструктора (чтобы узнать, какой вкус у вас есть).

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

В некотором смысле вы можете думать о динамической типизации как о переносе всех значений в один тип суммы, откладывая анализ всех типов на тесты времени выполнения.


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

В стороне, это в значительной степени описание того, что бы сделала система статического типа.

Ответ 3

причина, по которой это хорошо работает на некоторых языках, заключается в том, что вы отправляете (обычно по типу) результат - т.е. вы используете какое-то свойство (обычно тип) результата, чтобы решить, что делать дальше.

поэтому вам нужно посмотреть, как отправка может произойти в clojure.

  • nil специальный случай - значение nil имеет специальную обложку в разных местах и ​​может использоваться как часть "Нет" в "Может быть". например, if-let очень полезно.

  • соответствие шаблонов - базовый clojure не имеет большой поддержки для этого, кроме последовательностей деструктурирования, но существуют различные библиотеки. см. Clojure замена ADT и соответствия шаблону? [обновление: в комментариях mnicky говорит, что устарел, и вы должны использовать core.match]

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

  • теги вручную. Наконец, вы можете использовать case или cond с явными тегами. более полезно, вы можете обернуть их в какой-то макрос, который работает так, как вы хотите.

Ответ 4

Будучи динамически типизированным языком, типы в целом несколько менее важны/важны в Clojure, чем в Haskell/Scala. Вам не нужно явно указывать их - например, вы уже можете хранить значения типа A или типа B в переменной.

Так что это действительно зависит от того, что вы пытаетесь сделать с этими типами сумм. Вероятно, вы действительно заинтересованы в полиморфном поведении, основанном на типе, и в этом случае имеет смысл определить протокол и два разных типа записей, которые вместе дают полиморфные поведение типа суммы:

(defprotocol Fooable
  (foo [x]))

(defrecord AType [avalue]
  Fooable 
    (foo [x]
      (println (str "A value: " (:avalue x)))))

(defrecord BType [bvalue]
  Fooable 
    (foo [x]
      (println (str "B value: " (:bvalue x)))))

(foo (AType. "AAAAAA"))

=> A value: AAAAAA

Я думаю, что это принесет почти все выгоды, которые вы, скорее всего, захотите, от типов сумм.

Другие приятные преимущества этого подхода:

  • Записи и протоколы очень идиоматичны в Clojure
  • Отличная производительность (поскольку отправка протокола сильно оптимизирована)
  • Вы можете добавить обработку для nil в свой протокол (через extend-protocol)

Ответ 5

Без завершения чего-то умопомрачительного, как напечатано clojure Я не думаю, что вы можете избежать проверки версий во время выполнения.

Одна менее известная функция, предоставляемая clojure, которая может определенно помочь с проверками времени выполнения, - это реализация предварительных и пост-условий (см. http://clojure.org/special_forms и сообщение в блоге от fogus). Я думаю, вы могли бы использовать только одну функцию обертки более высокого порядка с предварительными и пост-условиями, чтобы проверить все ваши утверждения на соответствующем коде. Это позволяет избежать проверки работоспособности "проблемы загрязнения" довольно хорошо.

Ответ 6

Нет, в clojure нет такой вещи. Хотя вы можете реализовать его, но IMO этот тип, похоже, больше подходит для статически типизированных языков и не даст вам большой пользы в динамической среде, такой как clojure.

Ответ 7

Используйте вектор с тегом как первый элемент в векторе и используйте core.match для деструкции помеченных данных. Следовательно, в приведенном выше примере "либо" данные будут закодированы как:

[:left 123]
[:right "hello"]

Чтобы затем разрушить, вам нужно будет обратиться к core.match и использовать:

(match either
  [:left num-val] (do-something-to-num num-val)
  [:right str-val] (do-something-to-str str-val))

Это более кратким, чем другие ответы.

Этот разговор с youtube дает более подробное объяснение того, почему векторы желательны для вариантов кодирования по картам. Мое резюме состоит в том, что использование карт для кодирования вариантов является проблематичным, потому что вы должны помнить, что карта является "помеченной картой", а не обычной картой. Чтобы правильно использовать "помеченную карту", ​​вы должны всегда выполнять двухэтапный поиск: сначала тег, затем данные на основе тега. Если (при) вы забудете искать тег в варианте с кодировкой карты или получить неправильный поиск ключа для тега или данных, вы получите исключение нулевого указателя, которое трудно отследить.

Видео также охватывает эти аспекты векторных кодированных вариантов:

  • Ловушка незаконных тегов.
  • Добавление статической проверки при желании с помощью Типизировано Clojure.
  • Сохранение этих данных в Datomic.