受歧视工会内部的分配

Assignment inside discriminated unions

我是 F# 的新手,正在尝试开发一款贪吃蛇游戏,如果这听起来很愚蠢,请原谅我。

现在,这是游戏的模型:

// 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 }
//

为了让蛇移动起来更简单,我希望每个 Direction 都有自己的 Position,就像:

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

所以我可以在这里应用它:

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

但是,在我看来,of Position (x, y)在受歧视的工会内部不可能做到这一点?

有人可以解释为什么吗?我正在尽力学习类型。还有什么替代方案?

确保您清楚 F# 中 valuestypes 之间的区别。对于刚接触 F# 的人来说,这是一个常见的陷阱,尤其是在受歧视的联合周围。

1int.

类型的值

(1, 1)(int * int).

类型的值

当你定义一个DU类型时,每个case都可以保存某种类型的数据:

type DUType =
| DUCase1 of int

所以每个 DU 案例都可以包含 any int,而不仅仅是特定的 int.


您的代码中还有一个 类型别名 type Position = int * int。这只是说您可以在任何地方写 Position,它的意思与 int * int 相同。它实际上不是另一种类型。


因此在您的代码中您不能说 DU 案例必须始终包含某个 。您需要编写一个函数来代替 Direction 和 returns 一个 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)

您编写的任何 F# 代码通常始终处于 3 种“模式”:

  • 价值
  • 类型
  • 模式(如模式匹配)

尝试确保您在任何给定时间都知道自己属于这三个中的哪一个。

滥用@TheQuickFrownFox 的回答,我实际上让它按照我认为你想要的方式工作。我认为您的数据类型过于复杂,但 可以创建这样的贪吃蛇游戏。注意引用类型和可变变量的用法。

// 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

快速说明:

F# 不是一种干净的函数式语言,因此您可以像使用 object-oriented 语言一样使用它,但需要做一些工作,但这不是首选方式。最佳情况下,您将拥有一个读取蛇的函数(我建议简单地使用类型 type Snake = (int * int) list,并将其输出(映射)到包含更新位置的新列表中。这将更清晰、更易于维护且更贴合F# 的设计目标。

编辑:

我决定回来更新我的答案以包含我认为在 F# 中执行此操作的规范方法。我想您会发现它更简洁易读:

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

如果你真的想要,你可以声明蛇变量mutable,这样你就可以改变蛇了。但我建议远离这一点,并尽可能让您的程序严格运行。