一枚で理解する関数型プログラミング - クロージャ、カリー化、Functor、モナド
関数型プログラミング
関数型プログラミングは一言で要約できます。
関数を①変数に②パラメーターに③戻り値に利用でき、④純粋関数の特性を持つ必要があります。
- 「関数」は第一級関数 (first-class function) です。
- ① 「関数」を変数に代入できます。
- ② 「関数」をパラメーターとして渡せます。
- ③ 「関数」を戻り値として返せます。
- ④ 「関数」は純粋関数の特性を持ちます。
- 参照透過性および副作用なし (Side-Effects): 外部の状態や変数、環境の影響を受けず、同じパラメーターで関数を呼び出すと常に同じ結果を返します。
関数ポインタ
関数は値ではなく参照であるため、関数を第一級関数として利用するには関数ポインタを使用する必要があります。
- ① 関数ポインタを介して関数を変数として利用できます。
- ② 関数をパラメーターとして渡したい場合は、関数ポインタを渡せば可能です。
- ③ 関数を戻り値として返したい場合は、返したい関数へのポインタを返せば可能です。
void qsort (void* base, size_t n, size_t size, int (*compare)(const void*,const void*));
上記のC言語の例では、クイックソートアルゴリズムの最後のパラメーターとして compare 関数ポインタが渡されていることがわかります。ただし、C言語における関数は、ランタイムに定義される関数ではなく事前にコンパイルされる関数であるため、第一級関数 (first-class function) ではなく第二級関数 (second-class function) と呼ぶべきだという意見もあるようです。
ラムダ (匿名関数)
ラムダはコンピュータ科学および数理論理学で用いられる概念で、現在のプログラミング関数の原型に相当する概念です。
入力値を受け取り、関数外部で定義された自由変数を利用して結果を返す関数抽象表現法です。
関数を定義するだけで実行しない点は、プログラミング内で関数を先に定義することと同一です。数理論理の概念であり関数の原型であるため、**ラムダには関数名が存在しません。**このため、**ラムダはプログラミングでは匿名関数と呼ばれることもあります。**概念は理解できますが、ではラムダはいつ、なぜ使用されるのでしょうか?
プログラミングでは、値を使用する方法が二つあります。
- 再利用性のために値を定義し、変数に割り当てて**Ⓐ変数として使用する方法**
let defined: Int = 10;
print(defined);
- 使い捨てのために値を**Ⓑ直接インラインで使用する方法**
print(10);
関数を使用する方法も値と同様に二つあります。
- 再利用性のために関数を定義し、Ⓐ参照として使用する方法
const defined = (param: Int) => { return param; };
print(defined(10));
- 使い捨てのために関数を**Ⓑ直接インラインで使用する方法**
print(((param: Int) => { return param; })(10));
ラムダは変数、パラメーター、戻り値に関数ポインタを渡すという点では通常の関数使用と同じですが、関数定義の時点が異なるため、以下の利点があります。
- 関数名が不要
- 関数の有効範囲が使い捨てであるという利点があります。
- 通常の関数は定義時に定義区画内でグローバルに存在しますが、(より専門的な言葉で言えば、グローバルな名前空間に属する静的な実装体です)
- ラムダは定義時に定義ブロック内で一時的に存在します。
ラムダを通じて関数を第一級関数として使用することで、事前に定義することなくインラインで関数を定義し、すぐに使用できるようになりました。
関数オブジェクト
オブジェクト指向プログラミングでは、関数が単独で存在することはできず、必ずクラス内に属するという制約があります。関数をラムダとして使用したい場合は、関数オブジェクトを作成し、オブジェクトレベルで利用する必要があります。オブジェクト指向プログラミングにおいて、ラムダは一見すると単独の関数として存在するように見えますが、実際には名前のないオブジェクトが単一の関数をラップしている関数オブジェクトのシンタックスシュガーと見なすことができます。
クロージャ
ラムダとクロージャは似て見えますが、厳密には異なる概念です。それぞれの定義を見てみましょう。
- ラムダ
ラムダは匿名関数を指します。
関数を一時的に変数、パラメーター、戻り値として直接使用したい場合に使われます。
- クロージャ
クロージャは関数が定義された時点の環境(状態)を保持する関数を指します。
ここでいう環境とは、クロージャが定義されるスコープにあるローカル変数を意味します。
一般的に、クロージャは関数Aの内部で関数(クロージャ)Cを定義する形で多く使用されます。**関数Aの内部にクロージャCが定義される場合、CはAの変数群をパラメーターとして渡されていないにもかかわらず、自然に参照して使用することができます。これが環境(状態)**です。
- 関数Aの変数とクロージャCの関係を
- クラスAのフィールドとメソッドCの関係と考えると理解しやすいでしょう。
クロージャを関数をオブジェクトのように使用する方法と見なすならば、クロージャを使用する理由はオブジェクトを使用する理由と似ています。
- クロージャCが参照する外部変数を状態として保持し続けるため、繰り返し呼び出してもその状態を継続して活用できます。
- 外部変数は関数Aの範囲内でのみ定義されているため、外部からのアクセスは不可能です。
func query(dbName: String) -> (String) -> (Person) {
let instance: DBInstance = DBConfig.getInstance(dbName)
// * クロージャ内部 { } で、クロージャが定義された関数内に存在する instance 変数を使用しています。
return { (tableName: String) -> (Person) in
return instance.getTable(tableName).getFirst()
}
}
Swiftのクロージャ
上記で見たように、クロージャの定義は匿名関数ではありませんが、**Swiftではクロージャが名前なしで使用されるため、実質的に匿名関数の意味で使われます。**Swiftのクロージャは、「パラメーター」と「戻り値に該当する構文」をinで区別します。
Swiftのクロージャは、以下のように必要に応じて短縮できます。
- 基本形: パラメーターの型、戻り値の型を明示し、
in以降に関数構文を記述します。
{ (parameters) -> (return_type) in return /* statements using parameters */ }
- 短縮形: 戻り値の型を暗黙的に決定します。
{ parameters in return /* statesments using parameters */ }
- 超簡潔主義者向け: 戻り値の型だけでなく、戻り式の
returnも省略しました。
{ parameters in /* statesments using parameters */ }
- 究極の短縮形: パラメーターの型を暗黙的に決定します。使用はパラメーターの順に
$0,$1で表現します。
{ /* statesments using parameters with $0, $1 ... */ }
- Trailing Closure (後続クロージャ): クロージャが最後のパラメーターとして使用される場合、パラメーターに含めず、関数の後ろにクロージャ
{ }を直接記述します。
var sorted = sort(names, { $0 < $1 })
var sorted = sort(names) { $0 < $1 }
高階関数
高階関数は、前述の第一級関数の三つの条件のうち、二番目または三番目を利用した関数を指します。
- ② 「関数」をパラメーターとして渡せます。
- ③ 「関数」を戻り値として返せます。
高階関数とは、関数をパラメーターとして、あるいは戻り値として使用することを意味します。
関数を使用する関数であるため、メタ的関数という意味で一段階上の関数、高階関数と名付けられています。
カリー化
カリー化は、第一級関数の三つの条件のうち、三番目を利用した関数を指します。
- ③ 「関数」を戻り値として返せます。
カリー化 (Currying) とは、関数が関数を返すことを意味します。 一般的にSwiftでは、関数がクロージャを返す方法でカリー化が多く使用されます。
func curringExample: (a: Int, b: Int, c: Int) -> (Int, Int) -> (Bool) { ... }
上記のcurringExampleの例を見ると、a, b, cのパラメーターを受け取り、さらに(Int, Int)の二つのパラメーターを受け取ってBoolを返す関数を返すことがわかります。
Swiftでは、「クラスのオブジェクト」が「クラスオブジェクトの関数」を呼び出す方法もカリー化を使用しています。
let someInstance = SomeClass()
someInstance.someFunction(params: /* parameters */)
上記のクラスオブジェクトの関数は、実際には以下のようにクラス関数にオブジェクトを渡して実行されます。
SomeClass.someFunction(self: someInstance)(params: /* parameters */)
余談ですが、Kotlinの拡張関数もレシーバーオブジェクトタイプ(クラス)に対する関数にレシーバーオブジェクトをパラメーターとして渡す形で使用されます。
Functor
Functorはデータ構造です。Functorの概念に入る前に、関数について簡単に見ていきましょう。
関数 = マッピング
関数は、Input Aを入れるとOutput Bという結果が出るものです。 別の見方をすれば、関数はInput A → Output B、この両者に対するマッピングです。
データ構造のマッピング
あるデータ構造全体にマッピングを適用する場合、そのデータ構造内の要素それぞれにマッピングを適用する必要があります。例えば、データ構造がリストである場合、0, 1…とイテレートすることで次のような手順を踏みます。
- Pull: 各要素を取り出し
- Mapping: マッピングを適用した後
- Push: 結果の要素を返そうとするデータ構造に入れます。
各要素に対するマッピング関数を適用できることをMappableと定義するならば、例として挙げたリストはMappableデータ構造と定義できます。上記の図の例は、Intデータ構造からStringデータ構造へと各要素を文字列化するFunctorの例です。
- Functorの定義
Functorは、Mappable (マッピング関数を持つ) データ構造です。
各要素に対するマッピング関数を適用できるデータ構造であれば、Functorと呼ぶことができます。
- ① 「単位要素」で構成されたデータ構造
- ② 各「単位要素」から「単位要素」へのマッピング関数
どのような①データ構造であっても、目的の演算を適用したい場合、データ構造内の単位要素がどのような型(T)であるかを定義し、②単位要素(T)に対するマッピングを定義するだけで済みます。①をクラスのプロパティ、②をクラスのメソッドと見なすならば、FunctorをFunction Object、関数オブジェクトと呼ぶこともあります。
Functorは、圏論 (Category Theory) において、ある圏から同じ圏への射として写像される概念に由来しています。データ構造から同じデータ構造へと各単位要素に対してマッピングすることと概念的に同一であることがわかります。このようにデータ構造 (圏) は変わらず、値だけがマッピングされることを圏論では自然変換 (natural transformation) と定義します。
HaskellのFunctor
Functorを探すとHaskellのFunctor概念に最初に触れることになりますが、HaskellのFunctorはtypeclassとして以下のように定義されており、データ構造の型を明示して必要に応じてインスタンス化して使用します。Swiftのような文法で表現すると以下のようになります。
- Functor (typeclass)
- Operation(① T) -> (② R)
- ③ S (任意のデータ構造)
- Functor実装 (instantiation)
- Operation(① Int) -> (② String) { +1して文字列化 }
- ③ List
Haskellにおいて、Functorはデータ構造の型 (③ S) と、要素 (① T) から要素 (② R) へのマッピング抽象関数を持つジェネリック (③ S, ① T, ② R) 抽象クラスと見なすことができます。Haskellでfmap()やmap()関数を定義する際、マッピング抽象関数を定義し、変換したいデータ構造を注入すると、内部の値だけが変わった同一のデータ構造が返されます。
Javaユーザーであれば、Streamのmap()関数を思い浮かべると理解しやすいでしょう。
- Streamはマッピング関数を持つことができ、Mappableなデータ構造に該当するためFunctorと呼ぶことができ、
- そのマッピング関数は
Stream.map()にラムダ(匿名関数)の形で定義してパラメーターとして渡せば良いのです。
JavaのStreamは正確にはモナドです。その理由は、マッピング関数が
Operation(T) -> (R): 要素(T)から要素(R)へマッピングするのではなく、Operation(T) -> (S): 要素(T)から全く新しいFunctor(S)を返す、という点です。
Functorでは、演算前のデータ構造から単位要素を取り出し、マッピングを適用した後、**結果の要素をデータ構造に戻していました。一方モナドでは、演算前のデータ構造から単位要素を取り出し、マッピングを適用した後、その要素をデータ構造に入れて結果のデータ構造を直接返します。**関数自体がデータ構造を返すため、マッピング関数の結果にStream.map().map().map()...のように連続してチェインで繋げることができます。
なぜ**「要素 - 要素マッピング」ではなく「要素 - データ構造マッピング」**を行うのか、以下のモナドのセクションで見ていきましょう。
Monad
モナドとは何かを一言でまとめる前に、なぜモナドが必要なのかについて説明します。プログラミング言語の「プログラミング関数」と学問における「関数」の違いをご存知でしょうか?
- 数学における関数: 関数実行時に内部でどのような状況が発生しても、最終的に値を返すことを保証します。
- プログラミング関数: 関数実行時に内部で処理できない状況が発生すると、値を返せないまま途中でExceptionを発生させます。
高等、大学数学において、どのような関数f(x)も、途中で実行中に与えられた値が間違っていたとしてExceptionを出すことはありませんでした。しかし、プログラミング関数は動作中に状態が不正になった場合、Exceptionを発生させます。
Exceptionを発生させることを純粋関数の観点からはSide-Effectと定義するため、Exceptionが発生する関数は非純粋関数と定義されます。
もしプログラミング関数において、Exception発生時に途中で停止するのではなく、その失敗状態が発生したことを状態値として結果とともに返すとすれば、Side-Effectはなくなります。これはプログラミング関数の純粋関数化と言えるでしょう。このように①状態値と関数本来の②結果値を共に返すためには、この二つをまとめるデータ構造が必要になりそうです。
Functorのマッピング関数を純粋関数にするため、関数の結果に
Exceptionが発生しうる①状態値および②結果値の両方を含むデータ構造を返してみました。
Functorのマッピング関数がデータ構造を返すようにしましたが、返されるデータ構造がさらにFunctorのデータ構造でラップされて返される問題が発生しました。
- Exception状態値を持つデータ構造が
- マッピング関数を実行したFunctorのデータ構造に、さらに一度ラップされた状態で返されました。
これは、Functorが自身のデータ構造の内部要素からそれに対する演算を実行し、結果要素をデータ構造にマッピングして返すためです。
不必要に二重にラップされるのを避け、Exception状態値のみを含むデータ構造を返すために、マッピング関数の結果をそのまま返し、マッピング関数実行前に持っているデータ構造から値を抽出するアンラップ関数を明示します。これをflatMap関数と呼び、このflatMapによって得られた「データ構造の内部要素」に対するマッピング結果である「データ構造」を直接返すのがモナドパターンです。
- Monadの定義
Monadは、Unwrap (
flatMap) 関数を含むMappableデータ構造です。
Monadのマッピング関数は、①状態値と②結果値の両方を持つデータ構造を返します。
- 「単位要素」で構成された (1) データ構造
- 「単位要素」から「Exception状態を含む (2) データ構造」へのマッピング関数
- (1) データ構造から「単位要素」を取り出すUnwrap (
flatMap) 関数
モナドに関する説明では、ContextとContentの両方を持つデータ型として説明する記事が多いです。Contextを値の有無に関する「状態」値として、Contentを私たちが演算したい「値」または「結果」値として説明します。MonadのContextが必ずしも値の有無の状態を持つ必要はありませんが、一般的に関数実行中にExceptionが発生しうるケースは値がnullである場合が多いため、多くの説明でnullableとして解説されているようです。
関数合成
モナドは、結果データ構造が状態値を持つだけでなく、関数の合成が可能であるという性質も持ちます。
- 結合性を持つ合成 (composition with associative):
二つのマッピング関数f(x), g(x)がある場合、両関数を合成するとf(g(x)) = (f.g)(x)の結果を持つ。
また、結合性の性質によりf(g(x)) = (f.g)(x) = (g.f)(x) = g(f(x))も満たす。
これで、関数型プログラミングのクロージャ、高階関数、カリー化、Functor、モナドの合計5つの概念について解説しました。ご質問や議論すべき点があれば、コメントまたは個人的にお知らせいただければ幸いです。特に今回の記事は、シニア開発者の方のご協力により、誤った内容を修正し、補完することができました。次回の記事では、Swiftのクロージャが外部変数を参照することで発生する参照循環問題と、それを解決するための手法について説明します。
- https://medium.com/@sjsyrek/five-minutes-to-functor-83ef9075978b
- https://medium.com/@jooyunghan/functor-and-monad-examples-in-plain-java-9ea4d6630c6
- http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
- http://seorenn.blogspot.com/2014/06/swift-closures.html
- https://stackoverflow.com/questions/356950/what-are-c-functors-and-their-uses
- https://stackoverflow.com/questions/2030863/in-functional-programming-what-is-a-functor