Effects

With our business logic defined and tested, we can proceed to coding and integrating effects. The tic-tac-toe example has two effects: the UI and the (faked) AI service. Let's start with the latter.

(def ai-latency 100) (defonce hackorama (do (add-watch session-atom :move-x (fn [_ _ _ session] (async/go (<! (async/timeout (/ (* ai-latency (+ 1 (rand))) 2))) (when (not-empty (rules/query-partial session ::simple/move-request :?player :x)) (let [moves (map :?move (rules/query-partial session ::simple/move)) board (simple/squares->board moves) next-move (ai/choose-move board :x 0) move-request (rules/query-one :?request session ::simple/move-request :?position next-move :?player :x)] (common/respond-to move-request move-request)))))) (reset! session-atom (init-session simple/session)) ;;; Comment out view/run to run tests headless (view/run session-atom)))

The above code occurs in provisdom.simple.app. For the AI, the relevant part is the add-watch. Since our session state is held in an atom, effects will generally run as watches on session-atom. To fake the latency of a call to a service, we'll wrap the code in the watch inside a clojure.core.async/go block, and start with a random delay. We then proceed to query for the relevant requests, call the AI to generate the next move, followed by common/respond-to with the appropriate request and response data.

Note that the :move-x watch gets called on every session change. So we query for ::MoveRequest's for :x only. If none exist, either because it is the human player's turn or the game is over, then nothing happens. We again have simpler code and less coupling due to the self-describing nature of R³. We don't need to ask specific questions of the state, such as whose turn it is or which squares are marked, but just ask for the requests that might be relevant. Also, it should be clear that while we faked it here, replacing this code with an actual AJAX call would be pretty trivial. In particular, we would want to wire up the call to common/respond-to in the response callback, and perhaps add some error handling.

The call to view/run is, of course, wiring up the effect to render the UI. Let's look at the implementation in provisdom.simple.view.

(ns provisdom.simple.view (:require [rum.core :as rum] [provisdom.maali.common :as common] [provisdom.maali.rules :as rules] [provisdom.simple.rules :as simple])) (defn click-handler [request] (when request #(common/respond-to request request))) ;;; Markup and styling from https://codepen.io/leesharma/pen/XbBGEj (defn tile [session position] (let [c (click-handler (rules/query-one :?request session ::simple/move-request :?position position :?player :o)) marker (rules/query-one :?player session ::simple/move :?position position) win? (rules/query-one :?winning-square session ::simple/winning-square :?position position)] [:td {:id (str position) :class (cond-> "tile" win? (str " winningTile") c (str " clickable")) :on-click c} (condp = marker :x "X" :o "O" "")])) (rum/defc app < rum/reactive [session-atom] (let [session (rum/react session-atom) tiles (map #(into [:tr] (map (partial tile session)) %) (partition 3 3 (range 9))) reset-request (rules/query-one :?request session ::simple/reset-request) current-player (rules/query-one :?player session ::simple/current-player) game-over (rules/query-one :?game-over session ::simple/game-over) winner (rules/query-one :?player session ::simple/winner)] [:div [:h1 "Tic-Tac-Toe"] [:h2 (if game-over (condp = winner :x "X wins - BOW TO YOUR COMPUTER OVERLORD!" :o "O wins - I think I've been Kobayashi Maru'd" "Tie game. You are most logical, hooman.") (condp = current-player :x "I'm thinking - don't rush me!" :o "Your move hooman." "Uh-oh"))] [:div {:id "left"}] [:div {:id "right"}] [:div {:id "top"}] [:div {:id "left"}] [:table {:id "board"} (into [:tbody] tiles)] [:button (cond-> {:id "reset"} reset-request (assoc :on-click #(common/respond-to reset-request)) (not reset-request) (assoc :disabled true)) "Reset Board"] [:p "Welcome to Tic-Tac-Toe! The objective of this game is to get three of your symbol (either X or O) in a row vertically, horizontally, or diagonally."] [:p "Good luck!"]])) (defn ^:export run [session-atom] (rum/mount (app session-atom) (js/document.getElementById "app")))

The markup here as well as the styles were derived from a CodePen example by Lee Sharma. Again, we use rum to define React components. The run function simply mounts the root app component into the DOM. app itself uses the rum/reactive mixin, and though it isn't explicit, just as with the AI this just adds a watch to session-atom and triggers the appropriate React lifecycle methods when the session is updated.

The render logic itself is pretty straightforward. As with the AI effect, we make queries of interest for requests and data, and perform some conditional rendering based on such. Notice in particular how click-handlers are conditionally added only when the associated requests exists. And of course the actual handler function is just wrapping the call to common/respond-to.

There are a couple of key takeaways about effects in R³:

  • All effects are treated equally. The view does not hold a privileged spot in the information flow, as it does in most of the examples of Unidirectional User Interface Architectures.
  • Effects receive information through queries, conditionally act on that information, and may (through whatever mechanism) provide information through responses.
  • General business logic should be encoded in rules, while logic that is specific to an effect implementation should be handled by that implementation. We could, for instance, have written rules to output the CSS style string for a tile, but that's specific to the implementation of rendering HTML, so really belongs with the effect.

results matching ""

    No results matching ""