Моделирование Clojure доменов: спецификация против протоколов

Этот вопрос стал действительно длинным; Я приветствую комментарии, предлагающие лучшие форумы по этому вопросу.

Я моделирую роящееся поведение птиц. Чтобы помочь мне организовать свои мысли, я создал три протокола, представляющих основные концепции предметной области, которые я видел: Boid, Flock (коллекция боидов) и Vector,

Когда я больше думал об этом, я понял, что создаю новые типы для представления Boid а также Flock когда они могут быть очень точно смоделированы с использованием специальных карт: Boid - это простая карта положения и скорости (оба вектора), а стая - это набор карт Boid. Чистый, лаконичный и простой, и исключил мои пользовательские типы в пользу всей силы карт и clojure.spec,

(s/def ::position ::v/vector)
(s/def ::velocity ::v/vector)
(s/def ::boid (s/keys ::position
                      ::velocity))
(s/def ::boids (s/coll-of ::boid))

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

(defprotocol Vector
  "A representation of a simple vector. Up/down vector? Who cares!"
  (magnitude [vector] "Returns the magnitude of the vector")

  (angle [vector] "Returns the angle of the vector (in radians? from what
  zero?).")

  (x [vector] "Returns the x component of the vector, assuming 'x' means
  something useful.")

  (y [vector] "Returns the y component of the vector, assuming 'y' means
  something useful.")

  (add [vector other] "Returns a new vector that is the sum of vector and
  other.")

  (scale [vector scaler] "Returns a new vector that is a scaled version of
  vector."))

(s/def ::vector #(satisfies? Vector %))

Помимо эстетики согласованности, главная причина, по которой меня беспокоит это несоответствие, - это генеративное тестирование: я еще этого не делал, но я рад учиться, потому что он позволит мне тестировать функции более высокого уровня, как только я укажу свои нижестоящие функции. уровень примитивов. Проблема в том, что я не знаю, как создать генератор для ::vector спецификация без привязки абстрактного протокола / спецификации к конкретной записи, которая определяет функциональность. Я имею в виду, мой генератор должен создать Vector например, верно? Либо я proxy что-то прямо в генераторе и так создайте ненужное Vector реализация только для тестирования, или я соединяю свой красиво абстрактный протокол / спецификацию с конкретной реализацией.

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

Обновление № 1: Чтобы объяснить это по-другому, я создал многоуровневую модель данных, в которой определенный слой записывается только с точки зрения слоя под ним. (Ничего нового здесь.)

Flock (functions dealing with collections of boids)
----------------------------------------------------
Boid (functions dealing with a single boid)
----------------------------------------------------
Vector

Из-за этой модели удаление всех высших абстракций превратит мою программу в не что иное, как манипуляции с вектором. Желательное следствие этого факта: если я могу найти генератор для Векторов, я могу бесплатно протестировать все свои высшие абстракции. Итак, как мне настроить Vector и создать соответствующий тестовый генератор?

Очевидный, но неадекватный ответ: создать спецификацию ::vector который представляет карту пары координат, скажем, (s/keys ::x ::y), Но почему (x, y)? Некоторые вычисления были бы проще, если бы у меня был доступ к (angle, magnitude), Я мог бы создать ::vector для представления некоторой пары координат, но тогда те функции, которые хотят другое представление, должны знать и заботиться о том, как вектор хранится внутри, и поэтому должны знать, как обращаться к внешней функции преобразования. (Да, я мог бы реализовать это с помощью multispec / conform / мультиметоды, но стремление к этим инструментам пахнет излишне дырявой абстракцией; Я не хочу, чтобы высшие абстракции знали или заботились о том, чтобы векторы можно было представить несколькими способами.)

Еще более фундаментально, вектор не (x, y) или же (angle, magnitude) это просто проекции "реального" вектора, однако вы хотите это определить. (Я говорю о моделировании предметной области, а не о математической строгости.) Поэтому создание спецификации, представляющей вектор в виде пары координат, в данном случае является не только плохой абстракцией, но и не представляет сущность предметной области.

Лучшим вариантом будет протокол, который я определил выше. Все высшие абстракции могут быть написаны в терминах Vector протокол, давая мне чистый уровень абстракции. Тем не менее, я не могу создать хороший Vector тестовый генератор без привязки моей абстракции к конкретной реализации. Может быть, это компромисс, который я должен сделать, но есть ли лучший способ смоделировать это?

2 ответа

Решение

Хотя на этот вопрос наверняка есть много достоверных ответов, я бы посоветовал вам пересмотреть свои цели.

Поддерживая оба представления координат в спецификации, вы заявляете, что они поддерживаются одновременно. Это неизбежно приведет к сложностям, таким как полиморфизм во время выполнения. Например Ваш протокол Vector должен быть реализован для декартовой / декартовой, декартовой / полярной, полярной / декартовой, полярной / полярной. На этом этапе реализации связаны, и вы не получите ожидаемого преимущества "плавного" чередования представлений.

Я бы согласился на одно представление и при необходимости использовал внешний слой конверсии.

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

Предположим, у вас есть векторный интерфейс:

(defprotocol AbstractVector

  ;; method declarations go here...

  )

При объявлении AbstractVector протокол, нам не нужно знать о каких-либо конкретных реализациях этого протокола. Наряду с этим протоколом мы также реализуем место для сбора спецификаций:

(defonce concrete-spec-registry (atom #{}))

(defn register-concrete-vector-spec [sp]
  (swap! concrete-spec-registry conj sp))

Теперь мы можем реализовать этот протокол для различных классов:

(extend-type clojure.lang.ISeq
  AbstractVector

  ;; method implementations go here...

  )

(extend-type clojure.lang.IPersistentVector
  AbstractVector

  ;; method implementations go here...

  )

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

(spec/def ::concrete-vector-implementation (spec/cat :x number?
                                                     :y number?))
(register-concrete-vector-spec ::concrete-vector-implementation)

Давайте определим спецификацию для нашего абстрактного вектора, сначала написав функцию, которая проверяет, является ли что-то абстрактным вектором:

(defn abstract-vector? [x]
  (satisfies? AbstractVector x))

;; (assert (abstract-vector? []))
;; (assert (not (abstract-vector? {})))

Или, может быть, точнее реализовать это так:

(defn abstract-vector? [x]
  (some #(spec/valid? % x)
        (deref concrete-implementation-registry)))

И вот спецификация вместе с генератором:

(spec/def ::vector (spec/with-gen (spec/spec abstract-vector?)
                     #(gen/one-of (mapv spec/gen (deref concrete-spec-registry)))))

В приведенном выше коде мы разыменовываем атом, содержащий конкретную спецификацию, а затем строим генератор поверх этих спецификаций, которые будут генерироваться с использованием одной из них. Таким образом, нам не нужно знать, какие конкретные векторные реализации существуют, пока их источники загружены и register-concrete-vector-spec Функция была использована для регистрации конкретных спецификаций.

Теперь мы можем генерировать образцы:

(gen/generate (spec/gen ::vector))
;; => (-879 0.011494353413581848)
Другие вопросы по тегам