Tic-Tac-Toe: Haskell

Haskell is seriously packed with modern, interesting, expressive goodies.

But I have a message for the Haskell community: stop trying to “simplify” monads.  Really, just stop.  I have yet to see an “explanation” of monads that does not add to the confusion and fear a novice might feel at approaching the language.  You don’t need to know what a monad is in order to use the language effectively.  Talking about them to a beginner is about as useful as explaining loop unrolling and keyhole optimization to a beginning C programmer.  Later, yes, it’s helpful.  But it’s too much information at the start.  A beginner just needs to know that if you want to use code that does IO, it has to go inside a do, and if you want to pull out values and use them elsewhere, you need the <- operator.  It took me three nights of working on this one to learn that one, because the Gentle Introduction relegated I/O to chapter 7.

On that note, you’ve got to stop treating I/O in general as a red-headed stepchild.  I realize that isolating side effects is one of the raisons d’être for Haskell in the first place, but if you hope to win converts from the programming world at large (and I really hope you do) then you’ve got to understand that most of us mere mortals want our programs to do something, and that means input/output.  I understand, or at least I think I do, that you want to instill good habits from the start and minimize time spend in the IO monad, but honestly, the language is pretty good at doing that already.  You’ve got to go and talk to people where they already are before you can lead them somewhere else.

Once peple get used to IO and Maybe and whatever other monads are out there, then you can introduce the concept because there’s somewhere for it to land.  I remember taking CS theory in college, and talking with one of the TFs who I happened to know socially.  He is an extremely bright guy, and as is sometimes common with bright guys, can have trouble explaining a concept to someone who doesn’t get it themselves right away.  I made plea for more concrete examples to go with the theoretical explanations in the class, and his response was, “Well, we assume that you, the smart [name of school] students, can come up with the examples yourselves.”  This is exactly wrong for many many people out there, including a lot of the folks who are attracted to coding in the first place.  Some folks work well top-down — get the overarching concepts and then fill in the details themselves.  But some people learn the other way around — experience the details, and then build up the framework from there.  My impression is that the Haskell community is filled with the former type of learner.  But it’s a great language, even for the more experiential/practical and less theoretical, and I hate to see it shunned as too scary.

Ranting aside, I really enjoyed writing this.  For those of you who haven’t tried it yet, I highly recommend it.  It will make you smarter.  I stumbled on places where it differs from OCaml, and I got stuck on IO for a while, but I’m glad I persevered.

This was written against GHC 6.10.4.

import Data.List (intersperse)
import System.IO (stdout, hFlush)
import System.Exit (exitSuccess)

-- Users
data User = X | O
    deriving(Show, Eq)

otherUser X = O
otherUser O = X

-- Squares
data Square = Move User | Empty Int

instance Show Square where
    show (Move x)   = " " ++ show x ++ " "
    show (Empty x)  = "(" ++ show x ++ ")"

filled (Move _) = True
filled _        = False

-- Boards
data Board = Board [[Square]]

instance Show Board where
    show (Board ls) = "\n" ++ concat (intersperse "---+---+---\n" $ map showLine ls) ++ "\n"
            showLine xs = concat (intersperse "|" $ map show xs) ++ "\n"

full (Board squares) = all filled (concat squares)

-- Results
data Result = Continue User Board | Win User Board | Draw Board

instance Show Result where
    show (Continue user board) = show board ++ "Select a square, " ++ show user ++ ": "
    show (Win user board)      = show board ++ show user ++ " Wins!\n"
    show (Draw board)          = show board ++ "It's a Draw!\n"

-- Initial Board
startingBoard :: Board
startingBoard = Board [[Empty 1,Empty 2,Empty 3],
                       [Empty 4,Empty 5,Empty 6],
                       [Empty 7,Empty 8,Empty 9]]

matchSquare :: User -> Int -> Square -> Square
matchSquare user position (Empty x) | x == position = Move user
matchSquare _ _ square                              = square

-- This evaluates the board and determines the result of the current user's action
outcome :: User -> Board -> Result
outcome user board =
      case board of
           (Board [[a, _, _],
                   [_, b, _],
                   [_, _, c]]) | eq a b c -> Win user board

           (Board [[_, _, a],
                   [_, b, _],
                   [c, _, _]]) | eq a b c -> Win user board

           (Board [[a, b, c],
                   [_, _, _],
                   [_, _, _]]) | eq a b c -> Win user board

           (Board [[_, _, _],
                   [a, b, c],
                   [_, _, _]]) | eq a b c -> Win user board

           (Board [[_, _, _],
                   [_, _, _],
                   [a, b, c]]) | eq a b c -> Win user board

           (Board [[a, _, _],
                   [b, _, _],
                   [c, _, _]]) | eq a b c -> Win user board

           (Board [[_, a, _],
                   [_, b, _],
                   [_, c, _]]) | eq a b c -> Win user board

           (Board [[_, _, a],
                   [_, _, b],
                   [_, _, c]]) | eq a b c -> Win user board

           _ | full board -> Draw board
             | otherwise  -> Continue (otherUser user) board
        eq a b c = a == b && b == c

move :: User -> Int -> Board -> Result
move user pos board = result user pos board (place user pos board)
        place user pos (Board lines) = Board $ map (placeInLine user pos) lines

        placeInLine user pos         = map $ matchSquare user pos

        result user pos orig board 
            | orig == board          = Continue user orig
            | otherwise              = outcome user board

main :: IO ()
main = loop (Continue X startingBoard)
        loop result = do 
            putStr $ show result
            hFlush stdout
            case result of
                 Win user board      -> exitSuccess
                 Draw board          -> exitSuccess
                 Continue user board -> getInput user board
        getInput user board = do
             pos <- readLn::IO Int 
             loop (move user pos board)

3 Responses to “Tic-Tac-Toe: Haskell”

  1. April 7, 2010 at 11:56 am

    “Some folks work well top-down — get the overarching concepts and then fill in the details themselves. But some the other way around — experience the details, and then build up the framework from there.”

    This is why I don’t have a CS degree. I started in the CS program, but nearly failed out of the classes I was taking because they were teaching top-down and I learn bottom-up. I didn’t realize that at the time, but in retrospect, it seems very clear.

    I make an effort to get to the top as quickly as possible, but the concepts usually go right over my head unless I’ve got something concrete to attach them to.

  2. April 7, 2010 at 2:06 pm

    You seem to be asking for an article on IO like this one: http://neilmitchell.blogspot.com/2010/01/haskell-io-without-monads.html

    I agree! (which is why I wrote it)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )


Connecting to %s

April 2010
« Mar   May »

%d bloggers like this: