Статьи

Clojure: обработка состояния путем обновления вектора внутри атома

Как я уже упоминал в  предыдущем сообщении в блоге , я изучал алгоритмы ранжирования и хотел применить их к серии футбольных матчей, чтобы увидеть, какая команда является самой сильной.

До этого, однако, я хотел набросать функции, которые мне нужны для этого, и я начал со следующих коллекций матчей и командного рейтинга:

(def m
  [{:home "Manchester United", :away "Manchester City", :home_score 1, :away_score 0}
   {:home "Manchester United", :away "Manchester City", :home_score 2, :away_score 0}])
 
(def teams
  [ {:name "Manchester United" :points 1200}
    {:name "Manchester City" :points 1200} ])

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

Я не был уверен, как перебирать матчи и передавать обновленную коллекцию команд, поэтому я решил обернуть команды в  атом,  который я мог бы обновить:

(def t (atom teams))

Следующим шагом было выяснить, как обновить вектор внутри атома  t . Функция  Assoc  полезна здесь. Если мы хотим обновить рейтинг Манчестер Юнайтед, мы могли бы написать следующий код:

> (map #(if (= "Manchester United" (:name %))
         (assoc % :points 1500)
         %)
      teams)
[{:name "Manchester United", :points 1500} {:name "Manchester City", :points 1200}]

Мы планируем сбор команд и затем каждый раз проверяем, является ли команда «Манчестер Юнайтед». Если это так, мы обновляем значение ‘: points’, а если нет, то оставляем его в покое.

Следующий шаг — обновить вектор, на  который ссылается атом  t, что мы можем сделать, используя  своп!  функция:

> (swap! t
         (fn [teams]
           (map #(if (= "Manchester United" (:name %)) (assoc % :points 1500) %)
                teams)))
({:name "Manchester United", :points 1500} {:name "Manchester City", :points 1200})

Если мы посмотрим внутрь  t,  то увидим, что его ссылка также изменилась:

> @t
[{:name "Manchester United", :points 1500} {:name "Manchester City", :points 1200}]

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

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

(defn update-teams
  [teams team1 new-score1 team2 new-score2]
  (vec
   (map #(cond (= team1 (:name %)) (assoc % :points new-score1)
               (= team2 (:name %)) (assoc % :points new-score2)
               :else %)
        teams)))

Мы вызываем  vec  для результата, чтобы вернуться к вектору, который был у нас изначально. Мы будем заниматься обновлением ссылки на атом из другого места, эта функция обрабатывает только создание нового экземпляра базового вектора.

Теперь давайте вызовем эту функцию, пока мы перебираем совпадения, которые мы определили ранее:

> (map (fn [match]
       (swap! t (fn [teams]
                  (update-teams teams
                               (:home match)
                               (new-home-score match teams)
                               (:away match)
                               (new-away-score match teams)))))
     m)
([{:name "Manchester United", :points 1201} {:name "Manchester City", :points 1201}] [{:name "Manchester United", :points 1202} {:name "Manchester City", :points 1202}])

В этом случае я заглушил  новые оценки  и  новые оценки,  чтобы увеличить существующий рейтинг на единицу:

(defn new-home-score
  [match teams]
  (let [home-team (find-team (:home match) teams)]
    (inc (:points home-team))))
 
(defn new-away-score
  [match teams]
  (let [away-team (find-team (:away match) teams)]
    (inc (:points away-team))))
 
(defn find-team [team teams]
  (first
   (filter #(= team (:name %)) teams)))

Если бы мы использовали реальный алгоритм, мы бы присваивали очки победителю и забирали их у проигравшего.

Хотя карта совпадений фактически возвращает коллекцию, показывающую обновленные рейтинги после каждого совпадения, если мы хотим получить доступ к текущим рейтингам, мы бы предпочли атом  t,  как мы делали ранее:

> @t
[{:name "Manchester United", :points 1202} {:name "Manchester City", :points 1202}]

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