强制性break语句和其他循环检查的功能等效项是什么?


36

假设我具有以下逻辑。如何在函数式编程中编写它?

    public int doSomeCalc(int[] array)
    {
        int answer = 0;
        if(array!=null)
        {
            for(int e: array)
            {
                answer += e;
                if(answer == 10) break;
                if(answer == 150) answer += 100;
            }
        }
        return answer;
    }

大多数博客,文章中的示例……我看到的只是解释了一个简单的数学函数“ Sum”的简单例子。但是,我有一个类似于以上用Java编写的逻辑,并且希望将其迁移到Clojure中的功能代码。如果我们无法在FP中执行上述操作,则FP的促销方式不会明确说明这一点。

我知道上面的代码是绝对必要的。编写此程序并非出于将来将其迁移到FP的考虑。


1
需要注意的是组合breakreturn answer可以通过更换return内循环。在FP中,您可以使用延期
Giorgio

1
@Giorgio继续在这里将是一个巨大的过大杀伤力。无论如何,这都是一个循环,要调用它的下一个迭代,您需要进行一次尾部调用,因此要打断您,只需不再调用它,然后返回答案即可。对于嵌套的循环,或其他复杂的控制流,这就是你可能会使用的延续,而不是起伏调整你的代码使用上述简单的方法(这应该永远是可能的,但可能会导致过于复杂的代码结构,这将或多或少显明连续性;对于多个出口,您当然需要它们)。
内斯

8
在这种情况下:takeWhile
乔纳森·

1
@WillNess:我只想提及它,因为它可以随时用于留下复杂的计算。对于OP的具体示例,这可能不是最佳解决方案。
乔治

@ Giorgio,您是对的,一般来说,它是最全面的。实际上,这个问题是非常广泛的,即IYKWIM(即在SO上会心跳加速)。
威尔·内斯

Answers:


45

在大多数功能语言中,最类似于循环遍历数组的函数是fold函数,即,该函数针对数组的每个值调用用户指定的函数,并将累积值沿链传递。在许多功能语言中,fold通过提供其他功能的各种附加功能进行了扩充,包括在出现某种情况时提早停止的选项。在惰性语言(例如Haskell)中,可以通过不沿列表进行任何进一步的评估来实现尽早停止,这将导致永远不会生成其他值。因此,将您的示例翻译为Haskell,我将其编写为:

doSomeCalc :: [Int] -> Int
doSomeCalc values = foldr1 combine values
  where combine v1 v2 | v1 == 10  = v1
                      | v1 == 150 = v1 + 100 + v2
                      | otherwise = v1 + v2

如果您不熟悉Haskell的语法,请逐行进行分解,如下所示:

doSomeCalc :: [Int] -> Int

定义函数的类型,接受一个int列表并返回一个int。

doSomeCalc values = foldr1 combine values

该函数的主体:给定参数valuesfoldr1带有参数的return 调用combine(我们将在下面定义)和valuesfoldr1是fold原语的一种变体,它从将累加器设置为列表的第一个值开始(因此1在函数名称中为),然后使用用户指定的函数从左到右对其进行组合(通常称为right fold,因此r在函数名称中)。因此foldr1 f [1,2,3]等效于f 1 (f 2 3)(或f(1,f(2,3))更常规的类似于C的语法)。

  where combine v1 v2 | v1 == 10  = v1

定义combine局部函数:它接收两个参数,v1v2。当v1为10时,它仅返回v1。在这种情况下,永远不会评估v2,因此循环在此处停止。

                      | v1 == 150 = v1 + 100 + v2

或者,当v1为150时,向其添加一个额外的100,然后添加v2。

                      | otherwise = v1 + v2

并且,如果这两个条件都不成立,则只需将v1添加到v2。

现在,该解决方案在某种程度上特定于Haskell,因为如果合并功能不评估第二个参数,则右折终止的事实是由Haskell的惰性评估策略引起的。我不了解Clojure,但我相信Clojure使用严格的评估,因此我希望它fold在其标准库中具有一个功能,该功能包括对提前终止的特定支持。这通常被称为foldWhilefoldUntil或类似的。

快速浏览Clojure库文档表明,它与大多数功能性语言的命名有点不同,这fold并不是您想要的(它是旨在实现并行计算的更高级的机制),但是reduce更直接当量。如果reduced在您的合并函数中调用该函数,则会发生提前终止。我不是100%肯定我了解语法,但是我怀疑您正在寻找的东西是这样的:

(reduce 
    (fn [v1 v2]
        (if (= v1 10) 
             (reduced v1)
             (+ v1 v2 (if (= v1 150) 100 0))))
    array)

注意:Haskell和Clojure的两种翻译都不适合该特定代码。但它们传达了其基本要点-有关这些示例的具体问题,请参见以下评论中的讨论。


11
名称v1 v2令人困惑:v1是“来自数组的值”,但是v2累加结果。我相信您的翻译是错误的,当累积值(从左开始)达到10而不是数组中的某些元素时,OP的循环就会退出。与增加100相同。如果在此处使用折页,请使用左侧折页并提前退出,在foldlWhile 此处进行一些更改。
威尔·内斯

2
滑稽的最错误的答案如何获得对SE最upvotes ....这是确定的犯错误,你在好公司:),太。但是,关于SO / SE的知识发现机制肯定被打破了。
内斯

1
Clojure代码几乎是正确的,但是将(= v1 150)使用v2(aka。e)之前的值的条件求和。
NikoNyrh,

1
Breaking this down line by line in case you're not familiar with Haskell's syntax - 你是我的英雄。Haskell对我来说是个谜。
曼队长

15
@WillNess已被赞成,因为它是最容易理解的翻译和解释。错误的事实在这里可耻但相对不重要,因为轻微的错误并不能否定答案在其他方面会有所帮助的事实。但是,当然应该纠正它。
康拉德·鲁道夫

33

您可以轻松地将其转换为递归。而且它具有不错的尾部优化的递归调用。

伪代码:

public int doSomeCalc(int[] array)
{
    return doSomeCalcInner(array, 0);
}

public int doSomeCalcInner(int[] array, int answer)
{
    if (array is empty) return answer;

    // not sure how to efficiently implement head/tails array split in clojure
    var head = array[0] // first element of array
    var tail = array[1..] // remainder of array

    answer += head;
    if (answer == 10) return answer;
    if (answer == 150) answer += 100;

    return doSomeCalcInner(tail, answer);
}

14
是的 等效于循环的功能是尾递归,而等效于条件的功能仍然是条件。
约尔格W¯¯米塔格

4
@JörgWMittag我宁愿说尾递归在功能上等同于GOTO。(不是很糟糕,但是仍然很尴尬。)Jules所说的相当于循环的折叠是合适的。
大约

6
@leftaroundabout我实际上不同意。我想说,鉴于需要跳到自身并且仅在尾部位置,尾部递归比goto受到更多限制。它从根本上等效于循环构造。我想说递归一般等于GOTO。无论如何,当您编译尾部递归时,它大多会归结为while (true)带有函数主体的循环,在该循环中,早期返回只是一条break语句。折叠,虽然您对它是一个循环是正确的,但实际上它比一般的循环结构更受约束。它更像是一个for-each循环
J_mie6 '18

1
@ J_mie6之所以我更多地考虑尾递归的原因GOTO是,您需要精心记录所有处于什么状态的参数传递给递归调用,以确保它实际上按预期运行。在体面编写的命令式循环中(这是很明显的,有状态变量是什么,以及它们在每次迭代中如何变化),在相同程度上是没有必要的,在幼稚的递归中(通常对参数没有做太多的事情,而是结果以非常直观的方式进行组装)。...
大约

1
...关于折痕:您是对的,传统的折痕(同形异像)是一种非常特殊的循环,但是这些递归方案可以泛化(ana- / apo- / hylomorphisms);总的来说,这些是IMO的必要替代。
大约

13

我真的很喜欢Jules的答案,但我想指出的是,人们经常会错过一些关于懒函数编程的知识,这就是说,一切不必都在“循环内”。例如:

baseSums = scanl (+) 0

offsets = scanl (\offset sum -> if sum == 150 then offset + 100 else offset) 0

zipWithOffsets xs = zipWith (+) xs (offsets xs)

stopAt10 xs = if 10 `elem` xs then 10 else last xs

result = stopAt10 . zipWithOffsets . baseSums

result [1..]         -- 10
result [11..1000000] -- 500000499945

您会看到,逻辑的每个部分都可以在单独的函数中进行计算,然后组合在一起。这允许使用较小的功能,这些功能通常更易于排除故障。对于您的玩具示例,也许这增加了要消除的复杂性,但是在现实世界的代码中,分离功能通常比整体功能简单得多。


逻辑散布在这里。此代码将难以维护。stopAt10不是一个良好的消费。你的答案不是你的,因为它正确地隔离的基本生产举一个更好的scanl (+) 0价值。但是,它们的使用应直接合并控制逻辑,最好仅使用两个spans和一个a来实现last。也将严格遵循原始代码结构和逻辑,并且易于维护。
内斯

6

大多数列表处理的例子,你会看到这样的使用功能mapfiltersum等,这些列表作为一个整体进行操作。但是在您的情况下,您有条件提早退出-一种非常不常见的模式,通常的列表操作不支持这种模式。因此,您需要降低抽象级别并使用递归-也更接近命令式示例的外观。

这是直接转换为Clojure的(可能不是惯用的)翻译:

(defn doSomeCalc 
  ([lst] (doSomeCalc lst 0))
  ([lst sum]
    (if (empty? lst) sum
        (if (= sum 10) sum
            (let [sum (+ sum (first lst))]
                 [sum (if (= sum 150) (+ sum 100) sum)]
               (recur (rest lst) sum))))))) 

编辑:朱人指出,reduce用Clojure 支持提前退场。使用它更优雅:

(defn doSomeCalc [lst]  
  (reduce (fn [sum val]
    (if (= sum 10) (reduced sum)
        (let [sum (+ sum val)]
             [sum (if (= sum 150) (+ sum 100) sum)]
           sum))
   lst)))

无论如何,您都可以像使用命令式语言一样使用功能性语言来做任何事情,但是您通常必须有所改变以找到一种优雅的解决方案。在命令式编码中,您会考虑逐步处理列表,而在功能语言中,您会寻找适用于列表中所有元素的操作。


请参阅我刚刚添加到答案中的编辑:Clojure的reduce操作支持提前退出。
Jules

@Jules:很酷-这可能是一个惯用的解决方案。
JacquesB '18年

错误-是takeWhile不是“常用操作”?
乔纳森·

@jcast-虽然takeWhile是常用操作,但在这种情况下它并不是特别有用,因为在确定是否停止之前,您需要转换的结果。在懒惰的语言中,这没关系:您可以使用scantakeWhile根据扫描结果(请参阅Karl Bielefeldt的回答,虽然不使用它takeWhile可以很容易地重写为这样做),但是对于像clojure这样的严格语言,这会意味着要处理整个列表,然后再丢弃结果。生成器函数可以解决此问题,但是我相信clojure支持它们。
Jules

take-whileClojure中的@Jules 产生一个惰性序列(根据文档)。解决此问题的另一种方法是换能器(也许是最好的换能器)。
内斯

4

正如其他答案所指出的那样,Clojure可以reduced尽早停止减排:

(defn some-calc [coll]
  (reduce (fn [answer e]
            (let [answer (+ answer e)]
               (case answer
                 10  (reduced answer)
                 150 (+ answer 100)
                 answer)))
          0 coll))

这是针对您的特定情况的最佳解决方案。与结合reduced使用还可以带来很多好处transduce,它使您可以使用来自的换能器mapfilter等等。但是,对于一般性问题而言,这还远远不是一个完整的答案。

转义继续是break和return语句的通用版本。它们在某些Schemes(call-with-escape-continuation),Common Lisp(block+ returncatch+ throw)甚至C(setjmp+ longjmp)中直接实现。在标准Scheme中或在Haskell和Scala中作为延续单子词的更一般的带分隔符或不带分隔符的延续也可以用作转义延续。

例如,在球拍中,您可以这样使用let/ec

(define (some-calc ls)
  (let/ec break ; let break be an escape continuation
    (foldl (lambda (answer e)
             (let ([answer (+ answer e)])
               (case answer
                 [(10)  (break answer)] ; return answer immediately
                 [(150) (+ answer 100)]
                 [else  answer])))
           0 ls)))

许多其他语言也具有异常处理形式的类似于转义连续的构造。在Haskell中,您还可以将各种错误单子之一与一起使用foldM。因为它们主要是使用异常或错误单子进行早期返回的错误处理构造,通常在文化上是不可接受的,并且可能相当慢。

您还可以从高阶函数下拉至尾部调用。

使用循环时,到达循环主体的末尾时将自动输入下一个迭代。您可以continue使用break(或return)提前进入下一个迭代或退出循环。使用尾部调用(或loop模拟尾部递归的Clojure 构造)时,必须始终进行显式调用才能进入下一个迭代。要停止循环,您只需不进行递归调用,而是直接提供值:

(defn some-calc [coll]
  (loop [answer 0, [e es :as coll] coll]
    (if (empty? coll)
      answer
      (let [answer (+ answer e)]
        (case answer
          10 answer
          150 (recur (+ answer 100) es)
          (recur answer es))))))

1
在Haskell中重新使用错误monad时,我不相信这里会有任何实际的性能损失。他们倾向于考虑沿异常处理的思路,但是它们的工作方式不同,并且不需要执行任何堆栈步移,因此,如果以这种方式使用,则实际上应该不是问题。同样,即使出于文化原因,不使用MonadError,基本上等效的方法Either也不会偏重于错误处理,因此可以轻松地用作替代方法。
Jules

@Jules我认为返回Left不会阻止折叠查看整个列表(或其他序列)。不过,我对Haskell标准库内部并不十分熟悉。
nilern

2

复杂的部分是循环。让我们开始。通常通过使用单个函数表示迭代,将循环转换为函数样式。迭代是循环变量的转换。

这是常规循环的功能实现:

loop : v -> (v -> v) -> (v -> Bool) -> v
loop init iter cond_to_cont = 
    if cond_to_cont init 
        then loop (iter init) iter cond
        else init

它需要(循环变量的初始值,表示[循环变量]上的单个迭代的函数)(继续循环的条件)。

您的示例在数组上使用循环,该循环也会中断。命令式语言中的此功能已融入语言本身。在函数式编程中,通常在库级别实现这种功能。这是一个可能的实现

module Array (foldlc) where

foldlc : v -> (v -> e -> v) -> (v -> Bool) -> Array e -> v
foldlc init iter cond_to_cont arr = 
    loop 
        (init, 0)
        (λ (val, next_pos) -> (iter val (at next_pos arr), next_pos + 1))
        (λ (val, next_pos) -> and (cond_to_cont val) (next_pos < size arr))

在里面 :

我使用((val,next_pos))对,其中包含在外部可见的循环变量和该函数隐藏在数组中的位置。

迭代函数比常规循环中的函数稍微复杂一点,该版本使使用数组的当前元素成为可能。[采用咖喱形式。]

这些功能通常称为“折叠”。

我在名称中加上了“ l”,以表示数组元素的累加是以左关联的方式完成的;模仿命令式编程语言从低索引到高索引迭代数组的习惯。

我在名称中添加了“ c”,以指示此版本的fold带有控制是否及何时停止循环的条件。

当然,这些实用程序功能很可能在所使用的功能编程语言附带的基本库中很容易获得。我在这里写了它们进行演示。

现在,我们拥有了在必要情况下使用该语言提供的所有工具,接下来我们可以实现示例的特定功能。

循环中的变量是一对(“ answer”,一个编码是否继续的布尔值)。

iter : (Int, Bool) -> Int -> (Int, Bool)
iter (answer, cont) collection_element = 
  let new_answer = answer + collection_element
  in case new_answer of
    10 -> (new_answer, false)
    150 -> (new_answer + 100, true)
    _ -> (new_answer, true)

请注意,我使用了一个新的“变量”“ new_answer”。这是因为在函数式编程中,我无法更改已初始化的“变量”的值。我不担心性能,如果编译器认为效率更高,则可以通过生命周期分析将“ answer”的内存重新用于“ new_answer”。

将其纳入我们先前开发的循环函数中:

doSomeCalc :: Array Int -> Int
doSomeCalc arr = fst (Array.foldlc (0, true) iter snd arr)

此处的“数组”是导出函数foldlc的模块名称。

“ fist”,“ second”代表返回其对参数的第一,第二部分的函数

fst : (x, y) -> x
snd : (x, y) -> y

在这种情况下,“无点”样式可提高doSomeCalc实现的可读性:

doSomeCalc = Array.foldlc (0, true) iter snd >>> fst

(>>>)是函数组成: (>>>) : (a -> b) -> (b -> c) -> (a -> c)

与上面相同,只是在定义方程式的两侧都省略了“ arr”参数。

最后一件事:检查大小写(数组== null)。在设计更好的编程语言中,但即使是在某些基础学科设计不佳的语言中,人们也宁愿使用可选类型来表示不存在。这与函数编程没有太大关系,而函数编程最终是关于这个问题的,因此我不予理not。


0

首先,稍微重写一下循环,这样循环的每次迭代要么提前退出,要么answer恰好变异一次:

    public int doSomeCalc(int[] array)
    {
        int answer = 0;
        if(array!=null)
        {
            for(int e: array)
            {
                if(answer + e == 10) return answer + e;
                else if(answer + e == 150) answer = answer + e + 100;
                else answer = answer + e;
            }
        }
        return answer;
    }

应该清楚的是,此版本的行为与以前完全相同,但是现在,转换为递归样式要简单得多。这是Haskell的直接翻译:

doSomeCalc :: [Int] -> Int
doSomeCalc = recurse 0
  where recurse :: Int -> [Int] -> Int
        recurse answer [] = answer
        recurse answer (e:array)
          | answer + e == 10 = answer + e
          | answer + e == 150 = recurse (answer + e + 100) array
          | otherwise = recurse (answer + e) array

现在它已完全发挥作用,但是我们可以通过使用fold而不是显式递归来从效率和可读性的角度改进它:

import Control.Monad (foldM)

doSomeCalc :: [Int] -> Int
doSomeCalc = either id id . foldM go 0
  where go :: Int -> Int -> Either Int Int
        go answer e
          | answer + e == 10 = Left (answer + e)
          | answer + e == 150 = Right (answer + e + 100)
          | otherwise = Right (answer + e)

在这种情况下,Left它的价值会早日退出,而价值会Right继续递归。


现在可以进一步简化它,如下所示:

import Control.Monad (foldM)

doSomeCalc :: [Int] -> Int
doSomeCalc = either id id . foldM go 0
  where go :: Int -> Int -> Either Int Int
        go answer e
          | answer' == 10 = Left 10
          | answer' == 150 = Right 250
          | otherwise = Right answer'
          where answer' = answer + e

作为最终的Haskell代码,这更好,但是现在还不清楚它如何映射回原始Java。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.