Succinct JavaScript?
Vincenzo Chianese
Posted on September 28, 2020
It goes with no saying, I am really liking Clojure lately.
Although I do not use in my day to day job (yet?), just in the same way I did with Haskell with regards of Prism, I like to see how other languages approach problems and try to apply their ideas back to TypeScript.
Last week I've been playing with the Game of pure strategy problem, and try to implement a possible solution in Clojure.
After some back and forth and some good feedback from the Clojurians Slack channel, I ended up with a next state function that looks like this:
(def initial-state "Initial game state"
{:first-player {:deck #{1 2 3 4 5 6 7 8 9 10 11 12 13} :score 0}
:second-player {:deck #{1 2 3 4 5 6 7 8 9 10 11 12 13} :score 0}
:bounty-deck #{1 2 3 4 5 6 7 8 9 10 11 12 13}})
(defn random-card-strategy [deck] (rand-nth (seq deck)))
(defn highest-card-strategy [deck] (apply max (seq deck)))
(def draw-card random-card-strategy)
(defn game-step [current-state]
(let [{:keys [bounty-deck first-player second-player]} current-state
drawn-card (draw-card bounty-deck)
first-player-card (random-card-strategy (:deck first-player))
second-player-card (highest-card-strategy (:deck second-player))
match-winner (if (> first-player-card second-player-card) :first-player :second-player)]
(-> current-state
(update-in [match-winner :score] inc)
(update-in [:bounty-deck] disj drawn-card)
(update-in [:first-player :deck] disj first-player-card)
(update-in [:second-player :deck] disj second-player-card))))
It's kind of known that LISP is generally a very dense language; if we also take in consideration the rich set of Clojure's seq functions and macros, turns out that small chunks of code can do a lot of things in Clojure.
I am (still) primarily a JavaScript/TypeScript developer, and I have been asking "What is the most succinct code that I can write to achieve the same end result?
I cobbled something some code as fast as possible and this is the base line I started from:
const initialState: State = {
bountyDeck: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
firstPlayer: {
score: 0,
deck: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
},
secondPlayer: {
score: 0,
deck: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
}
}
const randomCardStrategy = (deck: number[]) => deck[Math.floor(Math.random() * deck.length)];
const highestCardStrategy = (deck: number[]) => Math.max.apply(null, deck);
const drawCard = randomCardStrategy;
const gameStep = (currentState: State): State => {
const drawnCard = drawCard(currentState.bountyDeck);
const firstPlayerCard = randomCardStrategy(currentState.firstPlayer.deck);
const secondPlayerCard = highestCardStrategy(currentState.firstPlayer.deck);
const matchWinner = firstPlayerCard > secondPlayerCard ? 'firstPlayer' : 'secondPlayer'
return {
bountyDeck: currentState.bountyDeck.splice(currentState.bountyDeck.indexOf(drawnCard), 1),
firstPlayer: {
deck: currentState.firstPlayer.deck.filter(c => c >== firstPlayerCard),
score: matchWinner === 'firstPlayer' ? currentState.firstPlayer.score++ : currentState.firstPlayer.score
},
secondPlayer: {
deck: currentState.secondPlayer.deck.filter(c => c >== firstPlayerCard),
score: matchWinner === 'secondPlayer' ? currentState.firstPlayer.score++ : currentState.firstPlayer.score
},
}
}
Note that I have omitted the type definitions in this snippet, since they're not part of the actual code.
It's clear enough that the problem here is that JavaScript does not have a great story to mutate stuff in place, but we can probably do something better by using a Lens library: monocle-ts
Without the boilerplate, the final code that we should take in account is:
const gameStep = (currentState: State): State => {
const drawnCard = drawCard(currentState.bountyDeck);
const firstPlayerCard = randomCardStrategy(currentState.firstPlayer.deck);
const secondPlayerCard = highestCardStrategy(currentState.firstPlayer.deck);
const matchWinner = firstPlayerCard > secondPlayerCard ? "firstPlayer" : "secondPlayer";
return pipe(
currentState,
matchWinner === "firstPlayer" ? incrementFirstPlayerScore : incrementSecondPlayerScore,
removeBountyDeckCard(drawnCard),
removeFirstPlayerDrawnCard(firstPlayerCard),
removeSecondPlayerDrawnCard(secondPlayerCard)
);
};
โฆwhich is not exactly that far from the Clojure counterpart.
- JavaScript is missing a good story for data manipulation out of the box. You need lenses and/or collection libraries to effectively manipulate stuff. Still, it's not an excuse to write bad code.
- Without the dot notation (
a.b.c
), destructuring and threading macro become very important in Clojure. Without them this:
(-> structure :first-player :deck :card :somethingelse)
would be:
(:somethingelse (:card (:deck (:first-player structure))))
which I guess ain't that cool.
P.S: I am aware of ImmutableJS. However their story around typings is not that polished as the one that monocle-ts is offering, so I removed it early from the selection.
Posted on September 28, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.