命令的な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
組み合わせであることに注意してくださいbreakとのreturn answerことで置き換えることができreturn、ループ内。FPでは、継続を使用してこの早期復帰を実装できます。たとえば、en.wikipedia.org / wiki / Continuation
Giorgio

1
@Giorgioの継続は、ここでは非常にやり過ぎです。とにかくループなので、次の繰り返しを呼び出すには末尾呼び出しを行います。ネストされたあなたは常に可能である必要がありますが、これは多かれ少なかれだろう過度に複雑なコード構造につながる可能性が上記の簡単なテクニック(使用するように再構築するようにコードを代わりにヒービングの継続を使用する可能性のある場所だループ、または他の複雑な制御フロー、解明を継続;そして、複数の出口点には必ず必要です)。
ウィルネス

8
この場合:takeWhile
ジョナサンキャスト

1
@WillNess:複雑な計算をいつでも残すために使用できるため、言及したいだけです。これはおそらく、OPの具体的な例にとって最適なソリューションではありません。
ジョルジオ

あなたが正しい@Giorgio、それは一般的に最も包括的なものです。実際、この質問は非常に幅広く、IYKWIMです(つまり、SOでハートビートで閉じられます)。
ウィルネス

回答:


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の構文に慣れていない場合に、この行を1行ずつ分解すると、次のように機能します。

doSomeCalc :: [Int] -> Int

関数の型を定義し、intのリストを受け入れ、単一のintを返します。

doSomeCalc values = foldr1 combine values

関数の本体:指定された引数valuesfoldr1引数combine(以下で定義します)およびで呼び出されたreturn valuesfoldr1リストの最初の値に設定されたアキュムレータで始まるフォールドプリミティブのバリアントであり(1関数名にあるため)、ユーザーが指定した関数を左から右に使用して結合します(通常、右フォールドと呼ばれます、したがって、r関数名に)。だから(またはより一般的なCのような構文)foldr1 f [1,2,3]と同等です。f 1 (f 2 3)f(1,f(2,3))

  where combine v1 v2 | v1 == 10  = v1

定義combineローカル関数を:それは2つの引数を受け取り、v1v2。場合はv110、それだけのリターンですv1。この場合、v2は評価されないため、ループはここで停止します。

                      | v1 == 150 = v1 + 100 + v2

または、v1が150の場合、さらに100を追加し、v2を追加します。

                      | otherwise = v1 + v2

そして、これらの条件のいずれも当てはまらない場合は、v1をv2に追加します。

結合関数が2番目の引数を評価しない場合、右側のフォールドが終了するという事実は、Haskellの遅延評価戦略が原因であるため、このソリューションはHaskellにある程度固有です。私は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)

NB:HaskellとClojureの両方の翻訳は、この特定のコードにはまったく適切ではありません。しかし、それらはその概要を伝えます。これらの例の特定の問題については、以下のコメントの議論を参照してください。


11
名前v1 v2は紛らわしいv1です。「配列からの値」ですv2が、累積された結果です。そして、あなたの翻訳が間違っていると、私は信じています、(左から)累積値が配列の要素ではなく10 に達すると、OPのループは終了します。100ずつインクリメントする場合も同じです。ここで折り畳みを使用する場合は、左側の折り畳みを早期終了で使用しfoldlWhile ます
ウィルネス

2
それはあるあなたがにいる、ミスをすることのOK ....最も間違った答えはSE上で最もupvotesを取得し、面白い方法:)良い会社も。しかし、SO / SEの知識発見メカニズムは間違いなく壊れています。
ウィルネス

1
Clojureコードはほぼ正しいですが、(= v1 150)使用前の値v2(aka。e)はそれに加算されます。
ニコニール

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。(それほど悪くはありませんが、それでもかなり扱いにくいです。)ループに相当するものは、ジュールが言うように、適切なフォールドです。
左辺約

6
@leftaroundabout私は実際に同意しません。テール位置でのみジャンプする必要があるため、テール再帰はgotoよりも制約されていると思います。基本的にループ構造と同等です。一般に再帰はと同等だと思いますGOTO。いずれにせよ、末尾再帰をコンパイルすると、ほとんどの場合、while (true)早期復帰が単なるbreak文である関数本体を持つループに要約されます。フォールドは、ループであることは正しいですが、実際には一般的なループ構造よりも制約があります。これは、for-eachループに似ています
-J_mie6

1
@ J_mie6末尾再帰をaとして考える理由GOTOは、実際に意図したとおりに動作することを保証するために、どの状態のどの引数が再帰呼び出しに渡されるのかを面倒に記録する必要があるからです。それは、きちんと書かれた命令型ループ(各反復でステートフル変数が何であり、どのように変化するかがかなり明確である)や、ナイーブな再帰(通常は引数であまり行われず、代わりに結果は非常に直感的な方法で組み立てられます)。...
レフトアラウンド約

1
...フォールドに関しては、そうです、伝統的なフォールド(異形)は非常に特殊なループですが、これらの再帰スキームは一般化できます(ana- / apo- / hylomorphisms)。集合的に、これらは命令ループの適切な置き換えであるIMOです。
左辺約

13

私はジュールの答えが本当に好きですが、怠peopleな関数型プログラミングについて人々がよく見逃しているものをさらに指摘したかったのです。例えば:

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、値の基本的な生産者を正しく分離するという点で、引用したものよりも優れています。ただし、それらを使用する場合は、制御ロジックを直接組み込む必要がspanありlast、明示的に2つと、2つだけを使用して実装する方が適切です。それは元のコード構造とロジックにも密接に従い、保守が容易になります。
ウィルネス

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:クール-それはおそらくより慣用的なソリューションです。
ジャックB

間違っている-またはtakeWhile「一般的な操作」ではない?
ジョナサンキャスト

@jcast- takeWhile一般的な操作ですが、停止するかどうかを決定する前に変換の結果が必要になるため、この場合は特に役立ちません。あなたが使用することができます。これは問題ではありません怠惰な言語でscantakeWhileスキャンの結果について(それが使用していない間、カールBielefeldtの答えは、参照takeWhileこのしまう簡単にそうするように書き直すことができる)が、Clojureのような厳格な言語のためのリスト全体を処理し、その後結果を破棄することを意味します。ジェネレーター関数はこれを解決できますが、clojureはそれらをサポートしていると思います。
ジュール

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ステートメントの一般化バージョンです。それらは、いくつかのスキーム(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でエラーモナドを使用していると、実際のパフォーマンスが低下することはないと思います。例外処理のラインに沿って考えられる傾向がありますが、同じようには機能せず、スタックウォーキングは必要ありません。また、のようなものを使用しない文化的理由がある場合でもMonadError、基本的に同等のものEitherはエラー処理のみに偏っていないため、代替として簡単に使用できます。
ジュール

@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))ペアを使用します。

反復関数は、一般的なループよりも少し複雑です。このバージョンでは、配列の現在の要素を使用できます。[ カレー形式です。]

このような関数は通常「fold」と呼ばれます。

名前に「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」を使用したことに注意してください。これは、関数型プログラミングでは、すでに初期化された「変数」の値を変更できないためです。パフォーマンスについては心配していません。コンパイラがより効率的であると考えた場合、ライフタイム分析を介して 'new'answer'の 'answer'のメモリを再利用できます。

これを以前に開発したループ関数に組み込みます:

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

ここでの「配列」は、foldlc関数をエクスポートするモジュール名です。

「fist」、「second」は、ペアパラメーターの最初の2番目のコンポーネントを返す関数を表します

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)。より良く設計されたプログラミング言語では、しかし基本的な規律がある悪い設計言語でも、存在しないことを表現するためにオプションの型を使用します。これは、関数型プログラミングとはあまり関係がなく、最終的に問題となるので、対処しません。


0

最初に、ループを少し書き直して、ループの各反復が早期に終了するか、answer1回だけ変更するようにします。

    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

現在は純粋に機能していますが、明示的な再帰の代わりにフォールドを使用することで、効率と読みやすさの両方の観点から改善できます。

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にどのようにマッピングされるかが少しわかりにくくなりました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.