【问题标题】:Is there a way to make handling of Arrays in SML more pleasant?有没有办法让 SML 中的数组处理更愉快?
【发布时间】:2014-09-16 09:16:08
【问题描述】:

在 SML 中实现算法时,我经常想知道是否有一种简单的方法可以使大量使用数组的代码更具可读性。例如,如果我定义一个 SML 函数来交换数组中的 2 个元素,则代码为 ...

local open Array in 
  fun exch (a, i, j) = 
    let
      val tmp = sub (a, i)
      val _ = update (a, i, sub (a, j))
      val _ = update (a, j, tmp)
    in () end
end

我想要的是一个更易读、更简洁的版本,就像这个 Scala-sn-p ...

def exch[T](a: Array[T], i: Int, j: Int) {
  val tmp = a(i)
  a(i) = a(j)
  a(j) = tmp
}

对于像交换数组中的 2 个元素这样简单的事情,SML 版本是可以的。但是一旦算法变得越来越复杂,代码就会变得越来越难以理解,并且会混淆底层算法。

一个稍微复杂一点的例子是这个堆栈(实现为可调整大小的数组)...

structure ArrayStack = struct
  type 'a stack = ('a option array * (int ref)) ref
  exception Empty
  fun mkStack () = ref (Array.array (1, NONE), ref 0)
  fun isEmpty (ref (_, ref 0)) = true
    | isEmpty _ = false
  fun resize (array as ref (xs, n), capacity) =
      let 
        val length = Array.length xs
      in
        array := (Array.tabulate (
                     capacity,
                     fn i => if i < length then Array.sub (xs, i) else NONE
                   ), n)
      end
  fun push (array as ref (xs, n : int ref), x) = 
    if Array.length xs = !n then (
      resize (array, !n*2)
    ; push (array, x)) 
    else (
      Array.update (xs, !n, SOME x)
    ; n := !n+1)
  fun pop (ref (xs, ref 0)) = raise Empty
    | pop (array as ref (xs, n : int ref)) =  let
      val _ = (n := !n-1)
      val x = Array.sub (xs, !n)
      val _ = Array.update (xs, !n, NONE)
      val q = (Array.length xs) div 4
      val _ = if !n > 0 andalso !n = q then resize (array, q) else ()
    in 
      valOf x
    end
end

http://algs4.cs.princeton.edu/13stacks/ResizingArrayStack.java.html 的java 实现相比,该实现(尤其是push/pop)变得难以阅读。

如何使此类代码更具可读性?

【问题讨论】:

标签: sml


【解决方案1】:

确实,数组在 SML 中使用起来相当尴尬。在某种程度上,这是有意阻止它们的使用——因为大多数时候,它们不是数据结构的最佳选择。你的堆栈就是一个很好的例子,因为它作为一个列表更好地实现:

structure ListStack =
struct
  type 'a stack = 'a list ref

  fun stack () = ref nil
  fun isEmpty s = List.null (!s)
  fun push (s, x) = s := x::(!s)
  fun pop s =
      case !s of
        nil => raise Empty
      | x::xs => (s := xs; x)
end

(事实上,你甚至不会通常这样做,并且完全避免使用这样的有状态数据结构,而是使用普通列表。)

如果您关心的是与列表有关的分配,请注意 (a) 它不会进行比数组版本更多的分配(每次推送一个 :: 而不是一个 SOME),并且 (b) 分配在像 SML 这样的语言。

但由于您的问题是关于使用数组,这里是您的数组堆栈的更惯用的实现:

structure ArrayStack =
struct
  open Array infix sub

  datatype 'a stack = Stack of {data : 'a option array ref, size : int ref}

  fun stack () = Stack {data = ref (array (1, NONE)), size = ref 0}

  fun isEmpty (Stack {size, ...}) = !size = 0

  fun resize (data, len') =
      let val data' = array (len', NONE) in
        copy {src = !data, dst = data', di = 0};
        data := data'
      end

  fun push (Stack {data, size}, x) =
      let val size' = !size + 1 in
        if size' > length (!data) then resize (data, !size * 2) else ();
        update (!data, !size, SOME x);
        size := size'
      end

  fun pop (Stack {data, size}) =
      if !size = 0 then raise Empty else
      let
        val _ = size := !size - 1
        val x = !data sub (!size)
        val q = length (!data) div 4
      in
        update (!data, !size, NONE);
        if q > 0 andalso !size = q then resize (data, q) else ();
        valOf x
      end
end

特别是我做了sub中缀,可以写成arr sub i。我这样做只是为了演示,在这个例子中它并不值得,只有一个这样的用法。

【讨论】:

  • 非常感谢您提供如此详细的回复。
  • 关于列表与数组实现问题的另一个问题......就可能导致将数据存储在 CPU 缓存中的更紧凑的内存表示而言,数组实现不是更可取吗?还是今天的 ML 编译器已经解决了这个问题,所以使用列表没有缺点?
  • @gruenewa,不,事实上,可能恰恰相反。每个条目都是通过 SOME 的间接寻址,每次都是新分配的。如果您使用这样的堆栈一段时间,这将导致老一代中的数组包含许多指向年轻一代中某些对象的代际指针(这反过来可能再次指向老一代中的一个值)。那是最不理想的情况。纯粹的函数式解决方案可能是最便宜的,因为它避免了从旧到新的指针以及完全通过写入障碍的巨大成本。
  • ArrayStack 实现中有一个小错误。 Array.copy 函数会在 Stack.pop () 缩小数组的情况下引发异常,因为新数组更小。
  • 您对性能的看法是正确的。我整理了一个简单的基准测试,它循环 1000 次推送 1000.000 个项目,然后弹出 1000.000 个项目。结果是 ListStack 的速度大约是 ArrayStack 的 2 倍。编译器是 MLton。
猜你喜欢
  • 2021-03-21
  • 2017-03-26
  • 1970-01-01
  • 2020-03-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多