Python vs. Clojure
Verfasst: Montag 6. Oktober 2008, 19:23
Hier ist ein kurzes Stück Python-Code, welches in einem Spiel, das Einheiten kennt, die Spielern gehören, den Städte besitzenden Spielern dafür Geld zahlt:
Das ist offensichtlich imperativ. Clojure bevorzugt einen funktionalen Stil, wo ich nicht einfach die Exemplarvariable `money` ändern kann. Stattdessen muss ich einen neuen Spielstand erzeugen, also neue Spieler als Teil eines neuen Spiel-Objekts.
Hier meine (in Python nicht gezeigten) Objektdefinitionen:
Leider kann ich nicht sowohl Spieler auf Einheiten und Einheiten auf Spieler verweisen lassen und muss daher IDs einführen, die ich stattdessen benutze. Das `def` definiert eine globale (und unveränderliche) Variable `*game*` als Struktur vom Typ `game`.
Dafür baue ich mir einige Zugriffsfunktionen:
Ich habe mit `all-cities` jetzt eine Liste alle Städte aller Spieler, kann aber jetzt nicht einfach Spieler-Objekte verändern, sondern sammle die Einkommen in einer Map:
Mittels `assoc` kann ich einzelne Werte einer Map aktualisieren und erhalte dann eine neue Map zurück. Die obige Funktion sammelt daher unter dem Schlüssel eines Spielerobjekts die aufsummierten Einkommen.
Nun muss ich ein neues Spiel bauen, welches eine neue Liste mit neuen Spielerobjekten hat, deren Geld das neue Einkommen enthält.
Ganz am Schluss leiste ich mir den Luxus, eine globale Variable zu ändern, was nur innerhalb eines `binding` funktioniert, wodurch man dann eine globale Variable mit einem anderen Wert überschatten kann:
Irgendwie ist der Python-Code einfacher :)
Hier sind noch zweite Varianten:
Die erste Form ist in Python umständlicher, lässt sich aber besser in Clojure überführen:
Die zweite Form ist funktional und entspricht ziemlich genau der Clojure-Version. Sie kommt ohne globale Variable aus, da das Spiel übergeben und ein neues Spiel mit einer Spielerliste zurückgegeben wird.
Clojure kennt übrigens eine Funktion `update-in`, mit der man (ineffizient) folgendes machen kann:
Die Funktion wendet die in `ks` übergebenen Funktionen der Reihe nach auf `@game` an, holt sich also zuerst die Liste der Spieler, dann den durch die Einheit definierten Spieler, dort das Geld und wendet jetzt auf den erhaltenen Wert die übergebene Funktion an, die zu dem existierenden Wert (der durch `%` in dieser Kurzform einer Funktion markiert ist) das durch den Einheitentyp definierte Einkommen addiert. Danach werden alle beteiltigen Objekte kopiert und der veränderte Wert aktualisiert. Ich nutze eine `ref`, eine in einer mit `dosync` definierten Transaktion änderbare globale Variable, auf deren Wert ich mit `@` zugreifen kann.
Ich speichere jetzt in `:player` nicht mehr die Nummer, sondern einen Index, aber ansonsten ist das jetzt fast genauso kompakt die der ursprüngliche Python-Code.
Stefan
PS: Leider ist die Entwicklung mit Clojure zur Zeit PITA^2, da einembei Fehlern nichtssagende Java-Exceptions um die Ohren fliegen und die API-Doku in Form einer alphabetischen Liste aller Funktionen und Macros absolut unzureichend ist.
Code: Alles auswählen
def income():
for u in game.units:
if u.type == ut_city:
u.player.money += u.type.income
Hier meine (in Python nicht gezeigten) Objektdefinitionen:
Code: Alles auswählen
(defstruct game :players :units)
(defstruct player :no :money :units)
(defstruct unit :no :type :player)
(defstruct unittype :income)
(def ut-city (struct unittype 10))
(def *game* (struct game
[(struct player 1 5 [1])
(struct player 2 0 [2 3])]
[(struct unit 1 ut-city 1)
(struct unit 2 ut-city 2)
(struct unit 3 ut-city 2)]))
Dafür baue ich mir einige Zugriffsfunktionen:
Code: Alles auswählen
(defn all-players [] (*game* :players))
(defn all-units [] (*game* :units))
;; ich finde kein "detect" oder "find" in der echt miesen Dokumentation...
(defn find-by-no [no coll] (first (filter #(= (% :no) no coll)))
(defn player [no] (find-by-no no (all-players)))
(defn unit [no] (find-by-no no (all-units)))
(defn is-city [u] (= (:type u) ut-city))
(defn all-cities [] (filter is-city (all-units)))
Code: Alles auswählen
(defn income-per-players []
(reduce (fn [m c]
(let [p (player (:player c)) income (:income (:type c))]
(assoc m p (+ (or (m p) 0) income))))
{}
(all-cities)))
Nun muss ich ein neues Spiel bauen, welches eine neue Liste mit neuen Spielerobjekten hat, deren Geld das neue Einkommen enthält.
Code: Alles auswählen
(defn add-income [new-income]
(map (fn [p] (assoc p :money (+ (:money p) (or (new-income p) 0))))
(all-players)))
Code: Alles auswählen
(defn income []
(set! *game* (assoc *game* :players (add-income (income-per-players)))))
Hier sind noch zweite Varianten:
Code: Alles auswählen
def income():
for p in game.players:
p.money = p.money + sum(u.type.income for u in p.units if u.type == ut_city)
def income(game):
return update(game, {
'players',
map(lambda p: update(p, {
'money':
p.money + sum(u.type.income for u in p.units if u.type == ut_city)
}), game.players)
})
def update(obj, dct):
import copy
nobj = copy.copy(obj)
for k, v in dct.items():
setattr(nobj, k, v)
return nobj
Code: Alles auswählen
(defn money-of-player [p]
(reduce +
(:money p)
(for [u (map unit (:units p)) :when (is-city u)] (:income (:type u)))))
(defn players-with-updated-income []
(map (fn [p] (merge p {:money (money-of-player p)})) (all-players)))
(def update-players! [players] (set! *game* (assoc *game* :players players)))
(defn income [] (update-players! (players-with-updated-income)))
Clojure kennt übrigens eine Funktion `update-in`, mit der man (ineffizient) folgendes machen kann:
Code: Alles auswählen
(def game (ref (struct ...)))
(defn update-game-in [ks f v]
(dosync (ref-set game (update-in @game ks #(f % v)))))
(defn income []
(for [u (:units @game) :when (is-city u)]
(update-game-in [:players (:player u) :money] + (:income (:type u)))))
Ich speichere jetzt in `:player` nicht mehr die Nummer, sondern einen Index, aber ansonsten ist das jetzt fast genauso kompakt die der ursprüngliche Python-Code.
Stefan
PS: Leider ist die Entwicklung mit Clojure zur Zeit PITA^2, da einembei Fehlern nichtssagende Java-Exceptions um die Ohren fliegen und die API-Doku in Form einer alphabetischen Liste aller Funktionen und Macros absolut unzureichend ist.