関数型プログラミングの6つの概念。 利点と使用例

良い一日! 私の名前はIvan Smolinです。iOSプラットフォームのモバイルアプリケーションの開発者です。 今日は、関数型プログラミングの世界に飛び込むことをお勧めします。 この記事は、ほとんどの場合、実用的というよりも理論的です。 その中で、関数型プログラミングの基本概念を定義し、C、Objective-C、Swift、Haskellでの実装例を示します。



関数型プログラミングは、数学的なスタイル、不変性、表現力、および変数と状態の使用の削減( リンク )で関数を計算することに焦点を当てたプログラミングパラダイムです。



6つの基本概念があります。







ファーストクラス機能



これは何ですか



これは、他のエンティティで一般的に利用可能な操作をサポートするエンティティです。 これらの操作は通常です。 include:エンティティを引数として渡し、関数からエンティティを返し、変数に割り当てます。



役に立つより



関数の操作を簡素化し、より多くの機会と機能の使用方法を提供します。



使用例



typedef void (*callback_func_t) (char*); void list_files(char* path, callback_func_t callback) { // recursive read directory structure ... } void print_file_path(char* file_path) { printf("%s\n", file_path); } callback_func_t get_print_func() { callback_func_t callback_var = &print_file_path; return callback_var; } list_files("/tmp/", get_print_func());
      
      





上記の例では、get_print_func関数は関数への参照を格納する変数を作成し、それを返します。 そして、コードの下で、get_print_func関数によって返された結果を別の関数に渡します。 これらは、ファーストクラスの機能のおかげで利用可能な操作です。



高階関数



これは何ですか



これは、他の機能で動作する機能です。 パラメータとして取得するか、返すことにより動作します。



役に立つより



一次関数と同様に、この概念は関数を操作するためのより多くのオプションを提供します。 また、この概念は、関数をイベントハンドラとして使用する可能性を開きます。システムまたはライブラリは、渡されたファーストクラスの関数を呼び出すことで、それを通知できます。



使用例



見る 前の例。 ディレクトリを読み込む機能があります。 そして、すべてのサブフォルダーを再帰的に読み取ります。 見つかったファイルごとに、渡された関数-コールバックを呼び出します。



Cの例は、前世紀の70年代には早くも、一流以上の関数で動作することが可能であったことを示しています。 Objective-Cはブロックを導入しました。 関数とは異なり、変数や何らかの状態をキャプチャできます。 Swiftに閉鎖が登場しました。 本質的に、これはObjective-Cブロックと同じです。



ネット機能



これは何ですか



これは、2つの条件を満たす関数です。 この関数は、常に同じ入力パラメーターで同じ結果を返します。 そして、結果の計算は、目に見える意味的な副作用や外部への出力を引き起こしません。



役に立つより



純粋な関数は通常、適切に記述されたコードの指標です。そのような関数はテストで簡単にカバーでき、簡単に移植して再利用できるためです。



使用例



以下のスニペットは、純粋な関数の例を示しています(「純粋」というコメントが付いています)。



 func quad1(x: Int) -> Int { // pure func square() -> Int { return x * x } return square() * square() } func quad2(x: Int) -> Int { // pure func square(y: Int) -> Int { // pure return y * y } return square(x) * square(x) } func square(x: Int) -> Int { // pure return x * x } func cube(x: Int) -> Int { return square(x) * x } func printSquareOf(x: Int) { print(square(x)) } let screenScale = 2.0 func pixelsToScreenPixels(pixels: Int) -> Int { return pixels * Int(screenScale) }
      
      





どこでも使用されます。 たとえば、ほとんどすべてのプログラミング言語の標準数学ライブラリには、ほとんど純粋な関数のみが含まれています。



不変の状態



これは何ですか



不変状態とは、オブジェクトの作成後に変更できないオブジェクトの状態です。 ここでのオブジェクトの状態の下では、そのプロパティの値のセットを意味します。



便利なもの



不変オブジェクトは、ライフサイクル中に状態を変更できないことを保証するため、そのようなオブジェクトをプログラム内の他の場所で使用または転送しても、予期しない結果につながることはありません。 これは、マルチスレッド環境で作業する場合に特に重要です。



Cでは、そのままでは、不変オブジェクトを作成する方法はありません。 constキーワードは、現在のコンテキストでのみ値を変更することを禁止しますが、この値へのリンクを関数に渡すと、この関数はこのリンクにあるデータを変更できます。 この問題は、カプセル化(パブリックおよびプライベートヘッダーファイル)によって解決できます。 ただし、この場合、変更からデータを「保護」するメカニズムを独自に実装する必要があります。



Objective-Cでは、新しいものも何もありません。 内部状態と可変(可変)アナログの変更を許可しない基本クラスのみが追加されました。



Swiftにはletキーワードがあります。これにより、作成後に変数または構造を変更できないことが保証されます。



使用例



Swiftで不変の値を使用する例:



 let one = 1 one = 2 // compile error let hello = "hello" hello = "bye" // compile error let argv = ["uptime", "--help"] argv = ["man", "reboot"] // compile error argv[0] = "man" // compile error
      
      





オプションタイプ



これは何ですか



オプションタイプは、オプション値のカプセル化を表す汎用タイプです。 このタイプには、特定の値またはnull値が含まれます。



便利なもの



nullの概念をより高いレベルに引き上げます。 言語の構文構成を使用して、オプションの値を操作できます。



使用例



ほとんどすべての現代の、特に若い言語には、オプションの型の概念と、それを操作するための構文構造があります。 Swiftでは、これはif letまたはswitch case構造です:



 let some: String? = nil switch (some) { case .None: print("no string") case .Some(let str): print("string is: \(str)") }
      
      





パターンマッチング



パターンマッチング-トークンのシーケンスを特定のパターンと照合する行為。



便利なもの



問題の解決に焦点を当てた短いコードを書くことができます。



使用例



Haskellの例を次に示します。 私の意見では、パターンマッチングの最良の例です。



 sum :: (Num a) => [a] -> a sum [] = 0 -- no elements sum (x:[]) = x -- one element sum (x:xs) = x + sum xs -- many elements
      
      





sum関数は、入力としてオブジェクトの配列を受け取ります。 sum関数が空の配列を受け取った場合、要素の合計は0になります。配列にオブジェクトが1つ含まれている場合は、このオブジェクトを取得します。 この関数をパターンとして説明しました。 これは、入力値に応じて、この関数が機能するために可能なすべての(または現時点で必要な)オプションを記述することを意味します。 ifおよびその他の条件ステートメントなし。



 addOne :: Maybe Int -> Maybe Int addOne (Just a) = Just (a + 1) -- not empty value addOne Nothing = Nothing -- empty value
      
      





addOne関数は、数値に1を追加します。 入力では、Maybe Int型の引数を取り、出力では、同様の型の値を返します。 たぶん、値を含む(Just a)または何も含まない(Nothing)モナドです。 addOne関数は次のように機能します:関数の引数に値がある場合(a)、値を追加して引数を返します。何もない(Nothing)場合、何も返しません(Nothing)。



Swiftでは、パターンマッチングは次のようになります。



 let somePoint = (1, 1) switch somePoint { case (0, 0): print("point at the origin") case (_, 0): print("(point on the x-axis") case (0, _): print("point on the y-axis") case (-2...2, -2...2): print("point inside the box") default: print("point outside of the box") }
      
      





私の意見では、パターンマッチングはSwiftではかなり制限されています。switchステートメントでのみケースをチェックできますが、これは非常に柔軟に行うことができます。



怠azineまたはレイジーコンピューティング



これは何ですか



遅延計算は、式の値が必要になるまで式の計算を遅らせる計算戦略です。



役に立つより



特定の、または事前に決定された不確実な時点まで、一部のコードの計算を延期できます。



使用例



 let dateFormatter = NSDateFormatter() struct Post { let id: Int let title: String let creationDate: String lazy var createdAt: NSDate? = { return dateFormatter.dateFromString(self.creationDate) }() }
      
      





初期化後に、クラスのフィールドを初期化するために使用できます。 この手法により、いくつかのクラスコンストラクターでのフィールド初期化コードの重複を回避し、必要になるまでこのフィールドの初期化を延期することができます。 上記の例では、createdAtフィールドの値は、最初にアクセスされたときに計算されます。



無限のデータ構造



無限データ構造は、定義が無限範囲または連続再帰の観点から与えられる構造ですが、実際の値は必要な場合にのみ計算されます。



役に立つより



この構造の値の計算にリソースを浪費することなく、無限または巨大なサイズのデータ​​構造を定義できます。



使用例



Swiftの例を次に示します。 範囲を1兆から1兆に増やします。 この範囲のマップを作成します-10億の値を文字列に変換します。 このような数の行は、パーソナルコンピューターのRAMにはほとんど収まりません。 しかし、それにもかかわらず、安全にこれを実行し、必要な値を取ることができます。 この例では、map関数に渡されたラムダは2回だけ呼び出されます。 すべてが非常に遅れて行われます。



 let bigRange = 1...1_000_000_000_000 // from one to one trillion let lazyResult = bigRange.lazy.map { "Number \($0)" } // called 3 times let fourHundredItem = lazyResult[400] // "Number 400" let lazySlice = lazyResult[401...450] // Slice<LazyMapCollection<Range<Int>, String>> let fiveHundredItem = lazyResult[500] // "Number 500"
      
      





Swiftでは、常に範囲に制限されます。 無限の一連の値を作成することはできません。 工夫してそれを別の方法で行うこともできますが、すぐに使用できます。 しかし、Haskellでは。



1から無限までのリストを作成できます。 すべての要素(数字と数字、文字列になります)へのマップを作成します。 次に、スライスまたはリストのいずれかの要素を使用します。 スライスも遅延リストとともに返されます。



 infiniteList = [1..] mappedList = map (\x -> "Number " ++ show x) infiniteList -- called 2 times fourHundredItem = mappedList !! 400 -- “Number 400” lazySlice = take (450 - 400) (drop 400 mappedList) -- [401..450] fiveHundredItem = mappedList !! 500 -- “Number 500” lazyArray = [2+1, 3*2, 1/0, 5-4] -- item values not evaluated lengthOfArray = length lazyArray -- still not evaluated
      
      





Haskellは私が今まで見た中で最も怠laな言語です。 その中の配列には、ボックス化(パック)および非ボックス化(アンパック)要素を含めることができます。 要素の値を取得する必要のない配列の操作中に配列の要素がパックされる(まだ計算されていない)場合、これらの値は計算されません。 そのような操作の例は、長さの方法です。



ラムダ計算



これは何ですか



ラムダ計算は、変数のバインドと置換によるアプリケーションの操作と関数の抽象化に基づいて計算を表現するための数学論理の形式的なシステムです。



便利なもの



ラムダ計算の概念は、匿名関数の概念をプログラミング言語にもたらします。プログラミング言語は、外部の(関数に関して)変数をキャプチャできます。



使用例



以下はSwiftの例で、名前付き関数の代わりにラムダを使用しています。



 let numbers = [0,1,2,3,4,5,6,7,8,9,10] let stringNumbers = numbers.map { String($0) } // ["0","1","2","3","4","5","6","7","8","9","10"] let sum = numbers.reduce(0, combine: { $0 + $1 }) // 55 let avg = numbers.reduce(0.0, combine: { $0 + Double($1) / Double(numbers.count) }) // 5.0 let from4To7SquareNumbers = numbers.filter { $0 > 3 }.filter { $0 < 7 }.map { $0 * $0 } // [16, 25, 36]
      
      





1行の合計または平均を計算する方法の例を次に示します。 そしてフィルター。



ラムダ計算の概念は、プログラミング言語のカリー化の概念も導入します。 カリー化により、複数のパラメーターを持つ関数を1つのパラメーターを持つ複数の関数に分割できます。 これにより、中間関数の計算結果を取得し、これらの関数に異なる引数を適用していくつかの結果を取得する機会が得られます。



カレーのケーススタディ



 func raiseToPowerThenAdd(array: [Double], power: Double) -> ((Double) -> [Double]) { let poweredArray = array.map { pow($0, power) } return { value in return poweredArray.map { $0 + value } } } let array = [3.0, 4.0, 5.0] let intermediateResult = raiseToPowerThenAdd(array, power: 3) intermediateResult(0) // [27, 64, 125] intermediateResult(5) // [32, 69, 230] intermediateResult(10) // [37, 74, 135]
      
      





ここで、配列内の数値の度合いを計算した結果を取得し、この結果に特定の数値を追加します。 raiseToPowerThenAdd関数が呼び出されたときに度の計算が1回だけ行われるという事実に注意することが重要です。



おわりに



私の意見では、モバイルソフトウェアを開発するための最も重要な概念(コード品質の観点から)は、純粋な機能の概念とオプション性の概念です。 最初のものは、コードをよりポータブルで、高品質で、テストしやすくする方法について、明確でシンプルなアイデアを提供します。 2番目は、外部から発生する可能性のある極端なケースとエラーについて考えさせ、それらを正しく処理します。



この資料が有用であり、コードがさらに改善されることを願っています。



Ivan Smolin、iOS開発者。



All Articles