C++でMaybeモナド (std::optional), Eitherモナド

(2009.1.3)

C++ で Haskell の Maybeモナドっぽいものを書いてみます。意味が近い boost::optional を活用してみます。

(2020.5) C++17 std::optional で作りなおしました。ついでに, Either っぽいのも作ってみました。

HaskellのMaybeモナド

HaskellのMaybe モナド (データ型) は, 正常値か失敗のどちらかを持つ型です。正常値は Just , 失敗は Nothing で表します。

>>= 演算子で Maybe モナドを返す関数を繋いでいき、途中で失敗したら以降の関数は実行させないようになっています。>>= 演算子は, bind と呼ばれます。

モナド (型クラス) とは何か、満たすべき制約などについては、別ページにて; Haskell: モナド, 具象データ型, モナド則の嬉しさ

典型的な使い方は、実行時 (コンパイル時ではなく) の値の探索です。

組み込みの lookup 関数を使ってみます。lookup 関数は、引数を2つ取り, 第1引数が探す key, 第2引数が探す対象のリストです。見つかった場合はタプルの2番目を返し, 見つからなかったときは Nothing を返します。こういう定義です;

Haskell
[RAW]
  1. lookup :: (Eq a) => a -> [(a,b)] -> Maybe b
  2. lookup key [] = Nothing
  3. lookup key ((x,y):xys)
  4. | key == x = Just y
  5. | otherwise = lookup key xys

例えば、次の例で tmpdir 関数を定義していますが、まず "path" で探し、見つかったらさらに "tmp" を探す、というのを愚直に実装しています。

Haskell
[RAW]
  1. config :: [(String, [(String, String)])]
  2. config = [
  3. ("path", [("tmp", "/tmp"), ("data", "/foo")]),
  4. ("user", [("u1", "hoge")])
  5. ]
  6. tmpdir :: Maybe String
  7. tmpdir = case (lookup "path" config) of
  8. Just entries -> lookup "tmp" entries
  9. Nothing -> Nothing
  10. main = print $ tmpdir

この関数 tmpdir を次のように書けます。

Haskell
[RAW]
  1. tmpdir = lookup "path" config >>= lookup "tmp"

Maybe は次のように実装されています。>>= の左辺の値が Nothing のときは, 右側の関数を評価 (実行) せず、Nothing を返すようになっています。

Haskell
[RAW]
  1. instance Monad Maybe where
  2. (Just x) >>= k = k x
  3. Nothing >>= _ = Nothing
  4. (>>) = (*>)
  5. return = Just
  6. fail _ = Nothing -- GHC では MonadFail のインスタンス

C++の optional

(2020.5) C++17 で std::optional が導入されました。今後はこちらを使えます。

boostoptionalクラスはHaskellのMaybeモナドのような機能を提供します。

boostがインストールされていれば、<boost/optional.hpp>ヘッダを取り込むだけで使えます。

値が有効なときは boost::optional<値の型>() で値を持つオブジェクトを生成し、無効の場合は boost::optional<値の型>() で値を持たない optionalオブジェクトを生成します。

optional には operator bool が定義されており、if文などで値が有効かどうか判定できます。

また、operator *と-> が定義されており、これらの演算子で値を取り出せます。

C++
[RAW]
  1. #include <cmath>
  2. #include <iostream>
  3. #include <boost/optional.hpp>
  4. using namespace std;
  5. // 平方根
  6. boost::optional<double> my_sqrt(double x) {
  7. if (x < 0)
  8. return boost::optional<double>(); // 不正
  9. else
  10. return boost::optional<double>(sqrt(x));
  11. }
  12. int main() {
  13. boost::optional<double> y = my_sqrt(150.0);
  14. if (y)
  15. cout << *y << endl;
  16. else
  17. cout << "error.\n";
  18. return 0;
  19. }

optional に渡す型は、デフォルトコンストラクタはなくても構いませんが、(暗黙にでも) コピーコンストラクタを持たなければなりません。

C++でMaybe モナドっぽく

C++17 前提。

Haskell >>= (bind) 演算子とlookup を書いてみます。

C/C++ の >>= 演算子は右結合なので、そのままではまずい。>= にします。この >= は汎用的なモナドではありません。std::optional<> 固定です。

lookup() は, std::map あるいは tr1::unordered_mapを受け付けるようにしてみました。キーも自由に選べます。戻り値はoptional 型です。

Haskellでは2引数の関数に一つだけ実引数を与えると1引数の関数が得られます (関数の部分適用)。C++では bind() を使います。

C++ は型推論が弱いので, 型をいたるところで書かないといけなくて大変です。

C++
[RAW]
  1. #include <optional> // C++17
  2. /**
  3. * コンテナからキーに対応する値を探す. 2引数の関数版.
  4. * Container は、key_type と mapped_type 型, find() 関数を持つこと.
  5. */
  6. template <typename Container>
  7. std::optional<typename Container::mapped_type>
  8. lookup(const typename Container::key_type& key, const Container& kv) {
  9. typename Container::const_iterator p = kv.find(key);
  10. if (p != kv.end())
  11. return std::optional<typename Container::mapped_type>(p->second);
  12. else
  13. return std::optional<typename Container::mapped_type>();
  14. }
  15. /**
  16. * 左辺が成功したときだけ右辺を評価.
  17. * 右辺の関数はContainerオブジェクトを引数に取る.
  18. *
  19. * std::result_of は C++17で非推奨。代わりに std::invoke_result を使う.
  20. * 左結合で弱いもの.
  21. */
  22. template <typename Val1, typename Fn>
  23. typename std::invoke_result<Fn, Val1>::type
  24. operator >= (const std::optional<Val1>& maybe_x, const Fn& fn) {
  25. if (maybe_x)
  26. return fn(*maybe_x);
  27. else {
  28. // 戻り値の型を optional<> で固定する。
  29. return std::optional<
  30. typename std::invoke_result<Fn, Val1>::type::value_type >();
  31. }
  32. }

使ってみます。

C++
[RAW]
  1. #include "c++maybe2.h"
  2. #include <map>
  3. #include <unordered_map> // C++11
  4. #include <cstdio>
  5. #include <string>
  6. #include <functional>
  7. using namespace std;
  8. void put_optional(const optional<string>& x) {
  9. if (x)
  10. printf("value = \"%s\"\n", x->c_str());
  11. else
  12. printf("value = Nothing\n");
  13. }
  14. typedef map<int, string> L2;
  15. typedef unordered_map<int, L2> L1;
  16. int main() {
  17. L1 db;
  18. L2 v1;
  19. v1.insert(pair<int, string>(10, "foo"));
  20. v1.insert(pair<int, string>(20, "bar"));
  21. db.insert(pair<int, L2>(1, v1));
  22. L2 v2;
  23. v2.insert(pair<int, string>(2, "hoge"));
  24. v2.insert(pair<int, string>(3, "hogehoge"));
  25. db.insert(pair<int, L2>(50, v2));
  26. // 以下のコードを何とかしたい
  27. optional<L2> x = lookup<L1>(1, db);
  28. if (x) {
  29. optional<string> y = lookup<L2>(10, *x);
  30. put_optional(y); // => "foo"
  31. }
  32. else
  33. printf("v = Nothing\n");
  34. // こんな感じ?
  35. optional<string> y = lookup<L1>(50, db) >= bind(lookup<L2>, 2, placeholders::_1);
  36. put_optional(y); // => "hoge"
  37. // 左辺がNothingのとき
  38. optional<string> z = lookup<L1>(11, db) >= bind(lookup<L2>, 2, placeholders::_1);
  39. put_optional(z); // => Nothing
  40. return 0;
  41. }

ついで: Either モナドっぽく

C++17 std::variant がまんま, Haskell Either です。

C++
[RAW]
  1. #include <variant> // C++17
  2. #include <string>
  3. #include <iostream>
  4. using namespace std;
  5. // 左辺が成功 (Right) のときだけ右辺を評価.
  6. // 左結合.
  7. template <typename LeftT, typename RightT, typename Fn>
  8. typename invoke_result<Fn, RightT>::type
  9. operator >= (const variant<LeftT, RightT>& either_x, const Fn& fn) {
  10. if (either_x.index() == 1)
  11. return fn( get<1>(either_x) );
  12. else {
  13. // エラー時.
  14. // Right の型は変わってもよいので, variant<> を作り直す.
  15. using ResultVariant = typename invoke_result<Fn, RightT>::type;
  16. variant<LeftT, variant_alternative_t<1, ResultVariant> > ret;
  17. return ret.template emplace<0>( get<0>(either_x) ); // 'template' が必要.
  18. }
  19. }
  20. struct Hoge { int xx; };
  21. // 値を取って variant<> を返す.
  22. variant<string, Hoge> func(int x) {
  23. variant<string, Hoge> ret;
  24. Hoge z = {.xx = x * 200};
  25. return ret.emplace<1>(z);
  26. }
  27. int main() {
  28. // Left (Error), Right
  29. variant<string, int> v;
  30. v.emplace<1>(25);
  31. v >= &func >= [](const Hoge& r) {
  32. cout << r.xx << "\n"; //=> 5000
  33. return variant<string, float>();
  34. };
  35. return 0;
  36. }

サイト内関連ページ

はすけるで遊ぶ
Haskellのメモがいくつか。

外部リンク

C++でMaybeモナドを返すlookup関数を作ってみた - Faith and Brave - C++で遊ぼう
こちらのほうが高度です。
C++ for Haskeller
ネタ。