Haskell style “Maybe” type & *chaining* in C++11

前端 未结 5 1010
隐瞒了意图╮
隐瞒了意图╮ 2020-12-23 20:43

I repeatedly find myself requiring Haskell style Maybe (especially Maybe chaining) in my project at work. E.g. withdrawal request from customer and we are give

相关标签:
5条回答
  • 2020-12-23 21:26

    It's been implemented in C++03 for a long time. You can find it in Boost as boost::optional. boost::optional offers a simple if (value) interface.

    0 讨论(0)
  • 2020-12-23 21:36

    As a recovering template-aholic, I feel it my duty to point out the simple non-template exception-based solution for the given example.

    Adjust the code to throw an exception instead of returning Maybe/Optional, and the code becomes...

    try
    {
      print(getBalance(getCheckingAccount(getCustomer(customers, "12346"))));
    }
    catch(my_error_t) 
    {}
    

    That's not to say Maybe/Optional monads are never useful in C++, but for many cases exceptions will get things done in a much more idiomatic and easily understood manner.

    0 讨论(0)
  • 2020-12-23 21:41

    Good start, but I think you're over-engineering in your zeal to make your class foolproof. Personally I'd recommend 'worse is better'. First, let's reuse Boost.Optional:

    struct nothing_type {
        template<typename T>
        operator boost::optional<T>() const
        { return {}; }
    };
    constexpr nothing_type nothing;
    
    template<typename T>
    boost::optional<T>
    just(T&& t)
    {
        return std::forward<T>(t);
    }
    
    template<typename Option, typename Functor>
    auto maybe_do(Option&& option, Functor&& functor)
    -> boost::optional<
        decltype( functor(*std::forward<Option>(option)) )
    >
    {
        // Forwarding 
        if(option)
            return functor(*std::forward<Option>(option));
        else
            return nothing;
    }
    

    Some various explanations on things that aren't really important:

    • nothing doesn't have to be an object, it can still be a function (returning nothing_type) like you're doing. That's not important.

    • I made sure to preserve the reference semantics of just to match your version. As a bonus though, it can still deal with values. As such, with int i = 0; auto maybe = just(i); then the type of maybe will be boost::optional<int&>, whereas with auto maybe = just(42); it is boost::optional<int>.

    • the *std::forward<Option>(option) can actually simply be *option as Boost.Optional is not move-aware and not many compilers support lvalue/rvalue *this (which would be needed for it to matter). I just like future-proofing perfect-forwarding templates.

    • you can still name maybe_do operator| instead. I would however recommend putting it in a namespace and use using ns::operator| (or using namespace ns;) to put it into scope. You can additionally (or instead) add an SFINAE check (or write several overloads) to make sure it only participates in overload resolution at appropriate times. I'm advising this to avoid namespace pollution and annoying errors.

    The important stuff:

    It may look like maybe_do is severely underpowered compared to your overloads that can deal with member pointers. But I'd recommend keeping it simple and instead putting the burden on client-code to adapt member pointers:

    auto maybe = /* fetch an optional<T cv ref> from somewhere */
    maybe_do(maybe, std::bind(&T::some_member, _1));
    

    Similarly client code can use std::bind to do the poor man's partial evaluation:

    maybe_do(maybe, std::bind(some_functor, _1, "foo", _2, bar));
    
    0 讨论(0)
  • 2020-12-23 21:47

    I was the OP (lost my account when SO migrated). Here is the latest I came up with using std::invoke. Life becomes much simpler

    template < typename T >
    auto operator | (Maybe < T > const & v, auto && f)
    {
        using U = std::decay_t < decltype(f(v.get())) >;
        if (v.isNull())
            return Maybe < U >::nothing();
        else
            return Maybe < U >::just(std::invoke(f, v.get()));
    }
    
    template < typename T >
    auto operator | (Maybe < T > & v, auto && f)
    {
        using U = std::decay_t < decltype(f(v.get())) >;
        if (v.isNull())
            return Maybe < U >::nothing();
        else
            return Maybe < U >::just(std::invoke(f, v.get()));
    }
    
    template < typename T >
    auto operator | (Maybe < T > && v, auto && f)
    {
        using U = std::decay_t < decltype(f(v.get())) >;
        if (v.isNull())
            return Maybe < U >::nothing();
        else
            return Maybe < U >::just(std::invoke(f, v.get()));
    }
    
    0 讨论(0)
  • 2020-12-23 21:50

    My 5 cts.

    Sample usage:

    Maybe<string> m1 ("longlonglong");
    
    auto res1 = m1 | lengthy  | length;
    

    lengthy and length are "monadic lambdas", i.e.

    auto length = [] (const string & s) -> Maybe<int>{ return Maybe<int> (s.length()); };
    

    Complete code:

    // g++ -std=c++1y answer.cpp
    
    #include <iostream>
    using namespace std;
    
    // ..................................................
    // begin LIBRARY
    // ..................................................
    template<typename T>
    class Maybe {
      // 
      //  note: move semantics
      //  (boxed value is never duplicated)
      // 
    
    private:
    
      bool is_nothing = false;
    
    public:
      T value;
    
      using boxed_type = T;
    
      bool isNothing() const { return is_nothing; }
    
      explicit Maybe () : is_nothing(true) { } // create nothing
    
      // 
      //  naked values
      // 
      explicit Maybe (T && a) : value(std::move(a)), is_nothing(false) { }
    
      explicit Maybe (T & a) : value(std::move(a)), is_nothing(false) { }
    
      // 
      //  boxed values
      // 
      Maybe (Maybe & b) : value(std::move(b.value)), is_nothing(b.is_nothing) { b.is_nothing = true; }
    
      Maybe (Maybe && b) : value(std::move(b.value)), is_nothing(b.is_nothing) { b.is_nothing = true; }
    
      Maybe & operator = (Maybe & b) {
        value = std::move(b.value);
        (*this).is_nothing = b.is_nothing;
        b.is_nothing = true;
        return (*this);
      }
    }; // class
    
    // ..................................................
    template<typename IT, typename F>
    auto operator | (Maybe<IT> mi, F f)  // chaining (better with | to avoid parentheses)
    {
      // deduce the type of the monad being returned ...
      IT aux;
      using OutMonadType = decltype( f(aux) );
      using OT = typename OutMonadType::boxed_type;
    
      // just to declare a nothing to return
      Maybe<OT> nothing;
    
      if (mi.isNothing()) {
        return nothing;
      }
    
      return f ( mi.value );
    } // ()
    
    // ..................................................
    template<typename MO>
    void showMonad (MO m) {
      if ( m.isNothing() ) {
        cout << " nothing " << endl;
      } else {
        cout << " something : ";
        cout << m.value << endl;
      }
    }
    
    // ..................................................
    // end LIBRARY
    // ..................................................
    
    // ..................................................
    int main () {
    
      auto lengthy = [] (const string & s) -> Maybe<string> { 
        string copyS = s;
        if  (s.length()>8) {
          return Maybe<string> (copyS);
        }
        return Maybe<string> (); // nothing
      };
    
      auto length = [] (const string & s) -> Maybe<int>{ return Maybe<int> (s.length()); };
    
      Maybe<string> m1 ("longlonglong");
      Maybe<string> m2 ("short");
    
      auto res1 = m1 | lengthy  | length;
    
      auto res2 = m2 | lengthy  | length;
    
      showMonad (res1);
      showMonad (res2);
    
    
    } // ()
    
    0 讨论(0)
提交回复
热议问题