我听过的反对功能语言的一个论点是,单一的分配编码太难了,或者至少比“正常”更难。编程。

但是通过我的代码,我意识到如果你用一种相当现代的语言写作,我真的没有很多(任何?)使用模式,使用单一的赋值形式也无法编写。

那么在一次调用范围内变量的变量的用例是什么?请记住,在这种情况下,在调用之间变化的循环索引,参数和其他范围限制值不是多个赋值(除非您必须在正文中更改它们出于某种原因),并假设你正在写一个远远高于汇编语言水平的东西,在那里你可以写出像

这样的东西
values.sum

或(如果没有提供总和)

function collection.sum --> inject(zero, function (v,t) --> t+v )

x = if a > b then a else b

n = case s 
  /^\d*$/ : s.to_int
  ''      : 0
  '*'     : a.length
  '?'     : a.length.random
  else    fail "I don't know how many you want"

当您需要时,可以使用列表推导,地图/收集等等。

你是否发现在这样的环境中你仍然需要/需要可变变量,如果是,那该怎么办?

澄清一点,我并不是要求对SSA表格提出异议,而是要求适用这些异议的具体例子。我正在寻找具有可变变量的清晰简洁的代码,如果没有它们就无法编写。

到目前为止我最喜欢的例子(以及我对他们的最佳反对意见):

  1. Paul Johnson的 Fisher-Yates算法回答,当你包含big-O约束时,它非常强大。但是,正如catulahoops所指出的那样,big-O问题与SSA问题无关,而是与可变数据类型相关联,并且在SSA中可以相当清楚地编写算法:

     shuffle(Lst) ->
         array:to_list(shuffle(array:from_list(Lst), erlang:length(Lst) - 1)).
     shuffle(Array, 0) -> Array;
     shuffle(Array, N) ->
         K = random:uniform(N) - 1,
         Ek = array:get(K, Array),
         En = array:get(N, Array),
         shuffle(array:set(K, En, array:set(N, Ek, Array)), N-1).
    
  2. jpalecek 多边形区域示例:

    def area(figure : List[Point]) : Float = {
      if(figure.empty) return 0
      val last = figure(0)
      var first= figure(0)
      val ret = 0
      for (pt <- figure) {
        ret+=crossprod(last - first, pt - first)
        last = pt
      }
      ret
    }
    

    可能仍然写得像:

    def area(figure : List[Point]) : Float = {
        if figure.length < 3
            0
          else
            var a = figure(0)
            var b = figure(1)
            var c = figure(2)
            if figure.length == 3
                magnitude(crossproduct(b-a,c-a))
              else 
                foldLeft((0,a,b))(figure.rest)) { 
                   ((t,a,b),c) => (t+area([a,b,c]),a,c)
                   }
    

    或者,由于有些人反对这种配方的密度,它可以重铸:

    def area([])    = 0.0   # An empty figure has no area
    def area([_])   = 0.0   # ...nor does a point
    def area([_,_]) = 0.0   # ...or a line segment
    def area([a,b,c]) =     # The area of a triangle can be found directly
        magnitude(crossproduct(b-a,c-a))
    def area(figure) =      # For larger figures, reduce to triangles and sum
        as_triangles(figure).collect(area).sum
    
    def as_triangles([])      = []  # No triangles without at least three points
    def as_triangles([_])     = []
    def as_triangles([_,_])   = []
    def as_triangles([a,b,c | rest) = [[a,b,c] | as_triangles([a,c | rest])]
    
  3. Princess关于使用不可变结构实现O(1)队列的难度的观点很有意思(并且很可能为一个引人注目的例子提供基础),但正如所述,它基本上是关于数据结构的可变性,而不是直接关于多重指派问题。

  4. 我对Eratosthenes的回答很感兴趣,但不相信。正如你所引用的论文中给出的那样,正确的大O,拉动尽可能多的素数,无论是否有SSA,都不容易正确实现。


  5. 好的,谢谢大家的尝试。由于大多数答案被证明是1)基于可变数据结构,而不是基于单一指派,2)在某种程度上它们是关于单一指派形式容易被本领域技术人员反击,我将要从我的谈话和/或重组中划线(可能在备份中将其作为讨论主题,在不太可能发生的事情中,在我用完时间之前用完了)。

    再次感谢。

有帮助吗?

解决方案

我从未发现过这种情况。虽然你总是可以创建新名称,就像转换为SSA形式一样,但我实际上发现每个值都有自己的名称是很容易和自然的。像Haskell这样的语言给了我很多关于命名值的选择,以及两个不同的地方来放置名称绑定( let where )。我发现单一作业形式非常自然,并不困难。

我偶尔会错过能够在堆上指向可变对象的指针。但这些东西没有名字,所以这不是同一个异议。 (我还发现,当我在堆上使用可变对象时,我倾向于编写更多错误!)

其他提示

我遇到的最难的问题是改变列表。 Fisher-Yates 算法(有时也称为Knuth算法)涉及遍历列表用随机的其他项目交换每个项目。该算法是O(n),众所周知且长期以来证明是正确的(在某些应用中是一个重要的属性)。但它需要可变数组。

这并不是说你不能在功能程序中进行改组。 Oleg Kiselyov已撰写了这篇文章。但是如果我理解正确的话,功能改组是O(n.log n),因为它通过构建二叉树来工作。

当然,如果我需要在Haskell中编写Fisher-Yates算法,我只需将其放入 ST monad ,它允许你包含一个涉及可变数组在一个漂亮的纯函数中,如下所示:

-- | Implementation of the random swap algorithm for shuffling.  Reads a list
-- into a mutable ST array, shuffles it in place, and reads out the result
-- as a list.

module Data.Shuffle (shuffle) where


import Control.Monad
import Control.Monad.ST
import Data.Array.ST
import Data.STRef
import System.Random

-- | Shuffle a value based on a random seed.
shuffle :: (RandomGen g) => g -> [a] -> [a]
shuffle _ [] = []
shuffle g xs = 
    runST $ do
      sg <- newSTRef g
      let n = length xs
      v <- newListArray (1, n) xs
      mapM_ (shuffle1 sg v) [1..n]
      getElems v

-- Internal function to swap element i with a random element at or above it.
shuffle1 :: (RandomGen g) => STRef s g -> STArray s Int a -> Int -> ST s ()
shuffle1 sg v i = do
  (_, n) <- getBounds v
  r <- getRnd sg $ randomR (i, n)
  when (r /= i) $ do
    vi <- readArray v i
    vr <- readArray v r
    writeArray v i vr
    writeArray v r vi


-- Internal function for using random numbers
getRnd :: (RandomGen g) => STRef s g -> (g -> (a, g)) -> ST s a
getRnd sg f = do
  g1 <- readSTRef sg
  let (v, g2) = f g1
  writeSTRef sg g2
  return 

v

如果你想提出学术论点,那么当然在技术上不必多次分配一个变量。证据是所有代码都可以在 SSA(单一静态分配)表单中表示。实际上,这是用于多种静态和动态分析的最有用的形式。

与此同时,我们并不是所有人都以SSA形式编写代码来开始:

  1. 以这种方式编写代码通常需要更多语句(或更多行代码)。简洁有价值。
  2. 几乎总是效率低下。是的,我知道你在谈论更高级的语言 - 一个公平的范围 - 但即使在Java和C#的世界里,远离集会,速度也很重要。速度无关紧要的应用很少。
  3. 这并不容易理解。虽然SSA是“更简单”的。在数学意义上,它从常识中更抽象,这在现实世界的编程中是重要的。如果你必须非常聪明地去理解它,那么它就无法进行编程。
  4. 即使在上面的示例中,也很容易发现漏洞。拿你的 case 语句。如果有一个管理选项可以确定是否允许'*',另一个是否允许'?',该怎么办?此外,对于整数情况,不允许为零,除非用户具有允许它的系统权限。

    这是一个更具现实意义的分支和条件示例。你能把它写成一个单一的“声明吗?”如果是这样,那么你的“陈述”是什么?真的不同于许多单独的陈述?如果没有,您需要多少个临时只写变量?那种情况明显好于只有一个变量吗?

我认为你会发现最高效的语言可以让你混合功能和命令式的风格,比如OCaml和F#。

在大多数情况下,我可以编写的代码只是“map x to y,reduce y to z”的长行。在95%的情况下,函数式编程简化了我的代码,但有一个领域不变性显示出来:

实现的简易性和不可变堆栈与不可变队列之间存在巨大差异。

堆栈很容易并且与持久性很好地匹配,队列很荒谬。

不可变队列的常见实现使用一个或多个内部堆栈和堆栈旋转。好处是这些队列大部分时间都在O(1)中运行,但有些操作将在O(n)中运行。如果您依赖于应用程序中的持久性,那么原则上每个操作都可以在O(n)中运行。当您需要实时(或至少一致)性能时,这些队列并不好。

Chris Okasaki在他的书中提供了不可变队列的实现,他们使用懒惰以实现所有操作的O(1)。它是一个非常聪明,相当简洁的实时队列实现 - 但它需要深入了解其底层实现细节,并且它仍然比不可变堆栈更复杂。

相反,我可以使用可变链接列表编写堆栈和队列,这些链接列表在所有操作的恒定时间内运行,结果代码非常简单。


关于多边形的区域,它很容易将其转换为功能形式。假设我们有一个像这样的Vector模块:

module Vector =
    type point =
        { x : float; y : float}
        with
            static member ( + ) ((p1 : point), (p2 : point)) =
                { x = p1.x + p2.x;
                  y = p1.y + p2.y;}

            static member ( * ) ((p : point), (scalar : float)) =
                { x = p.x * scalar;
                  y = p.y * scalar;}

            static member ( - ) ((p1 : point), (p2 : point)) = 
                { x = p1.x - p2.x;
                  y = p1.y - p2.y;}

    let empty = { x = 0.; y = 0.;}
    let to_tuple2 (p : point) = (p.x, p.y)
    let from_tuple2 (x, y) = { x = x; y = y;}
    let crossproduct (p1 : point) (p2 : point) =
        { x = p1.x * p2.y; y = -p1.y * p2.x }

我们可以使用一些元组魔法定义我们的区域函数:

let area (figure : point list) =
    figure
    |> Seq.map to_tuple2
    |> Seq.fold
        (fun (sum, (a, b)) (c, d) -> (sum + a*d - b*c, (c, d) ) )
        (0., to_tuple2 (List.hd figure))
    |> fun (sum, _) -> abs(sum) / 2.0

或者我们可以使用交叉产品

let area2 (figure : point list) =
    figure
    |> Seq.fold
        (fun (acc, prev) cur -> (acc + (crossproduct prev cur), cur))
        (empty, List.hd figure)
    |> fun (acc, _) -> abs(acc.x + acc.y) / 2.0

我发现这两种功能都不可读。

使用单一赋值实现该shuffle算法是微不足道的,实际上它与迭代重写为尾递归的命令式解决方案完全相同。 (Erlang,因为我可以比Haskell更快地编写它。)

 shuffle(Lst) ->
     array:to_list(shuffle(array:from_list(Lst), erlang:length(Lst) - 1)).

 shuffle(Array, 0) -> Array;
 shuffle(Array, N) ->
     K = random:uniform(N) - 1,
     Ek = array:get(K, Array),
     En = array:get(N, Array),
     shuffle(array:set(K, En, array:set(N, Ek, Array)), N-1).

如果担心这些数组操作的效率,那么这是一个关于可变数据结构的问题,与单一赋值无关。

你不会得到这个问题的答案,因为没有例子。这只是熟悉这种风格的问题。

回应杰森 -

function forbidden_input?(s)
    (s = '?' and not administration.qmark_ok) ||
    (s = '*' and not administration.stat_ok)  ||
    (s = '0' and not 'root node visible' in system.permissions_for(current_user))

n = if forbidden_input?(s)
    fail "'" + s + "' is not allowed."
  else
    case s
      /^\d*$/ : s.to_int
      ''      : 0
      '*'     : a.length
      '?'     : a.length.random
      else    fail "I don't know how many you want"

我会错过非纯函数语言的作业。主要是因为它们阻碍了循环的有用性。示例(Scala):

def quant[A](x : List[A], q : A) = {
  var tmp : A=0
  for (el <- x) { tmp+= el; if(tmp > q) return el; }
  // throw exception here, there is no prefix of the list with sum > q
}

这应该计算列表的分位数,注意多次分配的累加器 tmp

类似的例子是:

def area(figure : List[Point]) : Float = {
  if(figure.empty) return 0
  val last = figure(0)
  var first= figure(0)
  val ret = 0
  for (pt <- figure) {
    ret+=crossprod(last - first, pt - first)
    last = pt
  }
  ret
}

主要注意 last 变量。

这些示例可以使用元组上的fold来重写,以避免多次分配,但这实际上无助于可读性。

本地(方法)变量肯定永远不会分配给两次。但即使在函数式编程中,也允许重新赋值。它是更改(部分)不允许的值。正如dsimcha已经回答的那样,对于非常大的结构(可能是应用程序的根本),我似乎不可能更换整个结构。想一想。应用程序的状态最终都由应用程序的入口点方法包含。如果绝对没有状态可以在不更换的情况下进行更改,则必须在每次按键时重新启动应用程序。 :(

如果你有一个构建惰性列表/树的函数然后再次减少它,函数编译器可能能够使用砍伐森林

如果它很棘手,它可能不会。然后你有点不幸,表现和记住,除非你可以迭代并使用一个可变变量。

感谢Church-Turing论文,我们知道任何可以用图灵完整语言编写的内容都可以用 任何 图灵完备语言编写。所以,当你接下来的时候,你在Lisp中没有什么是你不能做的,如果你努力了,反之亦然。 (更重要的是,无论如何,在大多数情况下,任何一个都将被编译成x86机器语言。)

所以,你的问题的答案是:没有这样的情况。所有这些案例都更容易让人类用一种范式/语言或另一种语言理解 - 而且这里的理解容易与培训和经验联系在一起。

这里的主要问题可能是语言循环的风格。在我们使用递归的语言中,当再次调用函数时,在循环过程中更改的任何值都会重新绑定。在块中使用迭代器的语言(例如,Smalltalk和Ruby的 inject 方法)往往是类似的,尽管Ruby中的许多人仍然会使用每个和一个可变变量而不是注入

当您使用 for 编码循环时,另一方面,当您可以通过时,您没有自动重新绑定变量在几个参数中,你的代码块执行循环的一次迭代,因此不可变的变量会更加不方便。

在Haskell中工作是研究可变变量必要性的一种非常好的方法,因为默认值是不可变的但是可变的变量是可用的(如 IORefs MVars ,以及等等)。我最近,呃,“调查”以这种方式自己,并得出以下结论。

  1. 在绝大多数情况下,不需要变量变量,没有它们我很幸福。

  2. 对于线程间通信,可变变量是必不可少的,原因相当明显。 (这是Haskell特有的;当然,在最低级别使用消息传递的运行时系统不需要它们。)然而,这种用法很少见,必须使用函数来读取和写入它们( readIORef fooRef val 等)并没有太大的负担。

  3. 我在单个线程中使用了可变变量,因为它似乎使某些事情更容易,但后来后悔了,因为我意识到很难推断存储在那里的值发生了什么。 (几个不同的功能正在操纵这个价值。)这有点令人大开眼界;在典型的青蛙热水风格中,我没有意识到Haskell让我有理由使用价值,直到我遇到一个如何使用它们的例子

  4. 所以这些天我已经相当坚定地站在了不可变变量的一边。

    由于之前对这个问题的回答混淆了这些问题,我觉得有必要强烈指出这个问题与纯度和函数式编程正交。例如,我觉得Ruby可以从单一赋值的局部变量中受益,尽管可能对语言进行一些其他更改,例如添加尾递归,这对于使这真正方便是必要的。

当您需要对大型数据结构进行小的更改时,该怎么办?每次修改一些元素时,你真的不想复制整个数组或大类。

除非你现在指出它,否则我并没有真正考虑过这个问题。

实际上我尝试不下意识地使用多个作业。

这是我在python

中谈论的一个例子
start = self.offset%n
if start:
    start = n-start

以这种方式编写以避免不必要的额外Modulo或减法。这与bignum风格的长整数一起使用,因此它是值得优化的。然而,关于它,它确实只是一个任务。

我根本不会错过多项任务。

我知道您要求的代码确实显示了可变变量的好处。我希望我能提供它。但正如之前所指出的那样 - 没有任何问题无法用两种方式表达出来。尤其是因为你指出jpalecek的多边形示例区域可以用折叠算法编写(这是恕我直言的方式更麻烦,并将问题带到不同的复杂程度) - 这让我想知道为什么你会因为可变性而陷入困境硬。因此,我将尝试为不可变和可变数据的共同点和共存提出论据。

在我看来,这个问题忽略了一点。我知道我们程序员喜欢干净简单的东西,但我们有时会想念混合物也是可能的。这就是为什么在关于不变性的讨论中很少有人采取中间立场。我只是想知道为什么,因为让我们面对它 - 不变性是一个很好的工具来抽象各种问题。但有时它是一个<强烈的屁股的巨大痛苦。有时它只是过于约束。仅此一点就让我停下来了 - 我们真的想要放松可变性吗?它真的是 - 或者? 我们可以到达的是否有一些共同点?什么时候不变性能帮助我更快地实现目标,什么时候可变性? 哪种解决方案更易于阅读和维护?(对我来说这是最大的问题)

很多这些问题都受到程序员的品味和他们习惯编程的影响。所以我将关注其中一个方面,这些方面通常是大多数亲不变性论点的中心 - 并行性:

关于不变性的争论通常会引入并行性。我已经研究过在合理的时间内需要100多个CPU才能解决的问题集。它告诉我一个非常重要的事情:大多数时候并行化数据图的操作实际上并不是那种最有效的并行化方法。它肯定会受益匪浅,但不平衡是问题空间中的一个真正问题。因此,通常并行处理多个可变图并使用不可变消息交换信息更有效。这意味着,当我知道图形是孤立的,我没有向外界透露它时,我想以我能想到的最简洁的方式对它进行操作。这通常涉及改变数据。但是在对数据进行这些操作之后,我想将数据打开到整个世界 - 如果数据是可变的,我通常会有点紧张。由于程序的其他部分可能会破坏数据,因此状态变得无效,...因为在向世界开放之后,数据经常会进入并行世界。

因此,真实世界的并行程序通常具有数据图在最终单线程操作中使用的区域 - 因为它们根本不为外部所知 - 以及它们可能涉及多线程操作的区域(希望仅提供数据)被操纵)。在那些多线程部分中,我们从不希望它们发生变化 - 处理过时的数据比处理不一致的数据更好。这可以通过不变性的概念得到保证。

这让我得出一个简单的结论:对我来说真正的问题是,我熟悉的编程语言不允许我说:&quot;在此之后,这整个数据结构应该是不可变的““在这里给我一个这个不可变数据结构的可变副本,请验证我只能看到可变副本”。现在我必须通过翻转一个只读或类似的东西来保证它。如果我们可以为它提供编译器支持,那么它不仅可以保证我做到了

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top