한국어 | English | 日本語
Webアプリケーションエンジニア (経験8.8年)
技術・開発
engineering
ウェブフロントエンドと バックエンド開発を扱います

一枚で理解する関数型プログラミング - クロージャ、カリー化、Functor、モナド

オブジェクト指向とは異なるパラダイムである関数型プログラミングは、単に文法の違いを超え、データと関数に対する視点の転換を要求します。関数を変数のように扱い、副作用を最小限に抑えるこの哲学が、現代の開発環境でなぜ再び注目されているのか、その核心概念を探ります。
関数型プログラミングの根幹となる第一級関数の条件と、参照透過性を保証する純粋関数の概念を整理します。さらに、高階関数を活用したカリー化とクロージャ、データ構造を安全に扱うためのFunctorとモナドの定義、および関数合成の原理を学習します。

関数型プログラミング

関数型プログラミングは一言で要約できます。

関数を①変数に②パラメーターに③戻り値に利用でき、④純粋関数の特性を持つ必要があります。

関数ポインタ

関数は値ではなく参照であるため、関数を第一級関数として利用するには関数ポインタを使用する必要があります。

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の変数群をパラメーターとして渡されていないにもかかわらず、自然に参照して使用することができます。これが環境(状態)**です。

クロージャを関数をオブジェクトのように使用する方法と見なすならば、クロージャを使用する理由はオブジェクトを使用する理由と似ています。

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で区別します。

Closure Swift Example

Swiftのクロージャは、以下のように必要に応じて短縮できます。

{ (parameters) -> (return_type) in return /* statements using parameters */ }
{ parameters in return /* statesments using parameters */ }
{ parameters in /* statesments using parameters */ }
{ /* statesments using parameters with $0, $1 ... */ }
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…とイテレートすることで次のような手順を踏みます。

Function Example

各要素に対するマッピング関数を適用できることをMappableと定義するならば、例として挙げたリストはMappableデータ構造と定義できます。上記の図の例は、Intデータ構造からStringデータ構造へと各要素を文字列化するFunctorの例です。

Functorは、Mappable (マッピング関数を持つ) データ構造です。
各要素に対するマッピング関数を適用できるデータ構造であれば、Functorと呼ぶことができます。

Functor Definition

どのような①データ構造であっても、目的の演算を適用したい場合、データ構造内の単位要素がどのような型(T)であるかを定義し、②単位要素(T)に対するマッピングを定義するだけで済みます。①をクラスのプロパティ、②をクラスのメソッドと見なすならば、FunctorをFunction Object、関数オブジェクトと呼ぶこともあります。

Functorは、圏論 (Category Theory) において、ある圏から同じ圏への射として写像される概念に由来しています。データ構造から同じデータ構造へと各単位要素に対してマッピングすることと概念的に同一であることがわかります。このようにデータ構造 (圏) は変わらず、値だけがマッピングされることを圏論では自然変換 (natural transformation) と定義します。

HaskellのFunctor

Functorを探すとHaskellのFunctor概念に最初に触れることになりますが、HaskellのFunctorはtypeclassとして以下のように定義されており、データ構造の型を明示して必要に応じてインスタンス化して使用します。Swiftのような文法で表現すると以下のようになります。

Haskellにおいて、Functorはデータ構造の型 (③ S) と、要素 (① T) から要素 (② R) へのマッピング抽象関数を持つジェネリック (③ S, ① T, ② R) 抽象クラスと見なすことができます。Haskellでfmap()map()関数を定義する際、マッピング抽象関数を定義し、変換したいデータ構造を注入すると、内部の値だけが変わった同一のデータ構造が返されます。

Javaユーザーであれば、Streamのmap()関数を思い浮かべると理解しやすいでしょう。

JavaのStreamは正確にはモナドです。その理由は、マッピング関数が

Functorでは、演算前のデータ構造から単位要素を取り出し、マッピングを適用した後、**結果の要素をデータ構造に戻していました。一方モナドでは、演算前のデータ構造から単位要素を取り出し、マッピングを適用した後、その要素をデータ構造に入れて結果のデータ構造を直接返します。**関数自体がデータ構造を返すため、マッピング関数の結果にStream.map().map().map()...のように連続してチェインで繋げることができます。

なぜ**「要素 - 要素マッピング」ではなく「要素 - データ構造マッピング」**を行うのか、以下のモナドのセクションで見ていきましょう。

Monad

モナドとは何かを一言でまとめる前に、なぜモナドが必要なのかについて説明します。プログラミング言語の「プログラミング関数」と学問における「関数」の違いをご存知でしょうか?

高等、大学数学において、どのような関数f(x)も、途中で実行中に与えられた値が間違っていたとしてExceptionを出すことはありませんでした。しかし、プログラミング関数は動作中に状態が不正になった場合、Exceptionを発生させます。

Exceptionを発生させることを純粋関数の観点からはSide-Effectと定義するため、Exceptionが発生する関数は非純粋関数と定義されます。

もしプログラミング関数において、Exception発生時に途中で停止するのではなく、その失敗状態が発生したことを状態値として結果とともに返すとすれば、Side-Effectはなくなります。これはプログラミング関数の純粋関数化と言えるでしょう。このように①状態値と関数本来の②結果値を共に返すためには、この二つをまとめるデータ構造が必要になりそうです。

Functorのマッピング関数を純粋関数にするため、関数の結果に
Exceptionが発生しうる①状態値および②結果値の両方を含むデータ構造を返してみました。

Functor Return Value

Functorのマッピング関数がデータ構造を返すようにしましたが、返されるデータ構造がさらにFunctorのデータ構造でラップされて返される問題が発生しました。

これは、Functorが自身のデータ構造の内部要素からそれに対する演算を実行し、結果要素をデータ構造にマッピングして返すためです。

Monad Return Value

不必要に二重にラップされるのを避け、Exception状態値のみを含むデータ構造を返すために、マッピング関数の結果をそのまま返し、マッピング関数実行前に持っているデータ構造から値を抽出するアンラップ関数を明示します。これをflatMap関数と呼び、このflatMapによって得られた「データ構造の内部要素」に対するマッピング結果である「データ構造」を直接返すのがモナドパターンです。

Monadは、Unwrap (flatMap) 関数を含むMappableデータ構造です。
Monadのマッピング関数は、①状態値と②結果値の両方を持つデータ構造を返します。

Monad Definition

モナドに関する説明では、ContextとContentの両方を持つデータ型として説明する記事が多いです。Contextを値の有無に関する「状態」値として、Contentを私たちが演算したい「値」または「結果」値として説明します。MonadのContextが必ずしも値の有無の状態を持つ必要はありませんが、一般的に関数実行中にExceptionが発生しうるケースは値がnullである場合が多いため、多くの説明でnullableとして解説されているようです。

関数合成

モナドは、結果データ構造が状態値を持つだけでなく、関数の合成が可能であるという性質も持ちます。

Monad Composition

これで、関数型プログラミングのクロージャ、高階関数、カリー化、Functor、モナドの合計5つの概念について解説しました。ご質問や議論すべき点があれば、コメントまたは個人的にお知らせいただければ幸いです。特に今回の記事は、シニア開発者の方のご協力により、誤った内容を修正し、補完することができました。次回の記事では、Swiftのクロージャが外部変数を参照することで発生する参照循環問題と、それを解決するための手法について説明します。


一枚で理解する関数型プログラミング - クロージャ、カリー化、Functor、モナド
Author
Aaron
Posted on
Licensed Under
CC BY-NC-SA 4.0
CC BY-NC-SA 4.0
同じカテゴリーの関連記事
最新記事
LLMフィルターが奪う会話の筋肉とコミュニケーション様式
会話における無礼さを濾過し、洗練された回答を生成するLLMツールが日常化した現代において、私たちは本当に思慮深い会話をしているのだろうか?リアルタイムのコミュニケーションにおける数多くの失敗を通じて磨かれるべき会話能力が、外部ツールに依存することで退化している現象と、それがもたらす社会的な不安や世代間の行動様式の変化について考察する。
シニア採用における年俸交渉の最適なタイミングと戦略
年俸交渉は単なる数字の交換ではなく、心理的な駆け引きとタイミングが重要です。本稿では、企業側にとって、候補者が計算的な態度を取りがちな最終合格後よりも、採用プロセスの初期段階から段階的に交渉を進めることが、なぜより効率的であり、率直な情報の共有に繋がるのかを考察します。
法治主義の限界と人間の多様性
全ての人間の行為を単一の法体系で規制できるという信念は、傲慢であるかもしれない。この記事は、中世の階層的な統制から脱却し、現代の無限の自由を手に入れた人類が直面する法治主義の逆説と、多様性という名のもとに深化する社会的強制力と他者への悪魔化現象を鋭く分析する。
토스트 예시 메세지