Моделирование 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)