What minimum viable snippet would be good for trying out Light Table? Short, simple, instantly understandable, but not trivial. Suited for Clojure’s functional paradigm yet slightly atypical. How about a game? Two players, perfect information, multiple turns, binary options? How about a version of single-pile Nim?
Two players take turns removing either one or two stones from a single pile. The person who takes the last stone wins. Easy.
Since I don’t know Clojure well, I gravitated toward writing a first version in Haskell.
Haskell’s rich static type system facilitates type-driven design. Write out all the types for the program, then fill in the implementations. For the Nim game, I started by writing:
-- We have two players
data Player = P1 | P2
-- and a pile.
type Pile = Int
-- The state of a game is described by the size
-- of the pile and whose turn it is.
data Game = Game Pile Player
-- Players move by taking one or two stones.
type Move = Int
-- A move updates the game state.
move :: Game -> Move -> Game
With very specific types, Haskell requires that you pay very special attention in situations where other languages let you ignore details. For example to play a game, each player needs a strategy for picking options. With a human player, we ask the person what move to make and that means IO. So I ended up with two stategy types and a function to taint pure strategies:
type PureStrategy = Game -> Options -> Move
type Strategy = Game -> Options -> IO Move
taint :: PureStrategy -> Strategy
Does Haskell bog us down in details or are these exactly the sort of details we want to be paying attention to? I’m still not sure, but it is always thought provoking.
I’m sure about point-free style either. Instead of defining functions by passing named arguments around, point-free uses function composition and combination in sometimes elegant, sometimes rediculous, sometimes whimsical ways:
-- The so-called 'dot' combinator.
(.:) :: (b -> c) -> (a -> a' -> b) -> a -> a' -> c
(.:) = (.) . (.)
taint :: PureStrategy -> Strategy
taint = (return .:)
I’m not sure what to make of point-free style, but it too is always thought provoking.
The Clojure version is a fairly direct translation of the Haskell version.
Besides the immediately noticeable addition of parenthesis, there are a few other natural changes:
- There are no declared types.
- Use keywords
:P1
instead of data constantsP1
. - Use maps
{:pile 14 :player :P1}
instead of data constructorsGame 14 P1
. - Uncurry functions.
Use elimination forms instead of pattern matching:
-- Clojure Elimination via Map Lookup (defn move [game move] {:pile (- (:pile game) move) :player (next (:player game))}) -- Haskell Pattern Matching move :: Game -> Move -> Game move (Game n p) m = Game (n - m) (next p)
Use
nil
directly instead ofMaybe
:-- Clojure nil Comparison (defn winner-is [game player] (= (winner game) player)) -- Haskell Maybe Wrapping winnerIs :: Game -> Player -> Bool g `winnerIs` p = winner g == Just p
Reply on implicit effects instead of monadic threading:
-- Clojure Implicit Effects (defn until [test action initial] (loop [v initial] (if (test v) v (recur (action v))))) -- Haskell Monadology untilM :: Monad m => (a -> Bool) -> (a -> m a) -> a -> m a untilM p f x = rec $ return x where rec mx = do x <- mx if p x then return x else rec $ f x
Clojure comes off as small and well designed, a perfectly acceptable Lisp. Two facets were a little irritating:
- “Can only recur from tail position” and “Cannot recur from catch/finally”.
let
still requires a little too much syntax for convenient use.
Yes, Clojure makes let
easier than other Lisps:
-- Common Lisp and Scheme let
(let ((x 6) (y 9)) (* x y))
-- Clojure let
(let [x 6 y 9] (* x y))
But I would prefer a macro more like this:
-- with Macro for the Parenthetically Impared
(with x = 6 y = 9 (* x y))
To each their own.