你如何用函数式语言实现Grid?

时间:2014-12-04 02:18:19

标签: data-structures functional-programming ocaml lazy-evaluation self-reference

我对在函数式语言中实现常量网格的不同方式感兴趣。一个完美的解决方案应该在每步的pesimistic恒定时间内提供遍历,而不是使用命令式构造(懒惰是可以的)。不太满足这些要求的解决方案仍然受到欢迎。

我的建议基于四向链接节点,如此

References of a node

基本操作是构建给定大小的网格。似乎这个操作将决定类型,即哪个方向将是懒惰的(显然这种数据结构不能没有懒惰)。所以我建议(在OCaml中)

type 'a grid =
  | GNil
  | GNode of 'a * 'a grid Lazy.t * 'a grid Lazy.t * 'a grid * 'a grid

订购参考:左,上,右,下。左,上是暂停。然后我按对角线方式构建网格

Construction process

这是一个make_grid函数,它构造一个给定大小的网格,并将坐标元组作为节点值。请注意,glgugrgd功能允许在所有方向上在网格上行走,如果给出GNil,则会返回GNil }。

let make_grid w h =
  let lgnil = Lazy.from_val GNil in
  let rec build_ur x y ls dls = match ls with
    | l :: ((u :: _) as ls') ->
      if x = w && y = h then
        GNode ((x, y), l, u, GNil, GNil)
      else if x < w && 1 < y then
        let rec n = lazy (
          let ur = build_ur (x + 1) (y - 1) ls' (n :: dls) in
          let r = gd ur in
          let d = gl (gd r)
          in GNode ((x, y), l, u, r, d)
        )
        in force n
      else if x = w then 
        let rec n = lazy (
          let d = build_dl x (y + 1) (n :: dls) [lgnil]
          in GNode ((x, y), l, u, GNil, d)
        )
        in force n
      else
        let rec n = lazy (
          let r = build_dl (x + 1) y (lgnil :: n :: dls) [lgnil] in
          let d = gl (gd r)
          in GNode ((x, y), l, u, r, d)
        )
        in force n
    | _ -> failwith "make_grid: Internal error"
  and build_dl x y us urs = match us with
    | u :: ((l :: _) as us') ->
      if x = w && y = h then
        GNode ((x, y), l, u, GNil, GNil)
      else if 1 < x && y < h then
        let rec n = lazy (
          let dl = build_dl (x - 1) (y + 1) us' (n :: urs) in
          let d = gr dl in
          let r = gu (gr d)
          in GNode ((x, y), l, u, r, d)
        )
        in force n
      else if y = h then
        let rec n = lazy (
          let r = build_ur (x + 1) y (n :: urs) [lgnil]
          in GNode ((x, y), l, u, r, GNil)
        )
        in force n
      else (* x = 1 *)
        let rec n = lazy (
          let d = build_ur x (y + 1) (lgnil :: n :: urs) [lgnil] in
          let r = gu (gr d)
          in GNode ((x, y), l, u, r, d)
        )
        in force n
    | _ -> failwith "make_grid: Internal error"
  in build_ur 1 1 [lgnil; lgnil] [lgnil]

它看起来相当复杂,因为它必须分别处理我们上升时以及当我们分别下降时的情况 - build_urbuild_dl辅助功能。 build_ur函数的类型为

build_ur :
  int -> int ->
  (int * int) grid Lazy.t list -> 
  (int * int) grid Lazy.t list -> (int * int) grid

在给定当前位置xy的情况下构建一个节点,上一个对角线ls的暂停元素列表,暂停之前的列表当前对角线urs的元素。名称ls来自ls上的第一个元素是当前节点的左邻居。构建下一个对角线需要urs列表。

build_urs函数继续在右上对角线上构建下一个节点,将当前节点传递给暂停。 邻居取自ls,可以访问邻居通过对角线上的下一个节点。

请注意,我在GNilurs列表中放了一堆ls个。这样做始终确保build_urbuild_dl可以使用这些列表中的至少两个元素。

A step of <code>build_ur</code>

build_dl功能类似。

对于这种简单的数据结构,这种实现看起来过于复杂。事实上,我很惊讶它起作用,因为我在写作时被信仰驱使,并且无法完全理解为什么它有效。因此,我想知道一个更简单的解决方案。

我正在考虑逐行构建网格。这种方法具有较少的边界情况,但我不能消除在不同方向上构建后续行的需要。这是因为当我走到最后一行并想从头开始构建另一行时,我必须以某种方式知道当前行中第一个节点的 down 节点,在我从当前的函数调用返回之前,我似乎无法知道。如果我无法消除双向性,我需要两个内部节点构造函数:一个具有已挂起的顶部,另一个具有已挂起的顶部

The continuation problem

此外,这里有一个这个实现的要点以及省略的函数:https://gist.github.com/mkacz91/0e63aaa2a67f8e67e56f

2 个答案:

答案 0 :(得分:2)

如果您需要功能性解决方案,那么您正在寻找的数据结构是zipper。我已经在Haskell中编写了其余代码,因为我发现它更符合我的口味,但它很容易移植到OCaml。 Here's a gist没有交错评论。

{-# LANGUAGE RecordWildCards #-}

module Grid where

import Data.Maybe

我们可以从了解刚才列表的数据结构开始:您可以将拉链视为列表内部的指针。你有一个wathever在你指向的元素的左边,然后是你指向的元素,最后是右边的元素。

type ListZipper a = ([a], a, [a])

给定一个列表和一个整数n,您可以关注位于n的元素。当然,如果n大于列表的长度,那么你就失败了。需要注意的一件重要事情是列表的左侧部分向后存储:因此可以在恒定时间内将焦点向左移动。随着向右移动。

focusListAt :: Int -> [a] -> Maybe (ListZipper a)
focusListAt = go []
  where
    go _   _ []        = Nothing
    go acc 0 (hd : tl) = Just (acc, hd, tl)
    go acc n (hd : tl) = go (hd : acc) (n - 1) tl

现在让我们转向网格。 Grid只是一个行列表(列表)。

newtype Grid a = Grid { unGrid :: [[a]] }

Grid的拉链现在由一个网格给出,表示当前焦点的所有above,另一个代表below的所有内容,以及一个列表拉链(高级:注意这个看起来有点像嵌套列表拉链&amp;可以用更通用的术语重新表述。)

data GridZipper a =
  GridZipper { above :: Grid a
             , below :: Grid a
             , left  :: [a]
             , right :: [a]
             , focus :: a }

首先关注右侧行,然后关注右侧元素,我们可以将Grid关注到某个坐标xy

focusGridAt :: Int -> Int -> Grid a -> Maybe (GridZipper a)
focusGridAt x y g = do
  (before, line , after) <- focusListAt x $ unGrid g
  (left  , focus, right) <- focusListAt y line
  let above = Grid before
  let below = Grid after
  return GridZipper{..}

一旦我们有拉链,我们就可以轻松移动。左边或右边的代码并不令人惊讶地相似:

goLeft :: GridZipper a -> Maybe (GridZipper a)
goLeft g@GridZipper{..} =
  case left of
    []      -> Nothing
    (hd:tl) -> Just $ g { focus = hd, left = tl, right = focus : right }

goRight :: GridZipper a -> Maybe (GridZipper a)
goRight g@GridZipper{..} =
  case right of
    []      -> Nothing
    (hd:tl) -> Just $ g { focus = hd, left = focus : left, right = tl }

当上升或下降时,我们必须要小心,因为我们需要专注于我们在新行中留下的那个上方(或下方)的位置。我们还必须将我们关注的上一行重新组合成一个好的旧列表(通过将反向left附加到focus : right)。

goUp :: GridZipper a -> Maybe (GridZipper a)
goUp GridZipper{..} = do
  let (line : above')     = unGrid above
  let below'              = (reverse left ++ focus : right) : unGrid below
  (left', focus', right') <- focusListAt (length left) line
  return $ GridZipper { above = Grid above'
                      , below = Grid below'
                      , left  = left'
                      , right = right'
                      , focus = focus' }

goDown :: GridZipper a -> Maybe (GridZipper a)
goDown GridZipper{..} = do
  let (line : below')     = unGrid below
  let above'              = (reverse left ++ focus : right) : unGrid above
  (left', focus', right') <- focusListAt (length left) line
  return $ GridZipper { above = Grid above'
                      , below = Grid below'
                      , left  = left'
                      , right = right'
                      , focus = focus' }

最后,我还添加了几个辅助函数来生成网格(每个单元格包含一对坐标)和实例,以便能够在终端中显示网格和拉链。

mkGrid :: Int -> Int -> Grid (Int, Int)
mkGrid m n = Grid $ [ zip (repeat i) [0..n-1] | i <- [0..m-1] ]

instance Show a => Show (Grid a) where
  show = concatMap (('\n' :) . concatMap show) . unGrid

instance Show a => Show (GridZipper a) where
  show GridZipper{..} =
    concat [ show above, "\n"
           , concatMap show (reverse left)
           , "\x1B[33m[\x1B[0m",  show focus, "\x1B[33m]\x1B[0m"
           , concatMap show right
           , show below ]

main创建一个大小为5 * 10的小网格,聚焦于坐标(2,3)处的元素并稍微移动一下。

main :: IO ()
main = do
  let grid1 = mkGrid 5 10
  print grid1
  let grid2 = fromJust $ focusGridAt 2 3 grid1
  print grid2
  print $ goLeft =<< goLeft =<< goDown =<< goDown grid2

答案 1 :(得分:0)

实现无限网格的简单解决方案包括使用由坐标对索引的哈希表。

以下是不检查整数溢出的示例实现:

type 'a cell = {
  x: int; (* position on the horizontal axis *)
  y: int; (* position on the vertical axis *)
  value: 'a;
}

type 'a grid = {
  cells: (int * int, 'a cell) Hashtbl.t;
  init_cell: int -> int -> 'a;
}

let create_grid init_cell = {
  cells = Hashtbl.create 10;
  init_cell;
}

let hashtbl_get tbl k =
  try Some (Hashtbl.find tbl k)
  with Not_found -> None

(* Check if we have a cell at the given relative position *)
let peek grid cell x_offset y_offset =
  hashtbl_get grid.cells (cell.x + x_offset, cell.y + y_offset)

(* Get the cell at the given relative position *)
let get grid cell x_offset y_offset =
  let x = cell.x + x_offset in
  let y = cell.y + y_offset in
  let k = (x, y) in
  match hashtbl_get grid.cells k with
  | Some c -> c
  | None ->
      let new_cell = {
        x; y;
        value = grid.init_cell x y
      } in
      Hashtbl.add grid.cells k new_cell;
      new_cell

let left grid cell = get grid cell (-1) 0
let right grid cell = get grid cell 1 0
let down grid cell = get grid cell 0 (-1)
(* etc. *)