Clojure: моделирование простых отношений «многие ко многим»

Так как я изучаю испанский в данный момент, я делаю очень простое приложение Flashcard.

Приложение имеет две концепции:

  1. Сами карты. Две струны, одна спереди, другая сзади. Кроме того, каждая карта помечена тегами 0-m. Например. теги для данной карты могут быть ["spanish" "verb"].
  2. Профили. В профиле хранятся две вещи: какие карты включаются путем определения тегов и «оценка знаний» для каждой карты.

Приложение работает, просто выбрав профиль для практики, выдает лицевую сторону карты с наименьшим баллом знаний. Когда пользователь готов, он показывает обратную сторону. Затем пользователь вводит, запомнил ли он эту карту, что изменяет оценку знания этой карты.

Для тех, кто раньше использовал какое-либо приложение 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 в профиле.

Следующий вопрос, который возникает, — сохранение состояния. Я мог бы, конечно, просто сохранить это состояние прямо в файле, но если бы я расширил это до многопользовательского режима, создав веб-приложение, я бы выбрал базу данных SQL. На мой взгляд, я должен иметь возможность закодировать все это и сохранить в файл в начале, а позже иметь возможность поменять способ хранения данных, не касаясь структуры данных, которую приложение использует для работы.

Будем признательны за любые советы и опыт!

У меня такое ощущение, что приложение слишком простое, чтобы получить какие-либо преимущества Clojure. Особенно при введении базы данных, что в основном просто сделало бы это приложение CRUD.


person Knut Saua Mathiesen    schedule 02.02.2019    source источник
comment
какой интерфейс вы собираетесь использовать? Командная строка, SPA-сеть, обычная сеть...?   -  person myguidingstar    schedule 07.02.2019


Ответы (2)


Я бы, наверное, начал с того, что сначала немного разобрал вещи

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

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

(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}
person John Newman    schedule 03.02.2019

если вы знакомы с SQL, вам следует сразу начать с библиотеки Walkable sql и sqlite: http://walkable.gitlab.io Вы получите большую выгоду от нормализации SQL. Walkable упростит получение данных в виде древовидной структуры с фильтрацией всего несколькими нажатиями клавиш. Не тратьте время на борьбу с атомом, домен не сложный, не стоит тратить время на создание прототипов CRUD.

person myguidingstar    schedule 07.02.2019