あなたは関数型プログラマになりたい(パート1)

関数型プログラミングの概念を理解するための最初の一歩を踏み出すことは、最も重要で、時には最も困難な手順です。しかし、そうである必要はありません。正しい視点ではありません。

運転することを学ぶ

最初に運転することを学んだとき、私たちは苦労しました。他の人がそれをしているのを見たとき、それは確かに簡単に見えました。しかし、思ったより難しいことがわかりました。

私たちは親の車で練習しましたが、自分の近所の道路をマスターするまでは、実際に高速道路に出かけませんでした。

しかし、繰り返し練習し、両親が忘れたがるパニックな瞬間を通して、私たちは運転することを学び、ついにライセンスを得ました。

私たちのライセンスが手元にあれば、私たちはできる限りチャンスを車から取り去ります。旅行のたびに、私たちはどんどん良くなり、自信が上がりました。その後、私たちが他の人の車を運転しなければならなかった、または私たちの車がついに幽霊をあきらめ、新しいものを買わなければならなかった日がやってきました。

別の車のハンドルを握った最初の時間はどうでしたか?初めて運転したようなものでしたか?程遠い。初めて、それはとても外国のものでした。それ以前は車に乗っていましたが、乗客としてだけでした。今回は運転席にいた。すべてのコントロールを備えたもの。

しかし、2台目の車を運転するとき、キーがどこに行くのか、ライトがどこにあるのか、方向指示器をどのように使用するのか、サイドミラーをどのように調整するのかなど、いくつかの簡単な質問をしました。

その後、それはかなりスムーズなセーリングでした。しかし、なぜ今回が初めてに比べてこんなに簡単だったのでしょうか?

それは、新しい車が古い車によく似ていたからです。車に必要な基本的なものはすべて同じで、ほとんど同じ場所にありました。

いくつかの実装が異なっていて、おそらくいくつかの追加機能があったかもしれませんが、最初に運転したときでも、2回目でも使用しませんでした。最終的に、すべての新機能を学びました。少なくとも私たちが気にかけたもの。

プログラミング言語の学習は、このようなものです。最初が一番難しいです。しかし、一度ベルトの下に置くと、後続のものはより簡単になります。

最初に第2言語を開始するときに、「モジュールを作成するにはどうすればよいですか?配列をどのように検索しますか?サブストリング関数のパラメーターは何ですか?」

この新しい言語を習得できると確信しています。これは、あなたの人生を楽にするためのいくつかの新しいことで、古い言語を思い出させるからです。

初めての宇宙船

人生で1台の車を運転していたとしても、数十台の車を運転していたとしても、宇宙船のハンドルを握ろうとしていると想像してください。

宇宙船を操縦する場合、道路での運転能力が大いに役立つとは思わないでしょう。スクエアゼロからやり直すことになります。 (私たちは結局プログラマーです。ゼロから数えます。)

あなたは、物事が宇宙で非常に異なっており、この仕掛けを飛ぶことは地上での運転とは非常に異なることを期待してトレーニングを開始します。

物理学は変わっていません。同じ宇宙内をナビゲートする方法。

そして、それは関数型プログラミングの学習でも同じです。あなたは物事が非常に異なることを期待する必要があります。そして、あなたがプログラミングについて知っていることの多くは翻訳されません。

プログラミングは思考であり、関数型プログラミングは非常に異なる考え方を教えます。そのため、おそらく古い考え方に戻ることはないでしょう。

あなたが知っているすべてを忘れる

人々はこのフレーズを言うのが大好きですが、それはある種の真実です。関数型プログラミングの学習は、ゼロから始めるようなものです。完全ではありませんが、効果的です。同様の概念はたくさんありますが、すべてを再学習しなければならないと期待するのが最善です。

適切な視点で適切な期待を持ち、適切な期待で物事が困難になっても辞めません。

関数型プログラミングではこれ以上できないプログラマーとしての経験があります。

あなたの車のように、あなたは私道から抜け出すためにバックアップを使用していました。しかし、宇宙船では、その逆はありません。今、あなたは「何?逆転しない?!逆転せずに運転するのはどうすればいいですか?!」

さて、宇宙船では3次元空間で操縦する能力があるため、宇宙船を逆にする必要はありません。これを理解すると、逆のことを見逃すことはありません。実際、いつか、車の制限が本当にあったことを思い出すでしょう。

関数型プログラミングの学習には時間がかかります。我慢してください。

それでは、命令型プログラミングの寒い世界を脱出し、関数型プログラミングの温泉にゆっくり浸りましょう。

このマルチパートの記事に続くのは、最初の関数型言語に飛び込む前に役立つ関数型プログラミングの概念です。または、すでに思い切って取り組んでいる場合、これは理解を深めるのに役立ちます。

急がないでください。この時点から読み進めて、コーディングの例を理解してください。各セクションの後で読むのを止めて、アイデアを吸収してください。その後、戻って終了します。

最も重要なことは、理解することです。

純度

関数型プログラマーが純度について話すとき、彼らは純粋な関数について言及しています。

純粋な関数は非常に単純な関数です。それらは、入力パラメーターでのみ動作します。

以下は、純粋関数のJavascriptの例です。

var z = 10;
関数add(x、y){
    return x + y;
}

add関数はz変数に触れないことに注意してください。 zから読み取らず、zに書き込みません。 xとyの入力のみを読み取り、それらを加算した結果を返します。

それは純粋な機能です。 add関数がzにアクセスした場合、純粋ではなくなります。

考慮すべきもう1つの機能を次に示します。

関数justTen(){
    10を返します。
}

関数justTenが純粋な場合、定数のみを返すことができます。どうして?

何も入力していないからです。そして、純粋に、自身の入力以外にはアクセスできないため、返すことができるのは定数のみです。

パラメータを取らない純粋な関数は機能しないため、あまり有用ではありません。 justTenを定数として定義した方が良いでしょう。

最も有用な純粋関数は、少なくとも1つのパラメーターを取る必要があります。

次の機能を検討してください。

関数addNoReturn(x、y){
    var z = x + y
}

この関数が何も返さないことに注意してください。 xとyを追加して変数zに入れますが、返しません。

入力のみを処理するため、純粋な関数です。追加されますが、結果が返されないため、役に立ちません。

すべての有用な純粋関数は何かを返す必要があります。

最初の追加機能をもう一度考えてみましょう。

関数add(x、y){
    return x + y;
}
console.log(add(1、2)); // 3を印刷します
console.log(add(1、2)); //まだ3を出力
console.log(add(1、2)); //常に3を印刷します

add(1、2)は常に3であることに注意してください。大きな驚きではなく、関数が純粋だからです。 add関数が外部値を使用した場合、その動作を予測することはできません。

純粋関数は、同じ入力が与えられると常に同じ出力を生成します。

Pure Functionsは外部変数を変更できないため、次の関数はすべて不純です。

writeFile(fileName);
updateDatabaseTable(sqlCmd);
sendAjaxRequest(ajaxRequest);
openSocket(ipAddress);

これらの機能にはすべて、副作用と呼ばれるものがあります。それらを呼び出すと、ファイルやデータベーステーブルを変更したり、サーバーにデータを送信したり、OSを呼び出してソケットを取得したりします。これらは、入力を操作して出力を返すだけではありません。したがって、これらの関数が返すものを予測することはできません。

純粋な関数には副作用はありません。

Javascript、Java、C#などの命令型プログラミング言語では、副作用はどこにでもあります。これは、プログラム内のどこでも変数を変更できるため、デバッグが非常に困難になります。変数が間違った時間に間違った値に変更されたためにバグがある場合、どこを見ますか?どこにでも?それは良いことではありません。

この時点で、おそらく「純粋な機能だけで何をしているのでしょうか!」

関数型プログラミングでは、純粋な関数を書くだけではありません。

関数型言語は副作用を排除することはできず、副作用を制限することしかできません。プログラムは現実の世界と連動する必要があるため、すべてのプログラムの一部は不純でなければなりません。目標は、不純なコードの量を最小限に抑え、プログラムの残りの部分から分離することです。

不変性

次のコードを最初に見たときのことを覚えていますか:

var x = 1;
x = x + 1;

そして、あなたが数学の授業で学んだことを忘れるようにあなたに教えた人は誰ですか?数学では、xをx + 1に等しくすることはできません。

しかし、命令型プログラミングでは、xの現在の値に1を加算し、その結果をxに戻すことを意味します。

まあ、関数型プログラミングでは、x = x + 1は違法です。ですから、数学で忘れたことを覚えておく必要があります…。

関数型プログラミングには変数はありません。

格納された値は、履歴のために変数と呼ばれますが、定数です。つまり、xが値を取得すると、その値は一生に渡ります。

心配しないでください。xは通常ローカル変数なので、通常は寿命が短くなります。しかし、生きている間は、決して変化することはありません。

以下は、Web開発用の純粋な関数型プログラミング言語であるElmの定数変数の例です。

addOneToSum y z =
    させる
        x = 1
    に
        x + y + z

MLスタイルの構文に慣れていない場合は、説明させてください。 addOneToSumは、yとzの2つのパラメーターを受け取る関数です。

letブロック内では、xは値1にバインドされています。つまり、残りの期間は1に等しくなります。関数が終了すると寿命が終了し、letブロックが評価されるとより正確になります。

inブロック内では、計算にletブロックで定義された値を含めることができます。バツ。計算結果x + y + zが返されます。より正確には、x = 1なので1 + y + zが返されます。

繰り返しになりますが、「変数を使わずに何をすることをお勧めしますか?!」

変数を変更するタイミングについて考えてみましょう。頭に浮かぶ2つの一般的なケースがあります。複数値の変更(オブジェクトまたはレコードの単一の値の変更など)と単一値の変更(ループカウンターなど)です。

関数型プログラミングは、値が変更されたレコードのコピーを作成することにより、レコード内の値の変更を処理します。これを可能にするデータ構造を使用して、レコードのすべての部分をコピーすることなく、これを効率的に行います。

関数型プログラミングは、コピーを作成することにより、単一値の変更をまったく同じ方法で解決します。

ああ、はい、ループがないことによって。

「変数やループはありませんか?!大嫌い!!!"

つかまっている。ループを実行できないわけではありません(しゃれを意図していません)。for、while、do、repeatなどの特定のループ構造がないだけです。

関数型プログラミングでは、再帰を使用してループを実行します。

Javascriptでループを実行する方法は2つあります。

//単純なループ構造
var acc = 0;
for(var i = 1; i <= 10; ++ i)
    acc + = i;
console.log(acc); // 55を印刷します
//ループ構造または変数なし(再帰)
関数sumRange(start、end、acc){
    if(開始>終了)
        return acc;
    return sumRange(start + 1、end、acc + start)
}
console.log(sumRange(1、10、0)); // 55を印刷します

新しいアプローチ(start + 1)と新しいアキュムレータ(acc + start)で自分自身を呼び出すことにより、関数アプローチである再帰がforループと同じように達成されることに注意してください。古い値は変更されません。代わりに、古い値から計算された新しい値を使用します。

残念ながら、少し時間をかけて勉強しても、Javascriptでこれを確認するのは困難です。2つの理由があります。 1つ目は、Javascriptの構文がうるさく、2つ目は、再帰的に考えることに慣れていないことでしょう。

Elmでは、読みやすく、したがって理解しやすくなっています。

sumRange start end acc =
    開始>終了の場合
        acc
    他に
        sumRange(start + 1)end(acc + start)

実行方法は次のとおりです。

sumRange 1 10 0 =-sumRange(1 + 1)10(0 + 1)
sumRange 2 10 1 =-sumRange(2 + 1)10(1 + 2)
sumRange 3 10 3 =-sumRange(3 + 1)10(3 + 3)
sumRange 4 10 6 =-sumRange(4 + 1)10(6 + 4)
sumRange 5 10 10 =-sumRange(5 + 1)10(10 + 5)
sumRange 6 10 15 =-sumRange(6 + 1)10(15 + 6)
sumRange 7 10 21 =-sumRange(7 + 1)10(21 + 7)
sumRange 8 10 28 =-sumRange(8 + 1)10(28 + 8)
sumRange 9 10 36 =-sumRange(9 + 1)10(36 + 9)
sumRange 10 10 45 =-sumRange(10 + 1)10(45 + 10)
sumRange 11 10 55 =-11> 10 => 55
55

おそらくforループの方が理解しやすいと考えているでしょう。それは議論の余地があり、親しみの問題である可能性が高いですが、非再帰ループには可変性が必要です。これは悪いことです。

ここでは不変性の利点について完全には説明していませんが、詳細については「プログラマーに制限が必要な理由」の「グローバルな可変状態」セクションを参照してください。

明らかな利点の1つは、プログラムの値にアクセスできる場合、読み取りアクセスしかできないことです。つまり、他の誰もその値を変更できません。あなたも。したがって、偶発的な突然変異はありません。

また、プログラムがマルチスレッドである場合、他のスレッドはあなたの下からラグを引き出すことができません。その値は一定であり、別のスレッドがそれを変更したい場合、古い値から新しい値を作成する必要があります。

90年代半ばに、Creature Crunch用のゲームエンジンを作成しましたが、バグの最大の原因はマルチスレッドの問題でした。当時の不変性について知っていたらよかったのに。しかし、当時は、ゲームのパフォーマンスに関する2倍速と4倍速のCD-ROMドライブの違いが心配でした。

不変性は、よりシンプルで安全なコードを作成します。

私の脳!!!!

今のところ十分です。

この記事の以降の部分では、高階関数、機能構成、カリー化などについて説明します。

次:パート2

これが気に入ったら、下のをクリックして、他の人がこれを中に表示するようにします。

ElmのFunctional Programmingを使用してWebアプリの開発を学び、支援するWeb開発者のコ​​ミュニティに参加したい場合は、FacebookグループのLearn Elm Programming https://www.facebook.com/groups/learnelm/をご覧ください。

私のTwitter:@cscalfani