Моделирование Clojure доменов: спецификации против протоколов - PullRequest
0 голосов
/ 08 ноября 2018

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

Я моделирую поведение роящихся птиц . Чтобы помочь мне организовать свои мысли, я создал три протокола, представляющих основные концепции предметной области, которые я видел: Boid, Flock (набор boids) и 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 для представления некоторой пары координат, но тогда те функции, которым нужно представление other , должны знать и заботиться о том, как вектор хранится внутри, и поэтому должны знать, как достичь функции внешнего преобразования , (Да, я мог бы реализовать это, используя multispec / conform / multimethods, но использование этих инструментов пахнет излишне дырявой абстракцией; я не хочу, чтобы более высокие абстракции знали или заботились о том, чтобы Векторы могли быть представлены несколькими способами.)

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

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

Ответы [ 2 ]

0 голосов
/ 08 ноября 2018

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

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

(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)
0 голосов
/ 08 ноября 2018

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

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

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

...