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で次のように定義されます。
- data Maybe a = Nothing | Just a deriving (Eq, Ord, Read, Show)
- instance Monad Maybe where
- (Just x) >>= k = k x
- Nothing >>= k = Nothing
- return = Just
- fail s = Nothing
>>= の左辺の値がNothingのときは右側の関数が評価(実行)されません。
典型的な使い方は、実行時(コンパイル時ではなく)の値の探索です。lookup関数を使います。
- lookup :: (Eq a) => a -> [(a,b)] -> Maybe b
- lookup key [] = Nothing
- lookup key ((x,y):xys)
- | key == x = Just y
- | otherwise = lookup key xys
例えば、次の例で、
- config :: [(String, [(String, String)])]
- config = [
- ("path", [("tmp", "/tmp"), ("data", "/foo")]),
- ("user", [("u1", "hoge")])
- ]
- tmpdir :: Maybe String
- tmpdir = case (lookup "path" config) of
- Just entries -> lookup "tmp" entries
- Nothing -> Nothing
- main = print $ tmpdir
関数tmpdirを次のように書けます。
- 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 *と-> が定義されており、これらの演算子で値を取り出せます。
- #include <cmath>
- #include <iostream>
- #include <boost/optional.hpp>
- using namespace std;
- // 平方根
- boost::optional<double> my_sqrt(double x) {
- if (x < 0)
- return boost::optional<double>(); // 不正
- else
- return boost::optional<double>(sqrt(x));
- }
- int main() {
- boost::optional<double> y = my_sqrt(150.0);
- if (y)
- cout << *y << endl;
- else
- cout << "error.\n";
- return 0;
- }
optionalに渡す型は、デフォルトコンストラクタはなくても構いませんが、(暗黙にでも)コピーコンストラクタを持たなければなりません。
C++でMaybeモナド
>>=演算子とlookupを書いてみます。
Haskellでは2引数の関数に一つだけ実引数を与えると1引数の関数が得られます(関数の部分適用)が、C++ではそういうわけにはいきません。
MkLookupクラスを作って、>>=が渡すはずの一つの引数以外はコンストラクタで与えるようにします。
で、1回目。lookup のキーは整数限定です。
- #include <vector>
- #include <string>
- #include <utility>
- #include <cstdio>
- #include <boost/optional.hpp>
- using namespace std;
- template <typename ContainerType, typename ValueType>
- boost::optional<ValueType> lookup(const ContainerType& kv, int k) {
- typename ContainerType::const_iterator i;
- for (i = kv.begin(); i != kv.end(); i++) {
- if (i->first == k)
- return boost::optional<ValueType>(i->second);
- }
- return boost::optional<ValueType>();
- }
- template <typename ContainerType, typename ValueType>
- struct MkLookup: public unary_function<ValueType, boost::optional<ValueType> > {
- int const key;
- MkLookup(int k): key(k) {}
- boost::optional<ValueType> operator () (const ContainerType& c) const {
- return lookup<ContainerType, ValueType>(c, key);
- }
- };
- template <typename ContainerType, typename ValueType>
- boost::optional<ValueType>
- operator >>= (const boost::optional<ContainerType>& maybe_x,
- const MkLookup<ContainerType, ValueType>& f) {
- if (maybe_x)
- return f(*maybe_x);
- else
- return boost::optional<ValueType>();
- }
- typedef pair<int, string> KV;
- typedef pair<int, vector<KV> > Db;
- int main() {
- vector<Db> db;
- vector<KV> v1;
- v1.push_back(KV(10, "foo"));
- v1.push_back(KV(20, "bar"));
- db.push_back(Db(1, v1));
- v1.clear();
- v1.push_back(KV(2, "hoge"));
- v1.push_back(KV(3, "hogehoge"));
- db.push_back(Db(50, v1));
- // 以下のコードを何とかしたい
- boost::optional<vector<KV> > x = lookup<vector<Db>, vector<KV> >(db, 1);
- if (x) {
- boost::optional<string> y = lookup<vector<KV>, string>(*x, 10);
- if (y)
- printf("v = %s\n", y->c_str());
- else
- printf("v = Nothing\n");
- }
- else
- printf("v = Nothing\n");
- // こんな感じ?
- boost::optional<string> y = (lookup<vector<Db>, vector<KV> >(db, 1)
- >>= MkLookup<vector<KV>, string>(10));
- if (y)
- printf("v = %s\n", y->c_str());
- else
- printf("Nothing\n");
- return 0;
- }
pairとvectorを使いましたが、もっとC++っぽくと思うと、lookup はmapかmultimapを使う方がいいような気がします。
あと、型推論がないと、型をいたるところで書かないといけなくて大変です。
std::mapを使う
(2009.1.15) この節を追加。
2回目は、std::map あるいは tr1::unordered_mapを受け付けるようにしてみました。キーも自由に選べます。>>= の右辺がMkLookupに依存していたのも止めました。
- #include <string>
- #include <cstdio>
- #include <map>
- #include <tr1/unordered_map>
- #include <boost/optional.hpp>
- using namespace std;
- using namespace tr1;
- using namespace boost;
- /** コンテナからキーに対応する値を探す. 関数版.
- Container は、key_type と mapped_type 型, find() 関数を持つこと. */
- template <typename Container>
- optional<typename Container::mapped_type>
- lookup(const Container& kv, const typename Container::key_type& key) {
- typename Container::const_iterator p = kv.find(key);
- if (p != kv.end())
- return optional<typename Container::mapped_type>(p->second);
- else
- return optional<typename Container::mapped_type>();
- }
- /** 1引数の関数のようにする. */
- template <typename Container>
- struct MkLookup {
- typename Container::key_type const key;
- MkLookup(const typename Container::key_type& k): key(k) {}
- optional<typename Container::mapped_type>
- operator () (const Container& c) const {
- return lookup<Container>(c, key);
- }
- };
- /** 左辺が成功したときだけ右辺を評価.
- 右辺の関数はContainerオブジェクトを引数に取る. */
- template <typename Container, typename F>
- optional<typename Container::mapped_type>
- operator >>= (const optional<Container>& maybe_x, const F& f) {
- if (maybe_x)
- return f(*maybe_x);
- else
- return optional<typename Container::mapped_type>();
- }
- void put_optional(const optional<string>& x) {
- if (x)
- printf("value = \"%s\"\n", x->c_str());
- else
- printf("value = Nothing\n");
- }
- typedef map<int, string> L2;
- typedef tr1::unordered_map<int, L2> L1;
- int main() {
- L1 db;
- L2 v1;
- v1.insert(pair<int, string>(10, "foo"));
- v1.insert(pair<int, string>(20, "bar"));
- db.insert(pair<int, L2>(1, v1));
- v1.clear();
- v1.insert(pair<int, string>(2, "hoge"));
- v1.insert(pair<int, string>(3, "hogehoge"));
- db.insert(pair<int, L2>(50, v1));
- // 以下のコードを何とかしたい
- optional<L2> x = lookup<L1>(db, 1);
- if (x) {
- optional<string> y = lookup<L2>(*x, 10);
- put_optional(y);
- }
- else
- printf("v = Nothing\n");
- // こんな感じ?
- optional<string> y = lookup<L1>(db, 50) >>= MkLookup<L2>(2);
- put_optional(y);
- // 左辺がNothingのとき
- optional<string> z = lookup<L1>(db, 11) >>= MkLookup<L2>(2);
- put_optional(z);
- return 0;
- }
>>= の使い方がずいぶんすっきりしました。
サイト内関連ページ
- はすけるで遊ぶ
- Haskellのメモがいくつか。