web-dev-qa-db-ja.com

命令型ブレークステートメントおよびその他のループチェックと機能的に同等のものは何ですか?

たとえば、以下のロジックがあるとします。関数型プログラミングでそれをどのように書くのですか?

    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で上記を実行できない場合、プロモーションの種類はfor FPはこれを明示的に述べていません。

上記のコードは完全に必須です。将来的にはFPに移行することを念頭に置いて書かれていません。

36
Vicky

ほとんどの関数型言語で配列をループするのに最も近いものは、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
_

関数の本体:引数valuesを指定すると、引数combine(以下で定義します)とvaluesを指定して呼び出された_foldr1_を返します。 _foldr1_は、アキュムレータがリストの最初の値に設定されている(したがって、関数名の_1_)から始まり、ユーザー指定の関数を使用して左から右(通常右折りと呼ばれるため、関数名のr)。したがって、_foldr1 f [1,2,3]_はf 1 (f 2 3)(または、より一般的なCのような構文ではf(1,f(2,3)))と同等です。

_  where combine v1 v2 | v1 == 10  = v1
_

combineローカル関数の定義:_v1_と_v2_の2つの引数を受け取ります。 _v1_が10の場合、_v1_を返すだけです。この場合、v2は評価されないため、ループはここで停止します。

_                      | v1 == 150 = v1 + 100 + v2
_

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

_                      | otherwise = v1 + v2
_

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

現在、このソリューションはHaskellにいくらか特有のものです。なぜなら、結合関数が2番目の引数を評価しない場合、右の折りたたみが終了するという事実は、Haskellの遅延評価戦略が原因です。 Clojureは知りませんが、厳密な評価を使用していると思います。そのため、早期終了の特定のサポートを含む標準ライブラリにfold関数があると思います。これは、しばしばfoldWhilefoldUntilなどと呼ばれます。

Clojureライブラリのドキュメントをざっと見てみると、ほとんどの関数型言語とは命名が少し異なり、foldは探しているものではありません(並列計算を有効にすることを目的としたより高度なメカニズムです)が reduce はより直接的な同等物です。 reduced関数が結合関数内で呼び出されると、早期終了が発生します。構文を完全に理解しているとは思いませんが、あなたが探しているのは次のようなものだと思います。

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

注意:HaskellとClojureの両方の翻訳は、この特定のコードには完全に適切ではありません。しかし、それらはそれの一般的な要点を伝えます-これらの例の特定の問題については、以下のコメントの議論を参照してください。

45
Jules

簡単に再帰に変換できます。そして、それはニースのテールに最適化された再帰呼び出しを持っています。

疑似コード:

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);
}
33
Euphoric

私は本当に ジュールズの回答 が好きですが、遅延関数型プログラミングについて人々がよく見落としていること、つまりすべてが「ループの内側」にある必要はないということも指摘したかったのです。例えば:

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

ロジックの各部分を個別の関数で計算してから、一緒に構成できることがわかります。これにより、通常はトラブルシューティングがはるかに簡単になる小さな機能が可能になります。おもちゃの例では、これにより、削除するよりも複雑になる可能性がありますが、実際のコードでは、分割関数は全体よりもはるかに単純であることがよくあります。

13
Karl Bielefeldt

ほとんどのリスト処理の例では、リスト全体を操作する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))))))) 

編集:ジュールは、Clojureのreducedoが早期終了をサポートすることを指摘しました。これを使用するとよりエレガントになります:

(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)))

いずれにせよ、命令型言語と同じように関数型言語でも何でもできますが、エレガントな解決策を見つけるには、考え方を多少変えなければならないことがよくあります。命令型コーディングでは、リストを段階的に処理することを考えますが、関数型言語では、リスト内のすべての要素に適用する操作を探します。

6
JacquesB

他の回答で指摘されているように、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))

これは、特定の状況に最適なソリューションです。また、reducedtransduceを組み合わせることで、多くのマイレージを得ることができます。これにより、mapfilterなどのトランスデューサーを使用できます。ただし、一般的な質問に対する完全な回答。

エスケープ継続は、breakおよびreturnステートメントの一般化されたバージョンです。それらは、いくつかのスキーム(call-with-escape-continuation)、共通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でさまざまなエラーモナドの1つを使用することもできます。これらは主にエラー処理コンストラクトであるため、早期の復帰に例外またはエラーモナドを使用することは、通常、文化的に受け入れられず、おそらく非常に遅くなります。

高次関数から末尾呼び出しにドロップダウンすることもできます。

ループを使用する場合、ループ本体の終わりに到達すると、自動的に次の反復に入ります。 continueを使用して次の反復に早く入るか、break(またはreturn)を使用してループを終了できます。末尾呼び出し(または末尾再帰を模倣するClojureのloop構成)を使用する場合、次の反復に入るには常に明示的な呼び出しを行う必要があります。ループを停止するには、再帰呼び出しを行わず、直接値を指定します。

(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))))))
4
nilern

複雑な部分はループです。それから始めましょう。ループは通常、単一の関数で反復を表現することにより、機能的なスタイルに変換されます。反復はループ変数の変換です。

次に、一般的なループの機能的な実装を示します。

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」を付けて、このバージョンのフォールドが、ループを早期に停止するかどうかといつ停止するかを制御する条件をとることを示します。

もちろん、そのようなユーティリティ関数は、使用されている関数型プログラミング言語に同梱されているベースライブラリですぐに利用できる可能性があります。デモ用にここに書きました。

命令型の場合の言語のツールがすべて揃ったので、次に、サンプルの特定の機能を実装します。

ループ内の変数はペアです( '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)。より適切に設計されたプログラミング言語では、いくつかの基本的な規律のある不適切に設計された言語でさえ、存在しないことを表現するために optional type を使用します。これは関数型プログラミングとはあまり関係がありませんが、最終的には問題なので、私はそれを扱いません。

2
libeako

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

    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にどのようにマッピングするかは少し不明確になっています。