Search code examples
f#discriminated-union

Assignment inside discriminated unions


I am new to F# and am trying to develop a snake game so pardon me if this sounds stupid.

For now, this is the model for the game:

// value objects
type Position = int * int
type Block = { Position: Position }
type Tail = { Blocks: Block list }
type Direction = 
  | North  
  | South  
  | West  
  | East 
//
 
// entities
type Snake = { Tail: Tail }
type World = { Snake: Snake }
//

To make things simpler when moving the snake, I would like for each Direction to have its own Position, just like:

type Direction = 
  | North of Position (0, 1)
  | South of Position (0, -1)
  | West of Position (-1, 0)
  | East of Position (0, 1)

so I can just apply it here:

let moveSnakeHead direction snake =
  // easily move the snake's head
  // tail[0].x += direction.x, tail[0].y += direction.y

However, it seems to me that it is not possible to do that of Position (x, y) inside the discriminated union?

Could someone explain why? I am trying my best to learn types. And what would be the alternatives?


Solution

  • Abusing the answer from @TheQuickFrownFox I actually got it working the way I think you want. I think your data types are overly complex, but it is possible to create a snake game like this. Note the usage of reference types and mutables.

    // value objects
    type Position = int * int
    type Block = { mutable Position: Position }
    type Tail = { Blocks: Block list }
    type Direction = 
      | North  
      | South  
      | West  
      | East 
    //
    
    // entities
    type Snake = { Tail: Tail }
    //
    
    let directionToPostion = function
        | North -> (0, 1)
        | South -> (0, -1)
        | West -> (-1, 0)
        | East -> (0, 1)
    
    
    let moveSnakeHead (direction: Direction) (snake: Snake ref) =
      // easily move the snake's head
      let (dirX, dirY) = directionToPostion direction
      let snakeHeadPos = (!snake).Tail.Blocks.[0].Position
      (!snake).Tail.Blocks.[0].Position <- (dirX + fst snakeHeadPos, dirY + snd snakeHeadPos)
    
    
    let blocks: Block list = [ {Position = (5,3)}; {Position = (4,2)} ]
    let tail: Tail = { Blocks = blocks }
    let snake = ref <| {Tail = tail}
    
    printfn "%A" blocks
    moveSnakeHead North snake
    printfn "%A" blocks
    

    Quick note:

    F# is not a clean functional language, so you can use it like an object-oriented language with a bit of work, but it is not the preferred way. Optimally you would have a function which reads the snake (I recommend simply using the type type Snake = (int * int) list, and outputs (maps) it into a new list containing the updated positions. This would be cleaner, easier to maintain, and more adherent to the design goals of F#.

    Edit:

    I decided to come back and update my answer to contain which I think would be the canonical way of doing this in F#. I think you will find this cleaner and easier to read:

    type Snake = (int * int) list
    
    type Direction = North | South | East | West
    
    let moveSnake snake dir =
      if List.isEmpty snake then []
      else
        let h = List.head snake
        match dir with
        | North -> (fst h, snd h - 1) :: List.tail snake
        | South -> (fst h, snd h + 1) :: List.tail snake
        | East  -> (fst h + 1, snd h) :: List.tail snake
        | West  -> (fst h - 1, snd h) :: List.tail snake
    
    let snake = [(5,3); (1,2)]
    printfn "%A" snake
    printfn "%A" <| moveSnake snake North
    

    If you really want, you can declare the snake variable mutable, so that you can change the snake. But I recommend staying away from this and having your program strictly functional as far as possible.