【问题标题】:Clojure Domain Modeling: Spec vs. ProtocolsClojure 领域建模:规范与协议
【发布时间】:2018-11-08 00:36:34
【问题描述】:

这个问题真的很长;我欢迎 cmets 为这个问题建议更好的论坛。

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

当我仔细考虑时,我意识到我正在创建新类型来表示 BoidFlock,而这些类型可以使用规范的地图非常清晰地建模:boid 是位置和速度的简单地图(两个向量),flock 是 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))

但是,虽然 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,它表示一对坐标的映射,例如 (s/keys ::x ::y)。但是为什么(x, y)?如果我可以访问(angle, magnitude),一些计算会更容易。我可以创建 ::vector 来表示一对坐标,但是那些想要 other 表示的函数必须知道并关心向量是如何在内部存储的,因此必须知道如何进行外部转换功能。 (是的,我可以使用 multispec/conform/multimethods 来实现这一点,但使用这些工具听起来像是一种不必要的泄漏抽象;我不希望更高的抽象知道或关心向量可以以多种方式表示。)

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

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

【问题讨论】:

  • 这个问题我不是很清楚,好像同时问了很多东西。我什至会争辩说这有点“复杂”;-) 所以我读到你有某种向量和(1)。您需要多态行为,而现有的机制(协议和多方法)都不足以解决这个问题:为什么? (2) 您希望在不将规范与具体实现绑定的情况下对向量进行生成测试。
  • #1 的答案是#2:我会使用协议,除非我没有将我的抽象(规范/协议)耦合到具体实现的情况下进行生成测试。我会更新这个问题,让它更清楚一点。
  • 太好了,谢谢!我可能知道你怎么做,会看到......

标签: clojure clojure.spec


【解决方案1】:

虽然这个问题肯定有很多有效的答案,但我建议你重新考虑你的目标。

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

我会满足于一种表示形式,如有必要,使用外部转换层。

【讨论】:

  • 我不明白您的评论,我需要四个 Vector 实现。我只需要两个:PolarVector,它存储角度和幅度,并在运行中计算 x 和 y;CartesianVector,它存储 x 和 y,并在运行中计算角度和幅度。我将更新问题以尝试更好地解释。
  • 你的“重新考虑你的目标”评论是正确的。事实证明,PolarVector 并没有我最初想的那么有用。一旦我摆脱了这一点,我可以轻松地使用扩展类型我的协议到 Clojure 的向量来获取我的函数集,并且向量很容易与生成器一起使用。两全其美,感谢重新考虑我的目标。
【解决方案2】:

根据我们在 cmets 中的讨论,您似乎更喜欢使用协议的多态性。我想我明白你想做什么,并会尽力回应。

所以假设你有你的矢量界面:

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

【讨论】:

  • 如果这里需要耦合到具体的实现,您的回答是一个很好的回答,但我现在留下这个问题,看看是否有其他可能的解决方案。
  • 嗯,你的意思是理想情况下,从::vector 规范生成样本不需要知道存在哪些具体实现?
  • 这就是我所希望的......但考虑到你的问题,我现在意识到所有生成器 都将抽象(规范定义)耦合到具体(具体实例满足该规范)。因此,如果我坚持使用协议,那么是的,我必须为生成器创建一个具体的实现。这意味着我真正的问题是:协议是表示 Vector 的最佳方式,还是我可以使用规范充分表示 Vector?如果答案是协议,那么我会做类似你的答案的事情。
  • 我有一个想法可能更适合您想要做的事情。我会更新我的答案...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-05-29
  • 1970-01-01
  • 2019-06-21
  • 1970-01-01
  • 2011-12-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多