patternMinor
American Checkers
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.
Piece.fs This is an immutable class representing a Piece. It contains each option as a static member to ease building boards manually.
Board.fs This type represents a board, and the only stateful member of this class is immutable, like
```
type Board(board) =
new () = Board(Board.DefaultBoard)
new(board :IEnumerable>>) =
let boardArray = List.ofSeq(board.Select(fun r -> List.ofSeq(r)))
Board(
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
Use functional types
You already start by declaring the two record types
You can still support addition:
Notice how type annotations aren't required. Since this function reads from
The
In the same spirit, it's easy to refactor
F#'s functional types all have structural equality, so you can see how this approach saves you from having to override
Here, I also converted the 'factory' methods
Use built-in types
The
It can help you better communicate intent, but it doesn't preclude a user from providing a 'raw'
The only behaviour defined by
Notice how
You could define the default board like above, but perhaps you'll find the following more readable:
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:
You can also get a single square:
As far as I can tell, you could convert all the extension methods in
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 listIt 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.ColumnNotice 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 concluCode 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 listContext
StackExchange Code Review Q#149796, answer score: 7
Revisions (0)
No revisions yet.