Clojure域建模:规范与协议

时间:2018-11-08 00:36:35

标签: clojure clojure.spec

这个问题真的很长。我欢迎提出建议为这个问题提供更好论坛的评论。

我正在建模swarming behavior of birds。为了帮助我整理思想,我创建了三个协议,分别代表了我所看到的主要领域概念:BoidFlock(主体集合)和Vector

当我进一步考虑时,我意识到我正在创建新的类型来表示BoidFlock,而这些类型可以使用规范映射非常清晰地建模:Boid是一个简单的映射位置和速度(两个向量),而羊群则是伯德图的集合。简洁,简洁,简单,并且消除了我的自定义类型,而使用了map和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))

但是,尽管boid可以很容易地表示为一对向量(而羊群可以表示为boid的集合),但我为如何对向量建模而感到困惑。我不知道是否要使用笛卡尔坐标或极坐标来表示矢量,所以我想要一个可以使我的细节抽象的表示。我想要向量函数的基本代数,而不管我如何在后台存储向量分量。

(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的生成器,那么我可以免费测试所有更高的抽象。那么我如何指定Vector并创建合适的测试生成器?

一个明显但不充分的答案:创建一个规范::vector,它表示一对坐标的地图,例如(s/keys ::x ::y)。但是为什么(x, y)呢?如果我可以访问(angle, magnitude),则某些计算会更容易。我可以创建::vector来表示一对坐标,但是想要 other 表示的那些函数必须知道并关心矢量在内部的存储方式,因此必须知道要达到的目的。外部转换功能。 (是的,我可以使用multispec / conform / multimethods来实现这一点,但是伸手去拿那些工具闻起来就像是不必要的泄漏抽象;我不希望更高的抽象知道或关心Vector可以用多种方式表示。)

更根本的是,向量不是(x, y)(angle, magnitude),它们只是“真实”向量的投影,但是您需要定义它。 (我说的是领域建模,而不是数学上的严谨性。)因此,创建将向量表示为一对坐标的规范在这种情况下不仅是糟糕的抽象,而且也不代表领域实体。

一个更好的选择是我上面定义的协议。可以使用Vector协议来编写所有更高的抽象,这给了我一个干净的抽象层。但是,如果不将抽象与具体实现耦合,就无法创建好的Vector测试生成器。也许这是我必须做出的取舍,但是有没有更好的方法来建模呢?

2 个答案:

答案 0 :(得分:1)

尽管对于这个问题肯定有很多有效的答案,但我建议您重新考虑自己的目标。

通过支持规范中的两个坐标表示,您表示它们同时受到支持。这将不可避免地导致运行时多态性等复杂性开销。例如您需要为笛卡尔/笛卡尔,笛卡尔/极地,极地/笛卡尔,极地/极地实现Vector协议。此时,实现是耦合的,您不会获得在表示之间“无缝”交替的预期好处。

我会选择一种表示形式,并在必要时使用外部转换层。

答案 1 :(得分:1)

从我们在评论中的讨论看来,您似乎更喜欢使用协议的多态性。我想我知道您想做什么,并将尝试做出回应。

因此,假设您具有矢量接口:

(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)