2

Let's say I'm programming a chess game. At some point I have to check, which moves are valid for a given piece. What would be the proper way to select the correct pathfinding function for a given piece (king, queen, etc.)?

I came up with three different approaches:

  1. Store the pathfinding method (alongside the other data) inside a record (similar to the strategy pattern; too object oriented?)
  2. Create a PieceType Discriminated Union with all piece types and store a value of this type in the record. Select the proper pathfinding function via pattern matching
  3. Create a PieceData record-type which stores the data and a Piece DU, where all cases represent different piece types and are of type PieceData. Pattern matching is also used to select the correct function.

Here is some sample code, demonstrating the different approaches:

// The piece "constructor" is private as to only allow creating pieces via
// the provided construct functions for each piece type
type Piece = private { Position:Position; Pathfinder:Position->Board->Position list }

let kingPathfinder position board =
    // Check and return all possible targets for a king

let createKing position =
    { Position=position; Pathfinder=kingPathfinder }

let getPossibleTargets { Position=position; Pathfinder=pathfinder } board =
    pathfinder position board


    ####    ####    ####


type PieceType =
    | King
    | ...

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

let kingPathfinder position board =
    // Check and return all possible targets for a king

let createKing position =
    { Position=position; PieceType=King }

let getPossibleTargets { Position=position; PieceType=pieceType } board =
    match pieceType with
        | King -> kingPathfinder position board
        | ...


    ####    ####    ####


type PieceData = { Position:Position }

type Piece =
    | King of PieceData
    | ...

let kingPathfinder position board =
    // Check and return all possible targets for a king

let createKing position =
    King { Position=position }

let getPossibleTargets piece board =
    match piece with
        | King { Position:position } ->
            kingPathfinder position board

My question now is: Which one of these three approaches is preferred in functional programming? Or is there another, superior way?

1

Second approach is the best.

First approach admits invalid data - i.e. you can put any function in place of PawnData.Pathfinder, and there is no way to enforce that it's a valid one.

Third approach is generally ok, but in this specific case it's a little wasteful, because all cases of the DU have identical data.

Second approach is the right one for this particular problem.

Also, you're confusing terms a bit. The word "pawn" means a specific type of chess piece - the one of which there are eight. Saying "pawn of type King" is kind of like saying "bishop of type King". The correct term for this is "piece":

type PieceType =
   | King
   ...
4
  • Thanks for your answer, I edited my question for clarity. I also modified my first approach, so it's clear, that I only want to create pieces via the provided construct functions. So I don't think invalid data is possible? Still, I also think the second one is the most natural and the third one a tad "wasteful".
    – user108376
    Dec 18 '16 at 15:04
  • Even though you made the constructor private, the type itself still doesn't constrain the data. The correctness of getPossibleTargets now depends on the correctness of some other code (i.e. createKing) that is located in a different place. Private constructor is a good strategy when you don't have any other way to guarantee data correctness (i.e. "last resort"). But in this case you do. Dec 18 '16 at 17:37
  • Okay, now I see what you mean, thanks for clarifying. Just out of curiosity: Is it then usually "bad", to have a function as a record field or are there valid use cases for them?
    – user108376
    Dec 18 '16 at 18:33
  • Yes, there are valid cases. One that immediately comes to mind is passing an "options" record to a function - a common way to get around the "too many parameters" problem. In general, valid cases for "function in record" are same as valid cases for "function as parameter". Dec 18 '16 at 18:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy