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

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

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

Первое, что приходит мне в голову, это карта со специальным тегом: {: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]

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

7 ответов

Решение

как вы представляете типы сумм, также известные как теговые объединения и варианты записей? Что-то вроде Either a b в Хаскеле или Either[+A, +B] в Скале.

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

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

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

  1. {:tag :left :value 123} {:tag :right :value "hello"}
  2. {:left 123} {:right "hello"}

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

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

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

Тем не менее, мы можем сделать довольно хорошую работу во время выполнения.

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

Моя стратегия будет заключаться в том, чтобы преобразовать все, что обычно статично (в 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's 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!")))

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

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

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

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

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

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


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

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

Используйте вектор с тегом в качестве первого элемента в векторе и используйте 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 дается более подробное объяснение того, почему векторы желательны для кодирования вариантов на картах. Мое резюме состоит в том, что использование карт для кодирования вариантов проблематично, потому что вы должны помнить, что карта - это "карта с тегами", а не обычная карта. Чтобы правильно использовать "карту с тегами", вы всегда должны выполнять двухэтапный поиск: сначала тег, затем данные на основе тега. Если (когда) вы забудете поискать тег в варианте с кодировкой карты или неправильно подберете ключевые слова для тега или данных, вы получите исключение нулевого указателя, которое трудно отследить.

Видео также охватывает следующие аспекты векторных кодированных вариантов:

  • Захват незаконных меток.
  • Добавление статической проверки, если необходимо, с использованием Typed Clojure.
  • Хранение этих данных в Datomic.

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

так что вам нужно посмотреть, как отправка может произойти в ближайшем будущем.

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

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

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

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

Будучи динамически типизированным языком, типы в целом несколько менее актуальны / важны в 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 очень идиоматичны
  • Отличная производительность (так как диспетчеризация протокола сильно оптимизирована)
  • Вы можете добавить обработку для ноля в вашем протоколе (через extend-protocol)

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

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

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

Другие вопросы по тегам