C++ のモナド

C++ のモナド?投稿のなんて奇妙な名前。しかし、それはそれほど奇妙ではありません。 std::optional を使用すると、C++17 はモナドを取得します。 Eric Niebler の range ライブラリと拡張先物もモナドです。どちらも C++20 で期待できます。

Bjarne Stroustrup は、Meeting C++ 2016 の Secret Lightning Talk で、C++20 で高い確率で得られる Concepts Lite の概念のいくつかを紹介しました。リングやモナドなどの数学的概念もありました。私の仮定はますます現実のものになります。 最新の C++ は将来のために強化されます。

標準::オプション

std::optional は、Haskell の Maybe Monad に触発されています。 std::optional は、もともと C++14 の一部として意図されていたもので、値を持つ可能性のある計算を表します。したがって、検索アルゴリズムやハッシュ テーブルのクエリでは、質問に答えられないという事実に対処する必要があります。多くの場合、そのような場合には、値がないことを表す特別な値、いわゆる結果なしを使用します。多くの場合、null ポインター、つまり結果がない場合の特別な整数値の空の文字列を使用します。この手法は、特別な方法で結果が得られない場合に対処する必要があるため、費用がかかり、エラーが発生しやすくなります。結果なしは、通常の結果と同じタイプです。 std::optional は、結果がない場合は値がありません。

以下に短い例を示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// optional.cpp

#include <experimental/optional>
#include <iostream>
#include <vector>

std::experimental::optional<int> getFirst(const std::vector<int>& vec){
 if (!vec.empty()) return std::experimental::optional<int>(vec[0]);
 else return std::experimental::optional<int>();
}

int main(){
 
 std::vector<int> myVec{1, 2, 3};
 std::vector<int> myEmptyVec;
 
 auto myInt= getFirst(myVec);
 
 if (myInt){
 std::cout << "*myInt: " << *myInt << std::endl;
 std::cout << "myInt.value(): " << myInt.value() << std::endl;
 std::cout << "myInt.value_or(2017):" << myInt.value_or(2017) << std::endl;
 }
 
 std::cout << std::endl;
 
 auto myEmptyInt= getFirst(myEmptyVec);
 
 if (!myEmptyInt){
 std::cout << "myEmptyInt.value_or(2017):" << myEmptyInt.value_or(2017) << std::endl;
 }
 
}

std::optional は現在、実験的な名前空間にあります。これは C++17 で変更されます。関数 getFirst で std::optional を使用します (7 行目)。 getFirst は、存在する場合は最初の要素を返します (8 行目)。そうでない場合は、std::optional オブジェクトを取得します (9 行目)。メイン関数で 2 つのベクトルを使用します。 17 行目と 27 行目の呼び出し getFirst は std::optional オブジェクトを返します。 myInt の場合 (19 行目)、オブジェクトには値があります。 myEmptyInt (Zeile 29) の場合、オブジェクトには値がありません。これで、myInt の値を表示できます (20 ~ 22 行目)。 22 行目と 30 行目の value_or メソッドは、値またはデフォルト値を返します。これは、std::optional に値があるかどうかによるものです。

スクリーンショットは、cppreference.com のオンライン コンパイラを使用したプログラムの出力を示しています

拡張先物

最新の C++ はタスクをサポートしています。

タスクは、チャネルによって接続された std::promise オブジェクトと std::future オブジェクトのペアです。両方の通信エンドポイントが異なるスレッドに存在する場合があります。 std::promise (送信側) は、その値を std::future (受信側) が待機しているチャネルにプッシュします。送信者は、値、通知、または例外をチャネルにプッシュできます。タスクに関するいくつかの記事を書きました。詳細は次のとおりです:タスク。

promise を作成する最も簡単な方法は、関数 std::async を使用することです。 std::async は、非同期関数呼び出しのように動作します。

int a= 2000
int b= 11;
std::future<int> sum= std::async([=]{ return a+b; });
std::cout << sum.get() << std::endl;

呼び出し std::async は、より多くのアクションを実行します。まず、通信エンドポイントの promise と future を作成します。次に、チャネルを介して両方を接続します。ラムダ関数 [=]{ return a+b;} は、promise の作業パッケージです。定義コンテキストから引数 a と b をキャプチャします。 C++ ランタイムは、promise が同じスレッドで実行されるか、別のスレッドで実行されるかを決定します。決定の基準は、おそらく作業パッケージのサイズ、システムの負荷、またはコアの数です。

future は sum.get() を呼び出して promise から値を取得します。 sum.get() を呼び出すことができるのは 1 回だけです。 promise がそのジョブで完了していない場合、get 呼び出しはブロックされます。

タスクは、保護する必要がある共有状態を持たないため、同様のより安全なスレッドの処理を提供します。したがって、競合状態が発生することはなく、デッドロックが発生することはほとんどありません。しかし、Future の C++11 実装には大きな欠点があります。 std::future オブジェクトの合成はできません。これは、C++20 の拡張された将来には当てはまりません。

この表は、拡張された先物の関数を示しています。

提案 n3721 からのいくつかのコード スニペットを次に示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
future<int> f1= async([]() {return 123;});

future<string> f2 = f1.then([](future<int> f) {
 return f.get().to_string(); 
});

future<int> futures[] = {async([]() { return intResult(125); }), 
 async([]() { return intResult(456); })};

future<vector<future<int>>> any_f = when_any(begin(futures), end(futures));


future<int> futures[] = {async([]() { return intResult(125); }), 
 async([]() { return intResult(456); })};

future<vector<future<int>>> all_f = when_all(begin(futures), end(futures));

未来 f2 が準備できていれば、3 行目の未来 f2 は準備ができています。 f1.then(...).then(...).then(...) のように先物チェーンを拡大できます。 10 行目のフューチャー any_f は、そのフューチャーのいずれかが準備完了になると準備完了になります。反対に、16 行目のフューチャー all_f は、すべてのフューチャーが準備完了になると、準備完了になります。

1つの質問はまだ答えられていません。関数型プログラミングと共通する未来は何ですか?多くの!拡張先物はモナドです。私は記事 Pure Functions でモナドの考え方を説明しました。モナドの重要なアイデアは、モナドが単純な型を強化された型にカプセル化し、これらの強化された型の関数の構成をサポートすることです。したがって、モナドには単純型を拡張型に持ち上げる関数が必要です。さらに、モナドには、強化された型で関数を構成できるようにする関数が必要です。これは関数 make_ready_future、then、future> の仕事です。 make_ready_future は単純型を拡張型にマップします。いわゆるモナド値。この関数は ID と呼ばれ、Haskell では return という名前です。 then と future> の 2 つの関数は、Haskell の bind 演算子と同等です。バインド演算子の仕事は、あるモナド値を別のモナド値に変換することです。 bind はモナド内の関数構成です。

メソッド when_any std::future のおかげで Monad Plus にもなりました。 Monad Plus は、インスタンスがモナドであり、演算子 msum を持っていることをインスタンスから要求します。したがって、std::future は C++20 で一種の加算演算をサポートします。

詳細を知りたい場合は、Bartosz Milelweski の優れたブログを読み、彼のビデオ「C++17:I See a Monad in Your Future!」をご覧ください。

次は?

Recursion, List Manipulation, and Lazy Evaluation という記事で、次のように書いています。 C++ での遅延評価に関する話は非常に短いものです。しかし、私はテンプレートなしで結論を出しました。 CRTP 慣用句と式テンプレートのおかげで、C++ は怠惰です。したがって、悪名高い CRTP イディオムについては次の投稿で書きます。