HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

American Checkers

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
americancheckersstackoverflow

Problem

I am implementing the logic for a Checkers game in F#. I am writing my code in a library so it can be called from any UI provider, and am trying to do it in good FP style. I currently have the following code; each file is provided in the order of compilation:

Checkers.fs This contains some unions and records that will be used throughout the entire library.

type Player = Black | White

type PieceType = Checker | King

type Coord = {
    Row :int
    Column :int
} with
    static member (+) (coord1 :Coord, coord2 :Coord) =
        {Row = coord1.Row + coord2.Row; Column = coord1.Column + coord2.Column}


Piece.fs This is an immutable class representing a Piece. It contains each option as a static member to ease building boards manually.

type Piece(player:Player, pieceType:PieceType) =
    member this.Player = player
    member this.PieceType = pieceType

    member this.Promote() =
        new Piece(player, PieceType.King)

    override this.Equals (obj) =
        let piece = obj :?> Piece
        piece.Player = this.Player &&
        piece.PieceType = this.PieceType

    override this.GetHashCode() =
        this.Player.GetHashCode() ^^^ this.PieceType.GetHashCode()

    static member WhiteChecker() =
        Some <| new Piece(Player.White, PieceType.Checker)

    static member BlackChecker() =
        Some <| new Piece(Player.Black, PieceType.Checker)

    static member WhiteKing() =
        Some <| new Piece(Player.White, PieceType.King)

    static member BlackKing() =
        Some <| new Piece(Player.Black, PieceType.King)


Board.fs This type represents a board, and the only stateful member of this class is immutable, like Piece. The second optional constructor is for ease of creating a board from a hard-coded list in C#; it may not remain in future versions.

```
type Board(board) =
new () = Board(Board.DefaultBoard)

new(board :IEnumerable>>) =
let boardArray = List.ofSeq(board.Select(fun r -> List.ofSeq(r)))
Board(

Solution

One of the many benefits of F# is that it's a multiparadigmatic language; while it embraces a functional-first ideal, it clearly also enables you to write object-oriented code. This is useful if you're coming from a C# background, like I did some years ago. You can get started quickly writing F#, concentrating on learning the language syntax. Naturally, if you do that, you'd tend to start your F# journey writing F# in an object-oriented style. There's nothing wrong with that; it's part of the voyage.

It looks like you've already made an effort to make your F# code immutable, which is an important step in the right direction. In general, it's looking good.

You also use the word idiomatic F#, and that opens another discussion. Most importantly, what's idiomatic has a subjective component. Still, when I write F# code these days, my code is mostly assembled from functional types (records, discriminated unions, lists, tuples) and let-bound functions. Most of the code I've seen from other seasoned F# programmers seem to follow that pattern as well. With that in mind, here are some suggestions that would, in my opinion, make the code more idiomatic.

Use functional types

You already start by declaring the two record types Player and PieceType, and that's a good start, but why make Coord a class? You can make it a record, too:

type Coord = { Row :int; Column :int }


You can still support addition:

let (+) coord1 coord2 =
    { Row = coord1.Row + coord2.Row; Column = coord1.Column + coord2.Column }


Notice how type annotations aren't required. Since this function reads from Row and Column labels, the compiler can infer that both coord1 and coord2 are Coord values.

The + function still works, as this FSI session demonstrates:

> { Row = 1; Column = 4 } + { Row = 2; Column = 3 };;
val it : Coord = {Row = 3; Column = 7;}


In the same spirit, it's easy to refactor Piece to a record and associated functions:

type Piece = { Player : Player; PieceType : PieceType }

let promote p = { p with PieceType = King }

let whiteChecker = Some { Player = White; PieceType = Checker }

let blackChecker = Some { Player = Black; PieceType = Checker }

let whiteKing    = Some { Player = White; PieceType = King }

let blackKing    = Some { Player = Black; PieceType = King }


F#'s functional types all have structural equality, so you can see how this approach saves you from having to override Equals and GetHashCode.

Here, I also converted the 'factory' methods whiteChecker, blackKing, and so on, to values. The original functions always returned the same value, so I didn't see any reason to make them functions.

Use built-in types

The Board class is little but a wrapper around Piece option list list, so it can be eliminated. It can sometimes be useful to define a type alias:

type Board = Piece option list list


It can help you better communicate intent, but it doesn't preclude a user from providing a 'raw' Piece option list list value. This keeps things more flexible.

The only behaviour defined by Board are the two Item properties and the default board, all of which can easily be defined as let-bound values:

let row = List.item

let square coord = List.item coord.Row >> List.item coord.Column


Notice how row is nothing but an alias for List.item. Likewise, square is a composition of List.item functions. These functions are actually much more generic than the ones defined in the Board class, but they work correctly with a board as well.

You could define the default board like above, but perhaps you'll find the following more readable:

let defaultBoard = 
    [
        List.replicate 4 [None; blackChecker] |> List.concat
        List.replicate 4 [blackChecker; None] |> List.concat
        List.replicate 4 [None; blackChecker] |> List.concat
        List.replicate 8 None
        List.replicate 8 None
        List.replicate 4 [whiteChecker; None] |> List.concat
        List.replicate 4 [None; whiteChecker] |> List.concat
        List.replicate 4 [whiteChecker; None] |> List.concat
    ]


Code readability is subjective, but this is both shorter, and explicitly highlights the repetitive nature of the default board. On the other hand, as a reader, you can't 'see' the whole board laid out in the code.

In any case, you can get a row from the default board:

> row 2 defaultBoard;;
val it : Piece option list =
  [null; Some {Player = Black; PieceType = Checker;};
   null; Some {Player = Black; PieceType = Checker;};
   null; Some {Player = Black; PieceType = Checker;};
   null; Some {Player = Black; PieceType = Checker;}]


You can also get a single square:

> square { Row = 2; Column = 1 } defaultBoard;;
val it : Piece option = Some {Player = Black; PieceType = Checker;}


As far as I can tell, you could convert all the extension methods in FSharpExtensions to let-bound functions as well, but I think I'll conclu

Code Snippets

type Coord = { Row :int; Column :int }
let (+) coord1 coord2 =
    { Row = coord1.Row + coord2.Row; Column = coord1.Column + coord2.Column }
> { Row = 1; Column = 4 } + { Row = 2; Column = 3 };;
val it : Coord = {Row = 3; Column = 7;}
type Piece = { Player : Player; PieceType : PieceType }

let promote p = { p with PieceType = King }

let whiteChecker = Some { Player = White; PieceType = Checker }

let blackChecker = Some { Player = Black; PieceType = Checker }

let whiteKing    = Some { Player = White; PieceType = King }

let blackKing    = Some { Player = Black; PieceType = King }
type Board = Piece option list list

Context

StackExchange Code Review Q#149796, answer score: 7

Revisions (0)

No revisions yet.