関数型プログラミングの値を「記憶」する


20

関数型プログラミングを学ぶというタスクを自分で引き受けることにしました。これまでのところ、それは爆発でした、そして、私はそれがそうであったように「光を見ました」。残念ながら、私は質問を跳ね返すことができる機能的なプログラマーを実際には知りません。Stack Exchangeの紹介。

Web /ソフトウェア開発コースを受講していますが、講師は関数型プログラミングに精通していません。彼は私がそれを使って大丈夫であり、彼が私のコードをよりよく読むことができるように彼がそれがどのように機能するかを理解するのを助けるように私に尋ねた。

これを行う最善の方法は、値を累乗するなどの単純な数学関数を示すことだと判断しました。理論的には、事前に作成された関数を使用して簡単にそれを行うことができますが、これは例の目的に反します。

とにかく、私は値を保持する方法を理解するのに少し苦労しています。これは関数型プログラミングなので、変数を変更することはできません。これを命令的にコーディングすると、次のようになります。

(以下はすべて擬似コードです)

f(x,y) {
  int z = x;
  for(int i = 0, i < y; i++){
    x = x * z;
  }
  return x;
}

関数型プログラミングでは、確信が持てませんでした。これは私が思いついたものです:

f(x,y,z){
  if z == 'null',
    f(x,y,x);
  else if y > 1,
    f(x*z,y-1,z);
  else
    return x;
}

これは正解?zどちらの場合も値を保持する必要がありますが、関数プログラミングでこれを行う方法がわかりませんでした。理論的には、私がやった方法はうまくいきますが、それが「正しい」かどうかはわかりませんでした。それを行うより良い方法はありますか?


32
例を真剣に受け止めたい場合は、数学の問題ではなく実用的な問題を解決してください。開発者の間では「FPはすべて数学の問題を解決するのに適しています」という決まり文句であり、あなたの例がさらに別の数学関数である場合、あなたがしていることを有用に見せずに、ステレオタイプを強化するだけです。
メイソンウィーラー

12
実際の考慮事項を考慮すると、実際にあなたの試みはかなり良いです。再帰呼び出しはすべて末尾呼び出しです。つまり、関数は呼び出し後に何もしません。つまり、それをサポートするコンパイラーまたはインタープリターそれらを最適化できるため、再帰関数はに比例する量ではなく、固定量のスタックメモリを使用しyます。
8ビットツリー

1
サポートしてくれてありがとう!私はまだ非常に新しいので、私の擬似コードは完璧ではありません。@MasonWheelerこの場合、私のコードは本当に真剣に受け止められるものではありません。私はまだ学んでいますが、FPが好きな理由は、それがMath-yであるためです。私の例の要点は、FPを使用している理由を先生に説明することです。彼はそれが何であるかを本当に理解していないので、これは彼に利点を示す良い方法のように思えました。
ウセンナ

5
どの言語でコードを書く予定ですか?使用している言語に適さないスタイルを使用しようとしないでください。
カーステンS

回答:


37

まず、「光を見て」おめでとうございます。視野を広げることで、ソフトウェアの世界をより良い場所にしました。

第二に、関数型プログラミングを理解していない教授が、「インデントが見えない」などのささやかなコメント以外に、コードについて有用なことを言うことができない方法です。ほとんどのWeb開発はHTML / CSS / JavaScriptを使用して行われるため、これはWeb開発コースではそれほど驚くことではありません。Web開発の学習を実際にどの程度気にするかによって、教授が教えているツールを学習する努力をしたい場合があります(痛みがあるかもしれません-経験から知っています)。

上記の質問に対処するには、命令型コードがループを使用している場合、関数型コードが再帰的になる可能性があります。

(* raises x to the power of y *)
fun pow (x: real) (y: int) : real = 
    if y = 1 then x else x * (pow x (y-1))

このアルゴリズムは、実際には命令型コードとほぼ同じであることに注意してください。実際、上記のループは、反復的な再帰プロセスの構文糖衣であると考えることができます。

サイドノートとして、z実際には、命令コードまたは機能コードのいずれの値も必要ありません。命令関数を次のように記述する必要があります。

def pow(x, y):
    var ret = 1
    for (i = 0; i < y; i++)
         ret = ret * x
    return ret

変数の意味を変更するのではなくx


あなたの再帰powは正しくありません。そのままでは、ではなくをpow 3 3返します。それはする必要があります8127else x * pow x (y-1).
8bittree

3
おっと、正しいコードを書くのは難しいです:)修正し、型注釈も追加しました。@UcennaこれはSMLになっているはずですが、しばらく使用していないので、構文が少し間違っている可能性があります。関数を宣言するにはあまりにも多くの方法がありますが、正しいキーワードを思い出すことはできません。構文の変更以外は、JavaScriptのコードは同一です。
ガーデンヘッド

2
@jwg Javascriptにはいくつかの機能的な側面があります。関数はネストされた関数を定義し、関数を返し、パラメーターとして関数を受け入れることができます。レキシカルスコープを持つクロージャーをサポートします(ただし、Lispダイナミックスコープはありません)。状態の変更とデータの変更を控えることは、プログラマーの規律次第です。
キャスパーヴァンデンバーグ

1
@jwg「関数型」言語の合意された定義はありません(「命令型」、「オブジェクト指向」、または「宣言型」も)。可能な限り、これらの用語の使用は控えるようにします。太陽の下ではあまりにも多くの言語があり、4つのきちんとしたグループに分類できません。
ガーデンヘッド

1
人気はひどいメトリックです。だから誰かがその言語やツールXがもっと広く使われているので、それを口にしたときはいつでも議論を続けるのは無意味だと思います。私はHaskellより個人的にML言語ファミリーに精通しています。しかし、それが本当かどうかもわかりません。私の推測では、開発者の大多数はHaskellを最初に試したことがないでしょう。
ガーデンヘッド

33

これは本当にgardenheadの答えの補遺に過ぎませんが、見ているパターンの名前があります:折り畳みます。

関数型プログラミングでは、フォールドは、各操作間で値を「記憶」する一連の値を結合する方法です。数字のリストを強制的に追加することを検討してください。

def sum_all(xs):
  total = 0
  for x in xs:
    total = total + x
  return total

私たちは、値のリストとるxs、初期状態0(によって表されるtotalこの場合は)。次に、各xについてxs結合操作(この場合は加算)に従ってその値を現在の状態結合し、その結果を新しい状態として使用します。本質的に、はとsum_all([1, 2, 3])同等(3 + (2 + (1 + 0)))です。このパターンは、関数を引数として受け入れる関数である高次関数に抽出できます。

def fold(items, initial_state, combiner_func):
  state = initial_state
  for item in items:
    state = combiner_func(item, state)
  return state

def sum_all(xs):
  return fold(xs, 0, lambda x y: x + y)

この実装foldは依然として必須ですが、再帰的にも実行できます。

def fold_recursive(items, initial_state, combiner_func):
  if not is_empty(items):
    state = combiner_func(initial_state, first_item(items))
    return fold_recursive(rest_items(items), state, combiner_func)
  else:
    return initial_state

折り畳みの観点から言えば、あなたの機能は単純です:

def exponent(base, power):
  return fold(repeat(base, power), 1, lambda x y: x * y))

... where repeat(x, n)はのnコピーのリストを返しますx

多くの言語、特に関数型プログラミング向けの言語では、標準ライブラリで折りたたみが可能です。Javascriptでも名前で提供していますreduce。一般に、何らかのループを介して値を「記憶」するために再帰を使用している場合は、おそらくフォールドが必要です。


8
フォールドまたはマップによって問題を解決できる場合、確実に見つけられるようになります。FPでは、ほぼすべてのループをフォールドまたはマップとして表現できます。そのため、通常、明示的な再帰は必要ありません。
発がん物質

1
一部の言語では、次のように書くことができますfold(repeat(base, power), 1, *)
user253751

4
リコケーラーは:scan本質的にfoldだけではなく一つの値に値のリストを結合する、それが合成される場合、各中間値は、リスト生成、道に沿って吐き出すバックある全て倍作成代わりにちょうど製造中間状態を最終状態。fold(すべてのループ操作が)に関して実装可能です。
ジャック

4
@RicoKahlerそして、私が知る限り、縮小と折り畳みは同じことです。Haskellは「フォールド」という用語を使用しますが、Clojureは「縮小」を好みます。彼らの行動は私には同じように見えます。
発がん物質

2
@Ucenna:変数と関数の両方です。関数型プログラミングでは、関数は数値や文字列のような値です。変数に保存し、他の関数に引数として渡し、関数から返し、一般に他の値と同様に扱うことができます。そうcombiner_funcも引数であり、2つのアイテムをどのように結合するかを定義sum_allする匿名関数を渡しますlambda少しだけです-名前を付けずに関数値を作成します)。
ジャック

8

これは、マップと折り畳みの説明に役立つ補足的な回答です。以下の例では、このリストを使用します。このリストは不変であるため、変更されることはありません。

var numbers = [1, 2, 3, 4, 5]

コードを読みやすくするため、例では数字を使用します。ただし、折り畳みは、従来の命令型ループを使用できるものなら何でも使用できます。

マップは、何かのリストを受け取り、機能、および機能を使用して変更されたリストを返します。各アイテムは関数に渡され、関数が返すものになります。

これの最も簡単な例は、リスト内の各番号に番号を追加するだけです。疑似コードを使用して、言語に依存しないようにします。

function add-two(n):
    return n + 2

var numbers2 =
    map(add-two, numbers) 

を印刷するとnumbers2[3, 4, 5, 6, 7]各要素に2が追加された最初のリストが表示されます。使用する関数add-twoが指定されmapていることに注意してください。

Foldは似ていますが、それらを与えるために必要な関数は2つの引数を取る必要があります。通常、最初の引数はアキュムレータです(最も一般的なのは左折です)。アキュムレーターは、ループ中に渡されるデータです。2番目の引数は、リストの現在のアイテムです。上記のmap関数のように。

function add-together(n1, n2):
    return n1 + n2

var sum =
    fold(add-together, 0, numbers)

印刷sumすると、数字のリストの合計が表示されます:15。

引数foldは次のとおりです。

  1. これは、フォールドに与えている関数です。フォールドは、関数に現在のアキュムレーターとリストの現在のアイテムを渡します。関数が返すものはすべて新しいアキュムレータになり、次回に関数に渡されます。これは、FPスタイルをループしているときに値を「記憶」する方法です。私はそれに2つの数字を取り、それらを追加する機能を与えました。

  2. これが最初のアキュムレーターです。リスト内のアイテムが処理される前のアキュムレーターの起動。数字を合計するとき、数字を合計する前の合計はいくらですか?0、2番目の引数として渡した。

  3. 最後に、マップと同様に、処理する番号のリストも渡します。

それでも折り目が意味をなさない場合は、これを考慮してください。あなたが書くとき:

# Notice I passed the plus operator directly this time, 
#  instead of wrapping it in another function. 
fold(+, 0, numbers)

基本的には、渡された関数をリスト内の各アイテムの間に置き、左または右のいずれかに初期アキュムレーターを追加します(左または右の折り目によって異なります)。

[1, 2, 3, 4, 5]

になる:

0 + 1 + 2 + 3 + 4 + 5
^ Note the initial accumulator being added onto the left (for a left fold).

これは15です。

使う mapあるリストを同じ長さの別のリストに変換場合はます。

使う foldあなたは番号のリストを合計するように、1つの値にリストを有効にしたいとき。

@Jorgがコメントで指摘したように、「単一の値」は数字のような単純なものである必要はありません。リストやタプルを含む、単一のオブジェクトでもかまいません!実際に折り畳みをクリックする方法、折り畳みに関してマップを定義することでした。アキュムレータがリストであることに注意してください。

function map(f, list):
    fold(
        function(xs, x): # xs is the list that has been processed so far
            xs.add( f(x) ) # Add returns the list instead of mutating it
        , [] # Before any of the list has been processed, we have an empty list
        , list) 

正直なところ、それぞれを理解すれば、ほとんどすべてのループをフォールドまたはマップに置き換えることができることに気付くでしょう。


1
@Ucenna @Ucennaコードにはいくつかの欠陥があります(i定義されないなど)が、正しい考えがあると思います。この例の問題のx1つは、リスト全体ではなく、一度にリストの1つの要素のみが関数()に渡されることです。最初にx呼び出されると、y最初の引数として最初のアキュムレータ()が渡され、2番目の引数として最初の要素が渡されます。次回の実行時xには、左側の新しいアキュムレータ(x最初に返されたもの)と、2番目の引数としてリストの2番目の要素が渡されます。
発がん物質

1
@Ucenna基本的なアイデアが得られたので、ジャックの実装をもう一度見てください。
発がん物質

1
@Ucenna:残念ながら、foldに指定された関数がアキュムレーターを最初の引数として使用するか2番目の引数として使用するかについて、言語ごとに異なる設定があります。折り畳みを教えるために加算のような可換演算を使用するのが良い理由の1つです。
ジャック

3
foldリストを単一の値に変換する場合に使用します(数字のリストを合計するなど)。」–この「単一の値」は、リストを含めて任意に複雑になる可能性があることに注意してください!実際にfoldは、反復の一般的な方法であり、反復でできることはすべて実行できます。たとえば、次のmapように簡単に表現できます。func map(f, l) = fold((xs, x) => append(xs, f(x)), [], l)ここで計算される「単一値」foldは、実際にはリストです。
ヨルグWミットタグ

2
…おそらくリストを使用したい場合は、で実行できますfold。そして、それはリストである必要はありません、空/空でないと表現できるすべてのコレクションがそうします。これは基本的に、どのイテレーターでも実行できることを意味します。(「カタモフィズム」という言葉をそこに入れることは、初心者の紹介には多すぎると思います:-D)
ヨルグWミットタグ

1

ビルトイン機能では解決できない良い問題を見つけるのは困難です。また、組み込みの場合は、言語xの優れたスタイルの例として使用する必要があります。

たとえば、haskellでは(^)、Preludeにすでに機能があります。

または、プログラムでもっとやりたい場合 product (replicate y x)

私が言っているのは、スタイル/言語の長所を提供する機能を使用しない場合、スタイル/言語の長所を示すのは難しいということです。しかし、それが舞台裏でどのように機能するかを示すための良いステップかもしれませんが、使用しているどの言語でも最良の方法でコーディングし、必要に応じてそこから人が何をしているのかを理解するのを助けるべきだと思います。


1
この回答を他の回答に論理的にリンクするにproductfold、関数として乗算を使用し、初期引数として1を使用するショートカット関数でありreplicate、イテレータ(またはリスト;を作成する関数)であることに注意してください上記の2つは基本的にhaskellでは見分けがつかない)、同じ数の出力を与えます。この実装が上記の@Jackの答えと同じことをどのように行うかを理解するのは簡単です。同じ関数の定義済みの特殊なケースバージョンを使用して簡潔にするだけです。
ペリアタブレアッタ16
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.