import Control.Concurrent hiding (readMVar) import Control.Monad import Haste.DOM import Haste.Events import Haste.Foreign import Haste.JSString (pack) import Data.Graph.Inductive.Graph hiding (Node) import qualified Data.Graph.Inductive.Graph as G import Data.Graph.Inductive.PatriciaTree import Data.List import Data.Maybe import qualified Data.Map.Strict as M import Data.Tree import Control.Arrow import Text.Parsec
Deduced again (naturally)
When mathematicians first studied formal logic, they built Hilbert systems, where proving theorems corresponds to programming with combinators.
APL and its ilk show the power of this approach. However, some functions are easier to describe if we can name their arguments. In logic, analogous concerns drove Gentzen to devise natural deduction:
Hint
See Also
-
Implicational propositional calculus. Along with a false proposition, the three rules ⇒I , ⇒E, and LEM suffice for classical propositional logic. LEM is equivalent to Peirce’s law, which means we can remove false from the axioms completely.
-
Abandoning LEM makes sense for programming languages based on lambda calculus. Roughly speaking, LEM means two wrongs make a right. For instance, if
f
is a function that takes an integer but is buggy then LEM is sort of claiming we can feedf
to another buggy function to somehow cancel out the bugs and magically produce an integer! On the other hand, LEM makes sense in lambda-mu calculus.
Ideas
-
Intuitionistic natural deduction satisfies the subformula property: a theorem can be proved solely from hypotheses that appear somewhere in the theorem.
We could add some levels where the theorem is shown as a tree, and the player can select subtrees to create hypotheses.
-
Theorems proved in previous levels ought to be available in later levels.
-
We could add ∀ and ∃ for first-order logic. Or add Π and Σ types and move towards homotopy type theory. Or both!
-
Show the Haskell equivalent of a proof.
A Haste Bug
We use Data.Graph.Inductive.Graph
to store the proofs in progress.
Unfortunately,
a Haste bug complicates
installing this package. Hopefully this will be fixed soon, but for now:
-
Run
haste-cabal install fgl
. This fails. -
Extract the contents of the
fgl
package’s archive. -
Remove all lines containing
ANN
from all source files. -
Re-archive the files and replace the original.
-
Re-run
haste-cabal install fgl
.
Tree drawing
We copy our tree drawing code, and extend it to handle nodes of variable width, as well as nodes with only one child.
data RT a = RT { xpos :: Int , shift :: Int , hold :: a , link :: Maybe (Int, RT a) , kids :: [RT a] } deriving Show addx :: Int -> RT a -> Tree (Int, a) addx i rt = Node (xpos rt + shift rt + i, hold rt) $ addx (i + shift rt) <$> kids rt padding :: Int -- Minimum horizontal gap between nodes. padding = 50 placeRT :: (a -> Int) -> Tree a -> RT a placeRT _ (Node a []) = RT 0 0 a Nothing [] placeRT width (Node a [k]) = RT (xpos r) 0 a Nothing [r] where r = placeRT width k placeRT width (Node a [l, r]) = RT m 0 a Nothing xs where [ll, rr] = placeRT width <$> [l, r] g = padding - minimum (zipWith (-) (contour (((-1) *) . width) head (0, rr)) (contour width last (0, ll))) s = xpos ll + xpos rr gap = abs g + mod (abs g + s) 2 -- Adjust so midpoint is whole number. m = div (s + gap) 2 xs = if g >= 0 then weave ll rr { shift = gap } else weave ll { shift = gap } rr placeRT _ _ = error "binary trees only please" drawRT :: (a -> Int) -> Tree a -> Tree (Int, a) drawRT width = addx 0 . placeRT width contour :: (a -> Int) -> ([RT a] -> RT a) -> (Int, RT a) -> [Int] contour width f (acc, rt) = h : case kids rt of [] -> maybe [] (contour width f . first (+ acc')) (link rt) ks -> contour width f (acc', f ks) where acc' = acc + shift rt h = acc'+ xpos rt + width (hold rt) weave :: RT a -> RT a -> [RT a] weave l r = [weave' id (0, l) (0, r), weave' reverse (0, r) (0, l)] weave' :: ([RT a] -> [RT a]) -> (Int, RT a) -> (Int, RT a) -> RT a weave' f (accL, l) (accR, r) | Nothing <- follow = l | Just (dx, x) <- link l = l { link = Just (dx, weave' f (dx + accL', x) y) } | (k:ks) <- f $ kids l = l { kids = f $ weave' f (accL', k) y : ks } | otherwise = l { link = first (+(-accL')) <$> follow } where accL' = accL + shift l accR' = accR + shift r follow | (k:_) <- f $ kids r = Just (accR', k) | otherwise = first (accR' +) <$> link r Just y = follow
Expression parser
In free play mode, the player can type in arbitrary hypotheses, which we
read using parser combinators. The OrHalf
is a
special value that the ∨-elimination rule puts in a placeholder node in
order to keep the tree binary.
data Expr = Expr :-> Expr | Expr :& Expr | Expr :+ Expr | Var String | Bot | Top | OrHalf deriving Eq instance Show Expr where show OrHalf = "\x2228" show Bot = "⊥" show Top = "⊤" show (Var s) = s show (x :-> y) = showL x ++ "\x21d2" ++ show y where showL e = case e of _ :-> _ -> "(" ++ show e ++ ")" _ -> show e show (x :& y) = showAnd x ++ "\x2227" ++ showAnd y where showAnd t@(_ :-> _) = "(" ++ show t ++ ")" showAnd t@(_ :+ _) = "(" ++ show t ++ ")" showAnd t = show t show (x :+ y) = showOr x ++ "\x2228" ++ showOr y where showOr t@(_ :-> _) = "(" ++ show t ++ ")" showOr t = show t type Parser = Parsec String () proposition :: Parser Expr proposition = spaces >> expr <* eof where expr = atm `chainl1` and' `chainl1` or' `chainr1` imp atm = between (sp $ char '(') (sp $ char ')') expr <|> do s <- sp $ many1 alphaNum pure $ case s of "0" -> Bot "1" -> Top _ -> Var s imp = sp (string "->" <|> string "=>") >> pure (:->) and' = sp (string "&" <|> string "*") >> pure (:&) or' = sp (string "|" <|> string "+") >> pure (:+) sp = (<* spaces) parseProp :: String -> Expr parseProp s = x where Right x = parse proposition "" s
Levels
We look up a level’s specifications from an association list.
A hidden element in the HTML file stores level titles, hints, and victory messages.
data Level = Level Expr [Expr] String | FreePlay getLevel :: Int -> Level getLevel n | Just ((goal, hs), rules) <- lookup n lvls = Level (parseProp goal) (parseProp <$> hs) rules | otherwise = FreePlay where lvls = zip [1..] $ [ (("a->a", ["a"]), "impliesI") , (("a->b->a", ["a", "b"]), "impliesI") , (("a->(a->b)->b", ["a", "a->b"]), "impliesI impliesE") , (("(a->b->c)->(a->b)->a->c", ["a","a","a->b","a->b->c"]), "impliesI impliesE") ] ++ (`zip` (repeat "classy")) [ ("((a->0)->0)->a", ["a->0","(a->0)->0"]) , ("0->a", ["0","a->0","a->0"]) , ("(a->0)->a->b", ["a","a->0","b->0","b->0"]) , ("((a->0)->a)->a", ["a->0","a->0","(a->0)->a"]) , ("((a->b)->a)->a", ["a","a->0","a->0","b->0","b->0","(a->b)->a"]) , ("((a->b)->c)->((c->a)->(d->a))", ["a","a->0","a->0","b->0","b->0","d","c->a","(a->b)->c"]) ] ++ [ (("a&b->b", ["a&b"]), "impliesI impliesE and1E and2E") , (("a&b->b&a", ["a&b","a&b"]), "impliesI impliesE andI and1E and2E") , (("a->a+b", ["a","b"]), "impliesI impliesE or1I or2I orE") , (("a+b->b+a", ["a+b","a","a","b","b"]), "impliesI impliesE or1I or2I orE") , (("(a->c)->(b->c)->a+b->c", ["a", "b", "a->c", "b->c", "a+b"]), "impliesI impliesE or1I or2I orE") ] ++ (`zip` (repeat "intu")) [ ("0->a", ["0", "a"]) , ("(a+(a->0)->0)->0", ["a","a","a->0","a+(a->0)->0","a+(a->0)->0"]) , ("a+b->(a->b)->b", ["a+b","a","b","a->b","a->b"]) , ("((((a->b)->b)->a+b)->0)->0", ["a","a","b","b","(a->b)->b","(a->b)->b","(((a->b)->b)->a+b)->0","(((a->b)->b)->a+b)->0"]) , ("a&b+a&c->a&(b+c)", ["b","c","a&b","a&b","a&c","a&c","a&b+a&c"]) , ("a&(b+c)->a&b+a&c", ["b","c","a&b","a&c","a&(b+c)","a&(b+c)","a&(b+c)"]) ]
More Haste workarounds
The Haste.DOM
functions handle SVG poorly, so we provide workarounds.
The readMVar
function causes compile errors so we supply our own.
newElemSVG :: String -> IO Elem newElemSVG = ffi $ pack $ "(x => document.createElementNS('http://www.w3.org/2000/svg', x))" getChildrenSVG :: Elem -> IO [Elem] getChildrenSVG = ffi $ pack $ concat ["(function(e){var ch = [];", "for(e = e.firstChild; e != null; e = e.nextSibling)", "{ch.push(e);}return ch;})"] readMVar :: MVar a -> IO a readMVar v = do x <- takeMVar v putMVar v x pure x
Tedium
Clunky UI code unfortunately intertwined with the rules of inference.
A proof in progress is a forest along with a record of all discharges. We represent the forest with an inductive graph which is well-suited for handling player clicks on any node.
With each edge, we associate an SVG line as well as an integer so out-edges are ordered.
We record the step that discharges a hypothesis, but we have no use for this information yet.
type Grx = Gr (Elem, Expr) (Elem, Int) type Dis = M.Map G.Node G.Node type Proof = (Grx, Dis) halfWidth :: Expr -> Int halfWidth x = 6 * (length $ show x) rootOf :: Grx -> G.Node -> G.Node rootOf g h = case pre g h of [] -> h [t] -> rootOf g t _ -> undefined foliage :: Grx -> G.Node -> [G.Node] foliage g y | null ss = [y] | otherwise = concatMap (foliage g) ss where ss = suc g y nodeElem :: Grx -> G.Node -> Elem nodeElem g = fst . fromJust . lab g nodeExpr :: Grx -> G.Node -> Expr nodeExpr g = snd . fromJust . lab g xbounds :: Grx -> Tree (Int, G.Node) -> (Int, Int) xbounds g (Node (x, n) ks) = foldl1' mm $ (x - w, x + w) : (xbounds g <$> ks) where w = halfWidth $ nodeExpr g n mm = uncurry (***) . (min *** max) drawGr :: Grx -> G.Node -> Tree (Int, G.Node) drawGr g node = drawRT (halfWidth . nodeExpr g) $ fromGraph node where fromGraph n = Node n $ fromGraph . fst <$> sortOn (snd . snd) (lsuc g n) discharge :: Grx -> G.Node -> G.Node -> Dis -> Dis discharge g x y dis = M.union dis $ M.fromList $ zip xs $ repeat y where xs = filter ((== nodeExpr g x) . nodeExpr g) (foliage g $ rootOf g x) elemjs :: Elem -> String -> IO () elemjs e s = (ffi $ pack s :: Elem -> IO ()) e defaultNode :: Elem -> IO () defaultNode e = do setStyle e "cursor" "" elemjs e "e => e.childNodes[0].setAttribute('stroke','grey')" elemjs e "e => e.childNodes[0].setAttribute('stroke-dasharray','1,1')" elemjs e "e => e.childNodes[1].removeAttribute('fill')" hypNode :: Elem -> IO () hypNode e = do setStyle e "cursor" "pointer" elemjs e "e => e.childNodes[1].setAttribute('fill','red')" elemjs e "e => e.childNodes[0].removeAttribute('stroke-dasharray')" elemjs e "e => e.childNodes[0].setAttribute('stroke','blue')" rootNode :: Elem -> IO () rootNode e = do setStyle e "cursor" "pointer" elemjs e "e => e.childNodes[0].setAttribute('stroke','black')" elemjs e "e => e.childNodes[0].removeAttribute('stroke-dasharray')" elemjs e "e => e.childNodes[0].setAttribute('stroke','blue')" draw :: Elem -> Int -> Grx -> Int -> Tree (Int, G.Node) -> IO () draw soil x0 g y (Node (x, n) ks) = do setAttr (nodeElem g n) "transform" $ "translate" ++ show (x0 + x, -40*y) mapM_ (draw soil x0 g $ y + 1) ks enableRule :: Elem -> IO () enableRule e = do setStyle e "border" "2px solid blue" setStyle e "color" "black" disableRule :: Elem -> IO () disableRule e = do setStyle e "border" "1px dotted grey" setStyle e "color" "grey" selectNode :: Elem -> IO () selectNode = ffi $ pack $ "e => e.childNodes[0].setAttribute('fill','yellow')" ageNode :: Elem -> IO () ageNode = ffi $ pack $ "e => e.childNodes[0].setAttribute('fill','orange')" deselectNode :: Elem -> IO () deselectNode = ffi $ pack $ "e => e.childNodes[0].setAttribute('fill','white')" theorem :: Proof -> Maybe Expr theorem (g, dis) | null live, [t] <- tips = Just $ nodeExpr g t | otherwise = Nothing where live = filter (`M.notMember` dis) $ filter (null . suc g) $ nodes g tips = filter (null . pre g) $ nodes g mustEldest :: Elem -> IO Elem mustEldest = fmap fromJust . getFirstChild main :: IO () main = withElems [ "soil", "winBar", "ruleBar", "hypoT", "newHypoB", "errT", "msgsDiv" , "impliesI", "impliesE", "notNot" , "andI", "and1E", "and2E", "or1I", "or2I", "orE", "falseE" , "againB", "nextB", "hintB", "hintT", "undoB", "hypoDiv", "preT", "postT" , "homeB", "homeB2", "backB", "homeTab", "gameTab"] $ \[ soil, winBar, ruleBar, hypoT, newHypoB, errT, msgsDiv , impliesI, impliesE, notNot , andI, and1E, and2E, or1I, or2I, orE, falseE , againB, nextB, hintB, hintT, undoB, hypoDiv, preT, postT , homeB, homeB2, backB, homeTab, gameTab] -> do styleElem <- newElem "style" [headElem] <- elemsByQS document "head" appendChild styleElem =<< newTextElem (concat [ ".logic{cursor:pointer;border:2px solid blue;" , "border-radius:10px;padding:5px;margin:5px;}" , ".warpButton{cursor:pointer;border:2px solid blue;border-radius:5px;" , "padding:5px;margin:10px;font-size:150%;text-align:center;" , "width:2em;display:inline-grid;}" , ".warpButton:hover{background-color:lightblue;}" , ".winbutton{cursor:pointer;border:4px solid blue;border-radius:10px;" , "padding:5px;margin:10px;font-size:400%}" ]) appendChild headElem styleElem msgsDivKids <- getChildren =<< mustEldest =<< mustEldest msgsDiv msgs <- forM msgsDivKids $ \e -> do -- Title, hint, victory message. ol <- getChildren =<< mustEldest . (!!1) =<< getChildren e eldest <- mustEldest e mapM (`getProp` "innerHTML") $ eldest : ol let classicRules = [impliesI, impliesE, notNot] intuRules = [impliesI, impliesE, andI, and1E, and2E, or1I, or2I, orE, falseE] allRules = classicRules ++ intuRules sel <- newMVar [] proof <- newMVar (mkGraph [] [], M.empty) acts <- newMVar [] history <- newMVar [] level <- newMVar undefined -- Replaced later by `setup`. let resetActs = swapMVar acts [] >>= mapM_ (\(e, h) -> disableRule e >> unregisterHandler h) nodeClick i = do (g, dis) <- readMVar proof when (null (suc g i) && M.notMember i dis || null (pre g i)) $ select g i activate :: Elem -> (Proof -> IO Proof) -> IO () activate e f = do enableRule e h <- e `onEvent` Click $ const $ do resetActs prf <- f =<< load save prf case theorem prf of Just t -> do n <- readMVar level case getLevel n of Level goal _ _ -> if t == goal then do setProp postT "innerHTML" $ msgs!!(n - 1)!!2 setStyle winBar "visibility" "visible" setStyle ruleBar "display" "none" setStyle hintB "display" "none" else setProp postT "innerHTML" $ "Proved " ++ show t ++ " but we want " ++ show goal _ -> setProp postT "innerHTML" $ "Proved " ++ show t _ -> setProp postT "innerHTML" "" modifyMVar_ acts $ pure . ((e, h):) ghost x y t (g, dis) = do deleteChild soil $ nodeElem g x g1 <- delNode x . snd <$> newProp g [y] t pure (g1, dis) dSpawn x y t (g, dis) = do (k, g1) <- newProp g [y] t pure (g1, discharge g1 x k dis) spawn ks t (g, dis) = do g1 <- snd <$> newProp g ks t pure (g1, dis) select g i = do let e = nodeElem g i xs0 <- takeMVar sel xs <- if elem i xs0 then do deselectNode e case delete i xs0 of [] -> pure [] xs1@(h:_) -> do selectNode $ nodeElem g h pure xs1 else do forM_ xs0 $ ageNode . nodeElem g selectNode e pure $ i:xs0 putMVar sel xs resetActs let exprOf = nodeExpr g isRoot = null . pre g isLeaf = null . suc g impi | [y, x] <- xs, isLeaf x, isRoot x, isRoot y -- [Del x, New x [y] $ exprOf x :-> exprOF y] = [(impliesI, ghost x y $ exprOf x :-> exprOf y)] | [x] <- xs, isLeaf x, y <- rootOf g x = [(impliesI, dSpawn x y $ exprOf x :-> exprOf y)] | [y] <- filter isRoot xs, [x] <- delete y xs, rootOf g x == y = [(impliesI, dSpawn x y $ exprOf x :-> exprOf y)] -- [Dis x, New x [y] $ exprOf x :-> exprOF y] | otherwise = [] mopo | [x, y] <- xs, isRoot x, isRoot y, a :-> b <- exprOf y, a == exprOf x = [(impliesE, spawn [x, y] b)] | [x, y] <- xs, isRoot x, isRoot y, a :-> b <- exprOf x, a == exprOf y = [(impliesE, spawn [y, x] b)] | otherwise = [] lem | ([y], [x]) <- partition isRoot xs, y == rootOf g x, Bot <- exprOf y, t :-> Bot <- exprOf x = [(notNot, dSpawn x y t)] | [x] <- xs, isLeaf x, y <- rootOf g x, Bot <- exprOf y, t :-> Bot <- exprOf x = [(notNot, dSpawn x y t)] | otherwise = [] conj | [y, x] <- xs, isRoot x, isRoot y = [(andI, spawn [x, y] $ exprOf x :& exprOf y)] | [x] <- xs, isRoot x, l :& r <- exprOf x = [(and1E, spawn [x] l), (and2E, spawn [x] r)] | otherwise = [] disj | [y, x] <- xs, isLeaf x, isRoot x, isRoot y = [(or1I, ghost x y $ exprOf y :+ exprOf x), (or2I, ghost x y $ exprOf x :+ exprOf y)] | length xs == 3 = take 1 $ concatMap orCheck $ permutations xs | otherwise = [] orCheck [x, y, z] | isLeaf x, isLeaf y, rx <- rootOf g x, ry <- rootOf g y, exprOf x :+ exprOf y == exprOf z, t <- exprOf rx, rx /= ry, t == exprOf ry = [(orE, \(g0, dis0) -> do (k, g1) <- newProp g0 [rx, ry] OrHalf g2 <- snd <$> newProp g1 [z, k] t pure (g2, discharge g0 y k $ discharge g0 x k dis0))] orCheck _ = [] efq | [y, x] <- xs, Bot <- exprOf y = [(falseE, ghost x y $ exprOf x)] | [x, y] <- xs, Bot <- exprOf y = [(falseE, ghost x y $ exprOf x)] | otherwise = [] mapM_ (uncurry activate) $ concat [impi, mopo, lem, conj, disj, efq] -- | Reads proof from MVar. Records it to undo history. Clears selection. load = do enableRule undoB p@(g, _) <- takeMVar proof swapMVar sel [] >>= mapM_ (deselectNode . nodeElem g) modifyMVar_ history $ pure . (p:) pure p translate :: Parser (String, String) translate = do void $ string "translate(" x <- many1 (digit <|> char '-') void $ string "," y <- many1 (digit <|> char '-') void $ string ")" pure (x, y) -- | Saves proof to MVar. Draws it. save (g, dis) = do putMVar proof (g, dis) let f [] d acc = pure (acc, d) f (r:rs) d acc = do draw soil (acc - x0) g 0 drawing f rs (max d dep) $ acc + x1 - x0 + 25 where drawing = drawGr g r (x0, x1) = xbounds g drawing dep = length $ levels drawing (bx1, by1) <- f (filter (null . pre g) $ nodes g) 0 0 forM_ (labEdges g) $ \(i, j, (l, _)) -> do Right (x1, y1) <- parse translate "" <$> getAttr (nodeElem g i) "transform" Right (x2, y2) <- parse translate "" <$> getAttr (nodeElem g j) "transform" setAttr l "x1" x1 setAttr l "y1" y1 setAttr l "x2" x2 setAttr l "y2" y2 forM_ (nodes g) $ \i -> let e = nodeElem g i in when (nodeExpr g i /= OrHalf) $ do defaultNode e when (null (suc g i) && M.notMember i dis) $ hypNode e when (null $ pre g i) $ rootNode e setAttr soil "viewBox" $ "-5 " ++ show (-40 * by1) ++ " " ++ show (bx1) ++ " " ++ show (40*by1 + 40) newProp g qs t = do let k = if isEmpty g then 0 else snd (nodeRange g) + 1 e <- newElemSVG "g" let w = halfWidth t er <- newElemSVG "rect" `with` [ attr "width" =: show (2 * w) , attr "height" =: "24" , attr "fill" =: "white" , attr "stroke" =: "black" , attr "x" =: show (-w) , attr "y" =: "-12" , attr "rx" =: "4" , attr "ry" =: "4" ] when (t == OrHalf) $ do setAttr er "stroke" "none" setAttr er "fill" "none" appendChild e er when (t /= OrHalf) $ do et <- newElemSVG "text" `with` [ attr "x" =: "0" , attr "y" =: "0" , attr "text-anchor" =: "middle" , attr "alignment-baseline" =: "central" , prop "textContent" =: show t ] appendChild e et appendChild soil e void $ e `onEvent` Click $ const $ nodeClick k ls <- replicateM (length qs) $ newElemSVG "line" `with` [ attr "stroke" =: "black" ] ks <- getChildrenSVG soil setChildren soil $ ls ++ ks pure (k, insEdges [(k, q, ln) | (q, ln) <- zip qs $ zip ls [0..]] $ insNode (k, (e, t)) g) addHypo t = do (g0, dis0) <- load (k, g1) <- newProp g0 [] t let dis1 = if t == Top then M.insert k k dis0 else dis0 save (g1, dis1) void $ undoB `onEvent` Click $ const $ do hist <- readMVar history case hist of [] -> pure () ((g, dis):rest) -> do void $ load let ls = (\(_, _, (l, _)) -> l) <$> labEdges g ks = fst . snd <$> labNodes g setChildren soil $ ls ++ ks setProp errT "innerHTML" "" setProp postT "innerHTML" "" save (g, dis) when (null rest) $ disableRule undoB void $ swapMVar history rest void $ newHypoB `onEvent` Click $ const $ do s <- getProp hypoT "value" case parse proposition "" s of Right x -> do setProp errT "innerHTML" "" addHypo x Left err -> setProp errT "innerHTML" $ show err let ruleset "intu" = pure intuRules ruleset "classy" = pure classicRules ruleset names = catMaybes <$> mapM elemById (words names) setup n = do void $ swapMVar level n forM_ allRules disableRule void $ swapMVar proof (mkGraph [] [], M.empty) void $ swapMVar sel [] clearChildren soil setStyle winBar "visibility" "hidden" setStyle ruleBar "display" "" case getLevel n of Level goal hs rules -> do setStyle hintB "display" "" setProp hintT "innerHTML" "" forM_ allRules $ \e -> setStyle e "display" "none" setStyle hypoDiv "display" "none" ruleset rules >>= mapM_ (\e -> setStyle e "display" "") mapM_ addHypo hs setProp preT "innerHTML" $ concat [ "<p>Level " , show n , ":<i>" , msgs!!(n - 1)!!0 , "</i></p><p><b>Theorem</b>:" , "<div style='text-align:center;font-size:150%'><span>" , show goal , "</span></div>" , "</p><b>Proof:</b>" ] setProp postT "innerHTML" "" FreePlay -> do setStyle hintB "display" "none" setProp hintT "innerHTML" "" forM_ allRules $ \e -> setStyle e "display" "" setStyle hypoDiv "display" "" setProp preT "innerHTML" "<h2>Free Play</h2><p>Type '=>' or '->' for implication, '0' and '1' for false and true.</p>" setProp postT "innerHTML" "" disableRule undoB void $ swapMVar history [] void $ nextB `onEvent` Click $ const $ do n <- takeMVar level putMVar level $ n + 1 setup $ n + 1 void $ againB `onEvent` Click $ const $ setup =<< readMVar level void $ hintB `onEvent` Click $ const $ do n <- readMVar level setStyle hintB "display" "none" setProp hintT "innerHTML" $ msgs!!(n - 1)!!1 void $ homeB `onEvent` Click $ const $ do setStyle gameTab "display" "none" setStyle homeTab "display" "" void $ homeB2 `onEvent` Click $ const $ do setStyle gameTab "display" "none" setStyle homeTab "display" "" let addWarp n = do e <- newElem "div" `with` [ attr "class" =: "warpButton" , prop "innerHTML" =: show n ] appendChild homeTab e void $ e `onEvent` Click $ const $ do setStyle gameTab "display" "" setStyle homeTab "display" "none" setup n void $ backB `onEvent` Click $ const $ do setStyle homeTab "display" "none" setStyle gameTab "display" "" forM_ [1..22] addWarp setup 1