Clojure:建模简单的多对多关系

Clojure: Modeling simple many to many relationship

因为我现在正在学习西班牙语,所以我正在制作一个非常简单的抽认卡应用程序。

应用有两个概念:

  1. 卡片本身。两根绳子,一根在前面,一根在后面。此外,每张卡片都标有 0-m 标签。例如。给定卡片的标签可以是 ["spanish" "verb"].
  2. 个人资料。配置文件存储两件事:通过定义标签包含哪些卡片,以及每张卡片的 "knowledge score"。

该应用程序只需选择一个配置文件进行练习即可工作,为您提供知识得分最低的卡片正面。当用户准备好时,它会显示背面。然后用户输入是否记得那张卡片修改那张卡片的知识分数。

对于以前使用过任何 Flashcard 应用程序的人来说,这是非常微不足道的事情。

我的问题是:如何在 Clojure 中以惯用的方式对此进行建模?我面临的挑战是配置文件和卡片之间的多对多关系。

我可以创建这样的状态图:

{:card-universe [
  {:front "Correr" :back "To run" :tags ["spanish" "verb"]}
  {:front "Querer" :back "To want" :tags ["spanish" "verb"]}
  {:front "La mesa" :back "The table" :tags ["spanish" "noun"]}]

 :profiles [
  {
   :name "Spanish verbs"
   :tags ["spanish" "verb"] 
   :cards [{:front "Correr" :back "To want" :score 7}
           {:front "Querer" :back "To want" :score 10}]
  }
  {
   :name "Spanish"
   :tags ["spanish"] 
   :cards [{:front "Correr" :back "To run" :score 8}
           {:front "Querer" :back "To want" :score 3}
           {:front "La mesa" :back "The table" :score 2}]
  }
 ]
}

这对我来说似乎很愚蠢。假设我编辑了一张卡片,因为我犯了一个错误,那么我将不得不检查所有的配置文件并更新它们。我可以通过为所有卡片创建标识来(某种程度上)解决这个问题,然后只用它来引用卡片:

{:card-universe [
  {:id "c1" :front "Correr" :back "To run" :tags ["spanish" "verb"]}
  {:id "c2" :front "Querer" :back "To want" :tags ["spanish" "verb"]}
  {:id "c3" :front "Mesa" :back "Table" :tags ["spanish" "noun"]}]

 :profiles [
  {
   :name "Spanish verbs"
   :tags ["spanish" "verb"] 
   :cards [{:id "c1" :score 7}
           {:id "c2" :score 10}]
  }
  {
   :name "Spanish words"
   :tags ["spanish"] 
   :cards [{:id "c1" :score 8}
           {:id "c2" :score 3}
           {:id "c3"  :score 2}]
  }
 ]
}

这可能好一点,但这仍然意味着如果我在给定标签中添加更多卡片,我将不得不获取所有卡片。基本上是我的 :card-universe 和配置文件中的 :cards 之间的外部连接。

下一个弹出的问题是存储状态。我当然可以直接将此状态存储到一个文件中,但是如果我要通过创建 Web 应用程序将其扩展到多用户,那么 SQL 数据库将是我的首选。在我看来,我应该能够在开始时将所有这些编码并存储到一个文件中,然后能够在不触及应用程序用来运行的数据结构的情况下更换我存储数据的方式。

任何提示和经验将不胜感激!

我觉得应用程序太简单了,无法获得 Clojure 的任何好处。尤其是在引入数据库时​​——这基本上只会使它成为一个 CRUD 应用程序。

我可能会先把事情分开一点

(def card-data
  [{:id "c1" :front "Correr" :back "To run" :tags #{"spanish" "verb"}}
   {:id "c2" :front "Querer" :back "To want" :tags #{"spanish" "verb"}}
   {:id "c3" :front "Mesa" :back "Table" :tags #{"spanish" "noun"}}])

(defn spanish-words [cards]
  (filter #(-> % :tags (every? ["spanish"])) cards))

(defn spanish-verbs [cards]
  (filter #(-> % :tags (every? ["spanish" "verb"])) cards))

然后做一个小的atom db用于测试,里面有一个可以存储状态的函数。您可以稍后在您最终使用的任何数据库上抽象此函数。

(def db (atom {}))

(defn remembered! [scores-db card]
  (swap! scores-db update (:id card) #(if % (inc %) 0)))

现在我们可以测试一下了。

#_user=> (->> card-data spanish-verbs first (remembered! db))
{"c1" 0}
#_user=> (->> card-data spanish-verbs second (remembered! db))
{"c1" 0, "c2" 0}
#_user=> (->> card-data spanish-verbs first (remembered! db))
{"c1" 1, "c2" 0}

行得通。但是我们可以进一步将我们的过滤抽象成一个 select-tags 函数。

(defn select-tags [cards & tags]
  (filter #(-> % :tags (every? (->> tags flatten (remove nil?)))) cards))

(defn spanish [cards & tags]
  (select-tags cards "spanish" tags))

(defn verbs [cards & tags]
  (select-tags cards "verb" tags))

#_user=> (spanish (verbs card-data))
({:id "c1", :front "Correr", :back "To run", :tags #{"verb" "spanish"}} {:id "c2", :front "Querer", :back "To want", :tags #{"verb" "spanish"}})
#_user=> (verbs (spanish card-data))
({:id "c1", :front "Correr", :back "To run", :tags #{"verb" "spanish"}} {:id "c2", :front "Querer", :back "To want", :tags #{"verb" "spanish"}})

现在我们可以组合它们了。

(defn spanish-verbs [cards & tags]
  ((comp spanish verbs) cards tags))
;; or (apply spanish cards "verb" tags)
;; or even (apply select-tags cards "verb" "spanish" tags)

#_user=> (->> card-data spanish-verbs first (remembered! db))
{"c1" 2, "c2" 0}

如果您熟悉 SQL,您应该立即开始使用 Walkable sql 库和 sqlite: http://walkable.gitlab.io 您将从 SQL 的规范化中受益匪浅。 Walkable 将使以树结构形式获取数据变得轻而易举,只需按几下按键即可进行过滤。别把时间浪费在跟原子斗,领域并不复杂,不值得花时间做 CRUD 原型。