【问题标题】:Problems with Immutable Data in Functional Programming函数式编程中不可变数据的问题
【发布时间】:2016-04-22 01:54:24
【问题描述】:

我是函数式编程的新手。我的理解是函数式编程是使用纯函数编写代码,而不改变数据的值。

当我们需要更新变量时,我们在函数式编程中创建新变量,而不是更改变量的值。

假设我们有一个变量x,它代表程序发出的HTTP请求的总数。如果我们有两个线程,那么我希望线程在任何线程发出 HTTP 请求时增加x。如果两个线程都制作了变量x 的不同副本,那么它们如何同步x 的值。例如:如果线程 1 发出 10 个 HTTP 请求,线程 2 发出 11 个 HTTP 请求,那么它们将分别打印 10 和 11,但我将如何打印 21。

【问题讨论】:

  • 线程永远不是目标,永远是工具。你通常想尽可能地忘记它们,只让一些低级的、经过深思熟虑的机器在引擎盖下为你做线程。

标签: multithreading haskell clojure functional-programming erlang


【解决方案1】:

我可以为clojure 案例提供答案。在clojure 中,如果您需要协调对共享状态的访问,语言中有一些结构旨在处理这些情况。

在这种情况下,您可以使用atom 来保存该值。对atom 所做的更改是原子性的,将通过clojure 的STM 乐观地进行。原子是 clojure 的引用类型之一。原子本质上是对一个值的引用,它可以通过原子的变异函数以受控方式随时间变化。

有关原子和其他引用类型的更多信息,请参阅 clojure docs

【讨论】:

    【解决方案2】:

    我将讨论 Haskell 部分。 MVar 是线程的通信机制之一。这是取自 Simon Marlow 书中的示例之一(程序不言自明):

    main = do
      m <- newEmptyMVar
      forkIO $ do putMVar m 'x'; putMVar m 'y'
      r <- takeMVar m
      print r
      r <- takeMVar m
      print r
    

    上述程序的输出将是:

    'x'
    'y'
    

    您可以在上面的示例中看到变量m 中的MVar 值是如何在线程之间共享的。您可以在this book 中了解有关这些技术的更多信息。

    【讨论】:

    • -1 没有解决真正的问题——对线程应该如何使用的误解,尤其是在像 Haskell 这样的语言中。
    • @ErikAllik 我只是展示了线程如何相互通信的机制之一。我很高兴看到你的回答。
    【解决方案3】:

    我还将讨论 Haskell 部分。

    首先,我想澄清一点:

    我们创建新变量而不是改变变量的值 当我们需要更新变量时进行函数式编程。

    这不太准确。我们在需要时在 FP 中创建新的“变量”,而不是在需要改变现有变量时。当我们按照您的描述进行操作时,我们甚至不会考虑突变;我们可能只是认为我们正在创造一种与我们所拥有的价值相似的新价值。

    你用线程描述的有点不同。您实际上是在寻找副作用(增加一个计数器)。纯粹的 Haskell 不仅允许您在没有非常明确的情况下抛出副作用。因此,在这种情况下,您将需要求助于引用类型/可变单元格。最简单的称为IORef,在这个意义上它非常像一个变量;可以赋值、读取当前值等等。

    所以,如您所见,当您在寻找这类东西时,您确实只有一份计数器。

    以上是我回答的精髓,但是您已经具体询问了线程,所以我也会对此作出回应。
    IORefs 实际上并不是线程安全的。所以建议有MVars。它们不像常规变量,但它们很接近,并且可以优雅地完成工作。一般来说,松散地说:它们抽象变量和锁定。我想你可能会发现TVars 更容易。它们的行为类似于IORef/变量,只是与两者不同,它们是线程安全的;您可以将它们的操作组合成一个操作,对它们进行的任何操作都是原子完成的(STM)。

    顺便说一句,您可能会找到完全避免状态的方法,这是非常鼓励的。例如。您可以让两个线程执行一个异步递归函数,该函数通过参数记住已发出多少请求,然后将其作为返回值。总请求数是所有线程返回的请求的总和。这可以避免对计数器的副作用,但它只能在线程完成时给你一个结果。这是相当有限的,所以有时你可能想要这种副作用。

    【讨论】:

      【解决方案4】:

      好吧,我会尝试在保持状态时提供更一般的解释,因为我认为这是您真正想知道的。

      通常你可以通过递归来完成同样的事情,例如如果你有下面的函数:

      somefun ()->
         somefun(0).
      somefun (X) ->
        perform_http_request(),
        if(something!=quit)
           somefun(X+1)
      end function.
      
      generate_thread(0, Accumulator) ->
            Accumulator;
      generate_thread(X, Accumulator) ->
            Y = somefun(),
            NewAccumulator = add_to_accumulator(Y),
            generate_thread(X-1, NewAccumulator).
      

      我只是匆忙输入了这个,这是一个非常通用的解释(您将无法直接使用此代码)但认为您会发现您并没有真正的可变性...该函数将当所有线程完成处理时完成,现在您可以根据您选择的语言进行实际的线程同步,并且不同的语言有不同的处理并发和“线程”的方式..我建议您看看 Erlang如果你喜欢并发,因为它有一个非常好的并发模型 imo。

      无论如何,最后您可以将返回的累加器中的所有值相加并显示出来,顺便看看 foldl 和 foldr 函数。

      【讨论】:

        【解决方案5】:

        我自己不是大师,但我想你可能没有理解。

        如果不保持状态,就无法创建非常有用的程序。需要说明一种或另一种方式。 FP 的目标不是避免状态,它是控制状态的使用

        这样看,你的状态应该和你的数据库条目一样独立和安全。 如果你像对待数据库一样对待状态,我想你会没事的

        这意味着,

        • 您不会有像(inc count) 这样的登录名。你宁愿有一个函数increment-count! 可以安全地更新计数。注意!,这意味着副作用。
        • 您不会有依赖于副作用的代码。相反,您将依赖于期望从其参数中获取所有内容的函数。除非他们绝对必须依赖国家。就像更新计数一样,这是对状态的不可否认的要求。
        • 您的第一选择应该是避免状态。当将其传递给函数变得不可能时,您将创建正确更新的状态。
        • 像对待外部 API 一样对待状态。您必须使用某种协议才能访问远程的东西。

        我希望这是有道理的。

        【讨论】:

          【解决方案6】:

          我将地址 Erlang 部分。即使在 Erlang 中,同步也没有什么魔力,需要有人在某个地方处理同步。只是 Erlang 具有不变性(即无变量)的好处,有助于防止并发编程中常见的同步错误。像 gen_server 这样的 Erlang/OTP 已经拥有管理状态的基础设施。事实上,gen_server 是单线程的,它接收到的任何消息都会在邮箱中排队。这是一个关于 Erlang 的消息并发的链接。 How Erlang processes access mailbox concurrently

          在最初的 post 案例中,要固定一个 http 请求计数器,您可以使用单个 gen_server OTP (Erlang)。您会惊讶于它可以处理多少吞吐量。如果单个 gen_server 吞吐量确实不够,可以有一个分层的 gen_server 来聚合计数。 Erlang/OTP 带有一组运行时 API 来实时测量性能。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-05-26
            • 2021-02-22
            • 2018-07-16
            相关资源
            最近更新 更多