2

次の Scala スニペットがあります。私が与えられた問題を解決するために、私は少し「ごまかして」、var基本的に非最終的な可変データ型を使用します。その値は、ループの反復ごとに更新されます。再帰と不変のデータ型とリストのみを使用してこれを行う方法を理解しようと、かなりの時間を費やしました。

元のスニペット:

  def countChange_sort(money: Int, coins: List[Int]): Int =
    if (coins.isEmpty || money < 0)
      0
    else if (coins.tail.isEmpty && money % coins.head != 0) {
      0
    } else if (coins.tail.isEmpty && money % coins.head == 0 || money == 0) {
      1
    } else {
      -- redacted --
    }
}

i基本的に、特に累積cnt変数を排除するために使用できる基本的な手法はありますか?

ありがとう!!

4

3 に答える 3

3

機能的なスタイルの問題を解決するには、さまざまな方法があります。多くの場合、命令型アルゴリズムを設計するときとは異なる方法で問題を分析することから始めます。そのため、命令型アルゴリズムを作成してから関数型アルゴリズムに「変換」しても、非常に自然な関数型アルゴリズムは生成されません (そして、機能的なスタイルの潜在的な利点の多く)。しかし、関数型プログラミングを始めたばかりの経験豊富な命令型プログラマーの場合は、それだけで十分であり、新しい概念を理解するための良い方法です。したがって、関数型スタイルに記述したような関数をかなり非創造的な方法 (つまり、別のアルゴリズムを考え出さない) で "変換" する方法は次のとおりです。

else残りは問題ないので、式だけを考えてみましょう。

関数型スタイルにはループがないため、コード ブロック (命令型ループの本体) を何度も実行する必要がある場合、そのコード ブロックは関数でなければなりません。多くの場合、関数は単純な非再帰関数であり、map や fold などの高階関数を呼び出して実際の再帰を実行しますが、再帰的に考える練習が必要であり、それを明示的に見たいと思っていると思います。ループ条件は、ループ本体で手元にある量から計算されるため、まったく同じ条件に応じて、ループ置換関数を再帰的に呼び出すだけです。

} else {
  var cnt = 0
  var i = 0

  def loop(????) : ??? = {
    if (money - (i * coins.head) > 0) {
      cnt += countChange_sort(money - (i * coins.head), coins.tail)
      i = i + 1
      loop(????)
    }
  }

  loop(????)
  cnt
}

情報は、入力引数またはその定義を介してのみ関数に伝達れ、戻り値を介して関数から伝達されます。

定義を通じて関数に入力される情報は、関数が作成されるとき (コンパイル時、またはクロージャが作成される実行時) には一定です。cntiに含まれる情報は、呼び出しごとに異なる必要があるため、あまり役に立ちません。したがって、それらは明らかに引数として渡す必要があります。

} else {
  var cnt = 0
  var i = 0

  def loop(cnt : Int, i : Int) : ??? = {
    if (money - (i * coins.head) > 0) {
      cnt += countChange_sort(money - (i * coins.head), coins.tail)
      i = i + 1
      loop(cnt, i)
    }
  }

  loop(cnt, i)

  cnt
}

cntしかし、関数呼び出しの後に の最終値を使用したいと考えています。情報がその戻り値を介してのみ伝達される loop場合、最後の値はそれcntloop返すことによってのみ取得できます。それはとても簡単です:

} else {
  var cnt = 0
  var i = 0

  def loop(cnt : Int, i : Int) : Int = {
    if (money - (i * coins.head) > 0) {
      cnt += countChange_sort(money - (i * coins.head), coins.tail)
      i = i + 1
      loop(cnt, i)
    } else {
      cnt
    }
  }

  cnt = loop(cnt, i)

  cnt
}

coinsmoney、およびcountChange_sortは、「定義によって関数に入る」情報の例です。coinsおよびmoneyは「変数」でもありますが、 が定義された時点では一定ですlooploopの本体から離れてcountChange_sortスタンドアロン関数になりたい場合はcoinsmoney追加の引数を作成する必要があります。これらは の最上位呼び出しから渡され、countChange_sort内の各再帰呼び出しで変更されずに渡されますlooploopただし、それはそれ自体に依存することになります(算術演算子と! とcountChange_sort同様)。そのため、引数を介して関数に入らない外部のものについて関数に認識させることから逃れることはできません。*-

かなり良いですね。しかし、まだ代入ステートメントを内部で使用していますがloop、これは正しくありません。ただし、新しい値を に代入cntiて の再帰呼び出しに渡すだけなloopので、これらの代入は簡単に削除できます。

} else {
  var cnt = 0
  var i = 0

  def loop(cnt : Int, i : Int) : Int = {
    if (money - (i * coins.head) > 0) {
      loop(cnt + countChange_sort(money - (i * coins.head), coins.tail), i + 1)
    } else {
      cnt
    }
  }

  cnt = loop(cnt, i)

  cnt
}

cntここで、いくつかの明らかな単純化があります。なぜなら、ミュータブルをi初期化し、初期値を渡し、cnt一度割り当ててからすぐに返す以外は、実際には何もしていないからです。cntしたがって、(最終的に)変更可能なものをi完全に取り除くことができます。

} else {
  def loop(cnt : Int, i : Int) : Int = {
    if (money - (i * coins.head) > 0) {
      loop(cnt + countChange_sort(money - (i * coins.head), coins.tail), i + 1)
    } else {
      cnt
    }
  }

  loop(0, 0)
}

これで完了です。目に見える副作用はありません!

あなたのアルゴリズムが実際に何をするかについて、私はまったく考えていないことに注意してください (私はそれが実際に正しいかどうかを判断する試みさえしていませんが、正しいと思います)。私が行ったのは、情報は引数を介してのみ関数に入り、戻り値を介して出るという一般原則を直接適用したことだけです。式にアクセスできるすべての変更可能な状態は、実際には式の余分な隠し入力と隠し出力です。それらを不変の明示的な入力と出力にすると、不要なものを取り除くことができます。たとえば、実際には何も必要ないため、iからの戻り値に含める必要はありません。関数型スタイルへの変換により、純粋に内部にあることが明らかになりました。looploop、これを推測するために命令型バージョンのコードを実際に読む必要がありました。

cntおよびアキュムレータiとして知られているものです。アキュムレータは特別なものではなく、単なる通常の引数です。この用語は、それらの使用方法のみを指します。基本的に、アルゴリズムが進行中のデータを追跡する必要がある場合は、accumulator パラメーターを導入して、各再帰呼び出しがこれまでに行われたデータからデータを「転送」できるようにすることができます。それらはしばしば、ローカルの一時的な変更可能な変数が命令型アルゴリズムで果たす役割を果たします。

この場合のように、再帰関数の戻り値がアキュムレータ パラメーターの値になるのは非常に一般的なパターンですcnt

これらの種類の手法は、必ずしも適切な機能コードを生成するわけではありませんが、この手法を使用して、「ローカル」可変状態を使用して実装された関数を機能スタイルに変換するのは非常に簡単です。ほとんどの従来の OO プログラムに典型的な、広範囲にわたる非ローカルな可変性の使用は、このように変換するのが困難です。それはできますが、プログラム全体を一度に変更する必要があり、結果の関数には多数の余分な引数が含まれます (元のプログラムに存在していたすべての隠されたデータフローが明示的に公開されます)。

于 2012-09-26T00:24:50.850 に答える
1

誰もあなたの質問に答えていないので、ヒントをいくつかあげようと思います: ループとは何ですか? コレクションの各要素をトラバースします。条件を満たすのをやめる

再帰でできること: コレクションの各要素をトラバースします。条件を満たすのをやめます。

コレクションの各要素を出力する vars なしのメソッドを簡単に書き始めます。その後、ループと何をしているのかを見て、残りは簡単になります。(i=i + 1 のように) 変数を直接操作する代わりに、メソッドの再帰呼び出しに i + 1 を渡すだけです。

HTH

于 2012-09-25T14:09:21.873 に答える
1

あなたが具体的に持っているコードを変更するための基本的なテクニックはありません。ただし、再帰アルゴリズムを解決するための一般的なヒントを次に示します。

問題を下位の問題に分割できますか? たとえば、お金の例で、5 ドルで 10 ドルに到達しようとしている場合、それは 5 ドルで 5 ドルに到達するという問題に似ています (すでに 5 ドルを 1 回選択しています)。それを描いてルールを作ってみてください。あなたの解がどれだけ明らかに正しいかに驚くでしょう。

于 2012-09-25T06:38:33.537 に答える