関数型プログラミングの基本原理の紹介

長い間オブジェクト指向プログラミングを学び、作業した後、私は一歩下がってシステムの複雑さについて考えました。

" Complexity is anything that makes software hard to understand or to modify." — John Outerhout

いくつかの調査を行ったところ、不変性や純粋関数などの関数型プログラミングの概念が見つかりました。これらの概念は、副作用のない機能を構築するための大きな利点であるため、システムの保守が容易になりますが、他にもいくつかの利点があります。

この投稿では、関数型プログラミングといくつかの重要な概念について、多くのコード例とともに詳しく説明します。

この記事では、関数型プログラミングを説明するためのプログラミング言語の例としてClojureを使用しています。LISPタイプの言語に慣れていない場合は、同じ投稿をJavaScriptで公開しました。見てみましょう:Javascriptの関数型プログラミングの原則

関数型プログラミングとは何ですか?

関数型プログラミングはプログラミングパラダイム(コンピュータープログラムの構造と要素を構築するスタイル)であり、計算を数学関数の評価として扱い、状態の変化や可変データを回避します— Wikipedia

純粋関数

関数型プログラミングを理解したいときに私たちが学ぶ最初の基本的な概念は純粋関数です。しかし、それは本当にどういう意味ですか?関数を純粋にするものは何ですか?

では、関数がそうであるかどうかをどうやって知るのpureでしょうか?純度の非常に厳密な定義は次のとおりです。

  • 同じ引数が与えられた場合、同じ結果を返します(これはとも呼ばれますdeterministic
  • それは観察可能な副作用を引き起こしません

同じ引数を指定すると、同じ結果が返されます

円の面積を計算する関数を実装したいとします。不純な関数はradiusパラメータとして受け取り、次にを計算しradius * radius * PIます。Clojureでは、演算子が最初に来るので、次のようにradius * radius * PIなり(* radius radius PI)ます。

なぜこれは不純な機能なのですか?関数にパラメーターとして渡されなかったグローバルオブジェクトを使用しているという理由だけで。

ここで、一部の数学者が、PI値は実際には42であり、グローバルオブジェクトの値を変更すると主張していると想像してください。

不純な関数は10 * 10 * 42=になり4200ます。同じパラメータ(radius = 10)に対して、異なる結果が得られます。直そう!

TA-DA ?!これで、常にPI 値をパラメーターとして関数に渡します。これで、関数に渡されたパラメーターにアクセスするだけです。いいえexternal object.

  • パラメータradius = 10&については、PI = 3.14常に同じ結果になります。314.0
  • パラメータradius = 10&については、PI = 42常に同じ結果になります。4200

ファイルの読み取り

関数が外部ファイルを読み取る場合、それは純粋関数ではありません。ファイルの内容が変更される可能性があります。

乱数の生成

乱数ジェネレーターに依存する関数は、純粋にすることはできません。

それは観察可能な副作用を引き起こしません

観察可能な副作用の例には、グローバルオブジェクトまたは参照によって渡されたパラメーターの変更が含まれます。

ここで、整数値を受け取り、1ずつ増加した値を返す関数を実装します。

私たちにはcounter価値があります。不純な関数はその値を受け取り、値を1増やしてカウンターを再割り当てします。

観察:関数型プログラミングでは可変性は推奨されません。

グローバルオブジェクトを変更しています。しかし、どうやってそれを作るのpureでしょうか?1ずつ増加した値を返すだけです。そのように単純です。

純粋関数increase-counterが2を返すことを確認してくださいcounter。ただし、値は同じです。この関数は、変数の値を変更せずに、増分された値を返します。

これらの2つの簡単なルールに従うと、プログラムを理解しやすくなります。これで、すべての機能が分離され、システムの他の部分に影響を与えることができなくなりました。

純粋関数は安定していて、一貫性があり、予測可能です。同じパラメーターが与えられると、純粋関数は常に同じ結果を返します。同じパラメーターの結果が異なる状況を考える必要はありません—それは決して起こらないからです。

純粋関数の利点

コードのテストは間違いなく簡単です。何もモックする必要はありません。したがって、さまざまなコンテキストで純粋関数を単体テストできます。

  • 与えられたパラメータA→関数が値を返すことを期待するB
  • 与えられたパラメータC→関数が値を返すことを期待するD

簡単な例は、数値のコレクションを受け取り、このコレクションの各要素をインクリメントすることを期待する関数です。

numbersコレクションを受け取り、関数で使用mapしてinc各数値をインクリメントし、インクリメントされた数値の新しいリストを返します。

の場合、input[1 2 3 4 5]期待されるのoutputはです[2 3 4 5 6]

不変性

時間の経過とともに変化しないか、変化することができません。

データが不変の場合、その状態は変更できません作成後。不変オブジェクトを変更したい場合は、変更できません。代わりに、新しい値で新しいオブジェクトを作成します。

Javascriptでは、通常、forループを使用します。この次のforステートメントには、いくつかの可変変数があります。

反復ごとに、isumOfValue状態を変更します。しかし、反復で可変性をどのように処理するのでしょうか?再帰!Clojureに戻る!

これでsum、数値のベクトルを受け取る関数ができました。recur背中にジャンプloop我々はベクトル空の(私たちの再帰を得るまでbase case)。「反復」ごとに、値をtotalアキュムレータに追加します。

再帰を使用して、変数を保持します不変。

観察:はい!reduceこの関数を実装するために使用できます。これはHigher Order Functionsトピックで確認します。

オブジェクトの最終状態を構築することも非常に一般的です。文字列があり、この文字列をに変換したいとしurl slugます。

RubyのOOPでは、クラスを作成しますUrlSlugify。たとえば、。そして、このクラスにはslugify!、文字列入力をに変換するメソッドがありますurl slug

綺麗な!実装されました!ここでは、各slugifyプロセスで何をしたいのかを正確に示す命令型プログラミングがあります。最初に小文字、次に不要な空白を削除し、最後に残りの空白をハイフンに置き換えます。

ただし、このプロセスでは入力状態を変更しています。

この突然変異は、関数の合成または関数の連鎖を行うことで処理できます。つまり、関数の結果は、元の入力文字列を変更せずに、次の関数の入力として使用されます。

ここにあります:

  • trim:文字列の両端から空白を削除します
  • lower-case:文字列をすべて小文字に変換します
  • replace:指定された文字列内のすべての一致インスタンスを置換に置き換えます

3つの関数すべてを組み合わせて"slugify"、文字列を作成できます。

関数組み合わせと言えば、このcomp関数を使用して3つの関数すべてを構成できます。見てみましょう:

参照透過性

を実装しましょうsquare function

この(純粋)関数は、同じ入力が与えられると、常に同じ出力を持ちます。

square functionwillのパラメータとして「2」を渡すと常に4が返されます。これで、(square 2)を4に置き換えることができます。これで完了です。私たちの機能はreferentially transparentです。

基本的に、関数が同じ入力に対して一貫して同じ結果を生成する場合、それは参照透過性です。

純粋関数+不変データ=参照透過性

この概念で、私たちにできるクールなことは、関数をメモ化することです。この関数があると想像してください。

(+ 5 8)等しいです13。この関数は常に結果になり13ます。だから私たちはこれを行うことができます:

そして、この式は常に結果になり16ます。式全体を数値定数に置き換えてメモ化することができます。

ファーストクラスのエンティティとして機能します

ファーストクラスのエンティティとしての機能のアイデアは、機能がされていることで値として扱わ及びデータとして用います。

Clojureではdefn、関数の定義に使用するのが一般的ですが、これはの構文糖衣です(def foo (fn ...))fn関数自体を返します。関数オブジェクトを指すdefnavarを返します。

ファーストクラスのエンティティとして機能することができます:

  • 定数と変数から参照してください
  • パラメータとして他の関数に渡します
  • 他の関数の結果として返します

関数を値として扱い、関数をデータのように渡すという考え方です。このようにして、さまざまな関数を組み合わせて、新しい動作を備えた新しい関数を作成できます。

2つの値を合計してから値を2倍にする関数があるとします。このようなもの:

ここで、値を減算してdoubleを返す関数:

これらの関数のロジックは似ていますが、違いは演算子関数です。関数を値として扱い、これらを引数として渡すことができれば、演算子関数を受け取る関数を作成し、それを関数内で使用できます。作りましょう!

完了!これでf引数があり、それを使用してとを処理abます。+and-関数を渡して、関数で構成し、double-operator新しい動作を作成しました。

高階関数

高階関数について話すとき、次のいずれかの関数を意味します。

  • 1つ以上の関数を引数として取る、または
  • 結果として関数を返します

double-operatorそれは引数として演算子の機能を取り、それを使用していますので、我々は上記の実装機能は、高階関数です。

あなたはおそらくすでに聞いたfiltermapreduce。これらを見てみましょう。

フィルタ

コレクションを指定して、属性でフィルタリングします。フィルタ関数は、要素結果コレクションに含まれるべきかどうかを決定するために、trueまたはfalse値を期待します。基本的に、コールバック式がの場合、フィルター関数は結果コレクションに要素を含めます。そうでなければ、そうではありません。true

簡単な例は、整数のコレクションがあり、偶数のみが必要な場合です。

命令型アプローチ

Javascriptでそれを行うための必須の方法は、次のとおりです。

  • 空のベクトルを作成する evenNumbers
  • numbersベクトルを反復処理します
  • 偶数をevenNumbersベクトルにプッシュします

filter高階関数を使用して関数を受け取り、even?偶数のリストを返すことができます。

ハッカーランクFPパスで解決した興味深い問題の1つは、フィルター配列の問題でした。問題のアイデアは、指定された整数の配列をフィルター処理し、指定された値よりも小さい値のみを出力することですX

この問題に対する必須のJavascriptソリューションは次のようなものです。

関数が実行する必要があることを正確に言います—コレクションを反復処理し、コレクションの現在のアイテムをと比較し、条件に合格xしたresultArray場合にこの要素をプッシュします。

宣言型アプローチ

しかし、この問題を解決するためのより宣言的な方法が必要であり、filter高階関数も使用します。

宣言型のClojureソリューションは次のようになります。

この構文は、そもそも少し奇妙に思えますが、理解しやすいものです。

#(> x%)は、es xを受け取り、それをcollectioの各要素と比較する無名関数ですn。%は無名関数のパラメーターを表します—この場合はhe filtter内の現在の要素です。

マップを使用してこれを行うこともできます。我々は彼らを持つ人々のマップを持っている想像nameしてage。また、指定された年齢以上の人、この例では21歳以上の人のみをフィルタリングします。

コードの要約:

  • 人々のリストがあります(nameage)。
  • 匿名関数#(< 21 (:age %))があります。t he%がコレクションの現在の要素を表すことを覚えていますか?さて、コレクションの要素は人々の地図です。我々場合はdo (:age {:name "TK" :age 26})、それは年齢VALU返しますe,。この場合には26を。
  • この無名関数に基づいてすべての人をフィルタリングします。

地図

マップのアイデアは、コレクションを変換することです。

このmapメソッドは、すべての要素に関数を適用し、戻り値から新しいコレクションを構築することにより、コレクションを変換します。

people上記と同じコレクションを取得しましょう。今は「年齢超過」でフィルタリングしたくありません。のような文字列のリストが必要ですTK is 26 years old。したがって、最終的な文字列は、コレクション内の各要素の属性である場合:name is :age years old:nameあり:ageますpeople

命令型のJavascriptの方法では、次のようになります。

宣言的なClojureの方法では、次のようになります。

全体的なアイデアは、特定のコレクションを新しいコレクションに変換することです。

もう1つの興味深いハッカーランクの問題は、更新リストの問題でした。特定のコレクションの値を絶対値で更新したいだけです。

たとえば、入力[1 2 3 -4 5]は出力である必要があります[1 2 3 4 5]。の絶対値は-4です4

簡単な解決策は、各コレクション値のインプレース更新です。

このMath.abs関数を使用して値を絶対値に変換し、インプレース更新を実行します。

これは、このソリューションを実装するための機能的な方法ではありません

まず、不変性について学びました。関数の一貫性と予測可能性を高めるには、不変性がいかに重要であるかを知っています。アイデアは、すべての絶対値で新しいコレクションを構築することです。

次に、mapここですべてのデータを「変換」してみませんか?

私の最初のアイデアは、to-absolute1つの値のみを処理する関数を作成することでした。

負の場合は、正の値(絶対値)に変換します。それ以外の場合は、変換する必要はありません。

absolute1つの値に対して行う方法がわかったので、この関数を使用して、関数に引数として渡すことができますmaphigher order functionaが関数を引数として受け取り、それを使用できることを覚えていますか?はい、地図でできます!

ワオ。とても美しい!?

減らす

reduceの考え方は、関数とコレクションを受け取り、アイテムを組み合わせて作成された値を返すことです。

人々が話す一般的な例は、注文の合計金額を取得することです。あなたがショッピングサイトにいたと想像してみてください。あなたが追加したProduct 1Product 2Product 3、およびProduct 4ショッピングカート(オーダー)へ。次に、ショッピングカートの合計金額を計算します。

必須の方法では、注文リストを繰り返し、各製品の金額を合計して合計金額にします。

を使用してreduce、を処理する関数を作成し、amount sumそれを引数として関数に渡すことができますreduce

ここに、電流を受け取るshopping-cart関数と、それらに対するオブジェクトがあります。sum-amounttotal-amountcurrent-productsum

このget-total-amount関数はreduceshopping-cartを使用してsum-amount、から開始することでに使用され0ます。

合計金額を取得するための別の方法は、構成することであるmapreduce。それはどういう意味ですか?を使用mapして値のshopping-cartコレクションに変換しamountreduce関数と+関数を使用することができます。

get-amount製品のオブジェクトを受け取り、唯一返すamount値。つまり、ここにあるのは[10 30 20 60]です。そして、reduceすべてのアイテムを合計して結合します。綺麗な!

それぞれの高階関数がどのように機能するかを調べました。簡単な例で3つの関数すべてを構成する方法の例を示したいと思います。

について話してshopping cart、私たちが私たちの注文でこの製品のリストを持っていると想像してください:

ショッピングカート内のすべての本の合計量が必要です。そのような単純な。アルゴリズム?

  • 書籍の種類でフィルタリング
  • マップを使用してショッピングカートを金額のコレクションに変換します
  • すべてのアイテムをreduceで合計して結合します

完了!?

リソース

読んで勉強したリソースをいくつか整理しました。本当に面白いと思ったものを共有しています。その他のリソースについては、関数型プログラミングGithubリポジトリにアクセスしてください。

  • Ruby固有のリソース
  • Javascript固有のリソース
  • Clojure固有のリソース

イントロ

  • JSでFPを学ぶ
  • PythonでFPを紹介する
  • FPの概要
  • 関数型JSの簡単な紹介
  • FPとは何ですか?
  • 関数型プログラミング専門用語

純粋関数

  • 純粋関数とは何ですか?
  • 純粋関数型プログラミング1
  • 純粋関数型プログラミング2

不変データ

  • 関数型プログラミング用の不変DS
  • 共有された可変状態がすべての悪の根源である理由
  • Clojureでの構造共有:パート1
  • Clojureでの構造共有:パート2
  • Clojureでの構造共有:パート3
  • Clojureでの構造共有:最後の部分

高階関数

  • Eloquent JS:高階関数
  • 楽しい楽しい機能フィルター
  • 楽しい楽しい機能マップ
  • 楽しい楽しい機能ベーシックリデュース
  • 楽しい楽しい機能高度な削減
  • Clojure高階関数
  • 純粋に機能フィルター
  • 純粋に機能的なマップ
  • 純粋に機能的な削減

宣言型プログラミング

  • 宣言型プログラミングと命令型

それでおしまい!

みなさん、この投稿を楽しんでいただければ幸いです。ここで多くのことを学んだことを願っています。これは私が学んでいることを共有するための私の試みでした。

これは、この記事のすべてのコードを含むリポジトリです

私と一緒に学びに来てください。このLearningFunctionalProgrammingリポジトリでリソースとコードを共有しています

ここであなたに役立つ何かを見たといいのですが。そしてまた会いましょう!:)

私のTwitterとGithub。☺

TK。