2

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?

kibe
  • 90
  • 1
  • 8
  • 26
  • Do you get an error message when doing `of Position (x, y)`? If so please include that in the description. – TheNewGuy Nov 08 '20 at 07:10

2 Answers2

4

Make sure that you are clear on the distinction between values and types in F#. This is a common pitfall for people new to F#, especially around discriminated unions.

1 is a value of type int.

(1, 1) is a value of type (int * int).

When you define a DU type, each case can hold data of a certain type:

type DUType =
| DUCase1 of int

So each DU case can contain any int, not just a particular int.


You also have a type alias in your code: type Position = int * int. This is just saying that you can write Position anywhere and it will mean the same as int * int. It's not actually another type.


So in your code you can't say that a DU case must always contain a certain value. You need to write a function instead that takes a Direction and returns a Position:

type Direction = 
  | North  
  | South  
  | West  
  | East 

let directionToPostion direction : Position = 
    match direction with
    | North -> (0, 1)
    | South -> (0, -1)
    | West -> (-1, 0)
    | East -> (0, 1)

Any F# code you write will generally always be in 3 "modes":

  • Value
  • Type
  • Pattern (as in pattern matching)

Try to make sure you know which one of the three you're in at any given time.

TheQuickBrownFox
  • 10,544
  • 1
  • 22
  • 35
1

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.

alexpanter
  • 1,222
  • 10
  • 25
  • Btw I made a hand-on tutorial on this in F# a couple of years back. Can be found here, if you are interested: https://www.youtube.com/watch?v=3INrLPenFGc – alexpanter Nov 08 '20 at 11:23