C++でMaybeモナド (boost::optional)

(2009.1.3)

C++ で Haskell の Maybeモナドっぽいものを書いてみます。

元ネタはこちら。

元ネタはHaskellっぽくJustとNothingを定義していますが、このページでは、意味が近いboost::optionalを活用してみます。

HaskellのMaybeモナド

HaskellのMaybeモナドは失敗するかもしれない値です。また、>>= 演算子でMaybeモナドを返す関数を繋いでいき、途中で失敗したら以降の関数は実行させないようにできます。

  • All About Monads モナドのすべて Haskell におけるモナドプログラミングの理論と実践に関する包括的ガイド

MaybeモナドはPreludeで次のように定義されます。

Haskell
[POPUP]
  1. data Maybe a = Nothing | Just a deriving (Eq, Ord, Read, Show)
  2. instance Monad Maybe where
  3. (Just x) >>= k = k x
  4. Nothing >>= k = Nothing
  5. return = Just
  6. fail s = Nothing

>>= の左辺の値がNothingのときは右側の関数が評価(実行)されません。

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

Haskell
[POPUP]
  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

例えば、次の例で、

Haskell
[POPUP]
  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
[POPUP]
  1. tmpdir = lookup "path" config >>= lookup "tmp"

C++のboost::optional

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

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

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

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

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

C
[POPUP]
  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モナド

>>=演算子とlookupを書いてみます。

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

MkLookupクラスを作って、>>=が渡すはずの一つの引数以外はコンストラクタで与えるようにします。

で、1回目。lookup のキーは整数限定です。

C
[POPUP]
  1. #include <vector>
  2. #include <string>
  3. #include <utility>
  4. #include <cstdio>
  5. #include <boost/optional.hpp>
  6. using namespace std;
  7. template <typename ContainerType, typename ValueType>
  8. boost::optional<ValueType> lookup(const ContainerType& kv, int k) {
  9. typename ContainerType::const_iterator i;
  10. for (i = kv.begin(); i != kv.end(); i++) {
  11. if (i->first == k)
  12. return boost::optional<ValueType>(i->second);
  13. }
  14. return boost::optional<ValueType>();
  15. }
  16. template <typename ContainerType, typename ValueType>
  17. struct MkLookup: public unary_function<ValueType, boost::optional<ValueType> > {
  18. int const key;
  19. MkLookup(int k): key(k) {}
  20. boost::optional<ValueType> operator () (const ContainerType& c) const {
  21. return lookup<ContainerType, ValueType>(c, key);
  22. }
  23. };
  24. template <typename ContainerType, typename ValueType>
  25. boost::optional<ValueType>
  26. operator >>= (const boost::optional<ContainerType>& maybe_x,
  27. const MkLookup<ContainerType, ValueType>& f) {
  28. if (maybe_x)
  29. return f(*maybe_x);
  30. else
  31. return boost::optional<ValueType>();
  32. }
  33. typedef pair<int, string> KV;
  34. typedef pair<int, vector<KV> > Db;
  35. int main() {
  36. vector<Db> db;
  37. vector<KV> v1;
  38. v1.push_back(KV(10, "foo"));
  39. v1.push_back(KV(20, "bar"));
  40. db.push_back(Db(1, v1));
  41. v1.clear();
  42. v1.push_back(KV(2, "hoge"));
  43. v1.push_back(KV(3, "hogehoge"));
  44. db.push_back(Db(50, v1));
  45. // 以下のコードを何とかしたい
  46. boost::optional<vector<KV> > x = lookup<vector<Db>, vector<KV> >(db, 1);
  47. if (x) {
  48. boost::optional<string> y = lookup<vector<KV>, string>(*x, 10);
  49. if (y)
  50. printf("v = %s\n", y->c_str());
  51. else
  52. printf("v = Nothing\n");
  53. }
  54. else
  55. printf("v = Nothing\n");
  56. // こんな感じ?
  57. boost::optional<string> y = (lookup<vector<Db>, vector<KV> >(db, 1)
  58. >>= MkLookup<vector<KV>, string>(10));
  59. if (y)
  60. printf("v = %s\n", y->c_str());
  61. else
  62. printf("Nothing\n");
  63. return 0;
  64. }

pairとvectorを使いましたが、もっとC++っぽくと思うと、lookup はmapかmultimapを使う方がいいような気がします。

あと、型推論がないと、型をいたるところで書かないといけなくて大変です。

std::mapを使う

(2009.1.15) この節を追加。

2回目は、std::map あるいは tr1::unordered_mapを受け付けるようにしてみました。キーも自由に選べます。>>= の右辺がMkLookupに依存していたのも止めました。

C
[POPUP]
  1. #include <string>
  2. #include <cstdio>
  3. #include <map>
  4. #include <tr1/unordered_map>
  5. #include <boost/optional.hpp>
  6. using namespace std;
  7. using namespace tr1;
  8. using namespace boost;
  9. /** コンテナからキーに対応する値を探す. 関数版.
  10. Container は、key_type と mapped_type 型, find() 関数を持つこと. */
  11. template <typename Container>
  12. optional<typename Container::mapped_type>
  13. lookup(const Container& kv, const typename Container::key_type& key) {
  14. typename Container::const_iterator p = kv.find(key);
  15. if (p != kv.end())
  16. return optional<typename Container::mapped_type>(p->second);
  17. else
  18. return optional<typename Container::mapped_type>();
  19. }
  20. /** 1引数の関数のようにする. */
  21. template <typename Container>
  22. struct MkLookup {
  23. typename Container::key_type const key;
  24. MkLookup(const typename Container::key_type& k): key(k) {}
  25. optional<typename Container::mapped_type>
  26. operator () (const Container& c) const {
  27. return lookup<Container>(c, key);
  28. }
  29. };
  30. /** 左辺が成功したときだけ右辺を評価.
  31. 右辺の関数はContainerオブジェクトを引数に取る. */
  32. template <typename Container, typename F>
  33. optional<typename Container::mapped_type>
  34. operator >>= (const optional<Container>& maybe_x, const F& f) {
  35. if (maybe_x)
  36. return f(*maybe_x);
  37. else
  38. return optional<typename Container::mapped_type>();
  39. }
  40. void put_optional(const optional<string>& x) {
  41. if (x)
  42. printf("value = \"%s\"\n", x->c_str());
  43. else
  44. printf("value = Nothing\n");
  45. }
  46. typedef map<int, string> L2;
  47. typedef tr1::unordered_map<int, L2> L1;
  48. int main() {
  49. L1 db;
  50. L2 v1;
  51. v1.insert(pair<int, string>(10, "foo"));
  52. v1.insert(pair<int, string>(20, "bar"));
  53. db.insert(pair<int, L2>(1, v1));
  54. v1.clear();
  55. v1.insert(pair<int, string>(2, "hoge"));
  56. v1.insert(pair<int, string>(3, "hogehoge"));
  57. db.insert(pair<int, L2>(50, v1));
  58. // 以下のコードを何とかしたい
  59. optional<L2> x = lookup<L1>(db, 1);
  60. if (x) {
  61. optional<string> y = lookup<L2>(*x, 10);
  62. put_optional(y);
  63. }
  64. else
  65. printf("v = Nothing\n");
  66. // こんな感じ?
  67. optional<string> y = lookup<L1>(db, 50) >>= MkLookup<L2>(2);
  68. put_optional(y);
  69. // 左辺がNothingのとき
  70. optional<string> z = lookup<L1>(db, 11) >>= MkLookup<L2>(2);
  71. put_optional(z);
  72. return 0;
  73. }

>>= の使い方がずいぶんすっきりしました。

サイト内関連ページ

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

外部リンク

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