UP | HOME

FP monads Functional programming

Table of Contents

Overview FP monads

My fascination of Functional programming draw me to read a book Functional Programming in C++ by Ivan Čukić. To be honest, I did some functional programming when I was at Uni, at that time we learned some Haskell which was interesting, but when you get out of uni the world kind of catches up with you, and to be honest I haven't seen one job posting where they need someone with Haskell knowledge. Instead I got caught up in the main stream languages such as C++, python and so on and most of these programming languages are OO by design. But in the later years I've seen a trend where the languages tend to evolve in direction towards more functional programming. And the more I look at it the more I find it intriguing. Though I must admit, I actually do some functional programming on a very small scale since I use Emacs (elisp). Though I would not call my self good at it, I just create small functions to help me out when I do repetitive work on some text or code.

One thing that I remember during my Haskell years was the concept of monads. I do remember it as something that I could not grasp, I used it in some extent, but every time my lecturer try to explain it he got into a formal description which just was to overwhelming. I just wanted to know how it worked , not the why.

This book as mentioned above, gave me just the right tools to actually understand what monads are and why they are so good. This is my take from the book, some of it I clearly stole straight of, and some I've been experimenting with to get a better understanding.

One can say that these are my notes from the book.

Lazy evaluation

Lazy evluation is a way to only run an expensive computation once its needed, and then store the result. For example lets imagine that you have some function that takes a while to be evaluated every time its called.

int main(int argc, char *argv[])
{
    if( <exp> )
        compute(....);
    .
    .
    .
    if( <exp> )
            compute(....);
    return 0;
}

The obvious way is to cache the result for later retrieval (once the function is called again), but many times the compute function might not be called at all. Then you spent alot of unecessary cpu-resources computing. Wouldn't it be better to just run the compute function once when needed, and then save the cache for later retrieval?

Unfortunatly c++ doesn't support lazy evaluation by default. Instead we need to construct some kind of object to help us out. From the outside this class should look as if it was unmutable, that is all its member function should be const

 1: #include <mutex>
 2: 
 3: template<typename F>
 4: class lazy_eval
 5: {
 6:   private:
 7:     F computational_fn_;
 8:     mutable bool cache_initialized_{false};
 9:     mutable decltype(computational_fn_()) cache_;
10:     mutable std::once_flag is_called;
11:   public:
12:     lazy_eval(F&& fn):computational_fn_(fn){}
13: 
14: 
15: 
16:     operator const decltype(computational_fn_())& () const
17:     {
18:         std::call_once(is_called, [this] {
19:             std::cout << "Run Once " << "\n";
20:             cache_ = computational_fn_();
21:         });
22:         return cache_;
23:     }
24: 
25: 
26: 
27:     const decltype(computational_fn_())& operator()() const
28:     {
29:         return *this;
30:     }
31: 
32: 
33: };
34: 
35: template<typename T>
36: std::ostream& operator<<(std::ostream& out, const lazy_eval<T>& lazy)
37: {
38:     return out << lazy();
39: }
40: 
41: 
42: 
43: 
44: template<typename Fn>
45: inline lazy_eval<Fn> make_lazy_val(Fn&& fn)
46: {
47:     return lazy_eval<Fn>(std::forward<Fn>(fn));
48: 
49: }
50: 
51: 
52: void print(const std::string& str)
53: {
54:     std::cout << str << "\n";
55: 
56: }
57: 
58: int main(int argc, char *argv[])
59: {
60:     std::string a{"Hej hopp"};
61:     auto compute = [a]()
62:     {
63: 
64:         return a+" Concatenated";
65:     };
66: 
67:     auto lazy_compute = make_lazy_val(compute);
68:     print(lazy_compute);
69:     std::cout << lazy_compute() << "\n";
70:     std::cout << lazy_compute() << "\n";
71:     std::cout << lazy_compute << "\n";
72: 
73:     return 0;
74: }
Run Once  
Hej hopp Concatenated
Hej hopp Concatenated
Hej hopp Concatenated
Hej hopp Concatenated

This works as expected. Though maybe we need some arguments to our function? Stackoverflow

The take from this is not completely ground breaking, but one take away from this is the conversion operator. So how does that work?

Converstion operator

A conversion operator (see line 27) allows the compiler to implicitly convert the user defined type to some other type.

Lets do an example

 1: #include <fmt/format.h>
 2: #include <cstdint>
 3: #include <array>
 4: 
 5: struct Header
 6: {
 7:     using bin_type = std::array<uint8_t,6>;
 8:     uint16_t id{};
 9:     uint16_t command{};
10:     uint16_t len{};
11: 
12:     operator bin_type () const
13:     {
14:         // This should be converted from the id,command and length
15:         // to the bytes and create an array,...
16:         return {0xaa,0xbb,0xcc,0xdd,0xee,0xff};
17:     }
18: };
19: 
20: int main(int argc, char *argv[])
21: {
22:     Header hdr{};
23: 
24:     Header::bin_type arr = hdr;
25: 
26:     for( const auto& item : arr )
27:     {
28:         fmt::print("|{:x} ",item);
29:     }
30: 
31: 
32:     return 0;
33: }
34: 
aa bb cc dd ee ff

This is quite useful! What it does is that it implicitly converts the \(item \rightarrow bin\_type\) So in the case above ( see line 24 ) we implcitly converted the hdr to an array.

Functors

A class template F is a functor, if it has a transform function defined on it. The transform function takes an instance f of type F<T1> and a function \(t: T1 \rightarrow T2\) and returns a value of type F<T2>.

\begin{equation} f: T1 \rightarrow T2 \\ Functor: (F< T1 >,f) \rightarrow F< T2 > \end{equation}

Functor: (F<T1>,t) → F<T2>

Ok, that was a bit informal, so lets do an example:

 1: #include <fmt/format.h>
 2: #include <cstdint>
 3: 
 4: 
 5:   struct Nothing
 6: {
 7: 
 8: };
 9: 
10: template<typename T=Nothing>
11: struct F
12: {
13:     using type = T;
14:     T val_{};
15:     bool is_initialized{false};
16: };
17: 
18: 
19: template<typename T1, typename Fn>
20: auto transform(const F<T1>& t1, Fn f )
21: {
22:     if(t1.is_initialized)
23:     {
24:         auto t = f(t1.val_);
25:         return F<decltype(t)>{t,true};
26:     }
27:     else
28:       {
29:         return F<decltype(f(t1.val_))>{.val_ =0,.is_initialized = false};
30:       }
31: }
32: 
33: 
34: 
35: 
36: template<typename T, typename Fn>
37: auto operator|(const F<T>& f1,Fn f)
38: {
39:     return transform(f1,f);
40: }
41: 
42: int main()
43: {
44:     using T1=uint16_t;
45: 
46:     F<T1> F_T1{0xff'ff,true};
47: 
48:     auto f1 = [](uint16_t val) -> uint32_t
49:     {
50:         return (val << 16) | 0x00'00;
51:     };
52: 
53:     auto f2 = [](auto val) ->uint16_t
54:     {
55:         return static_cast<uint16_t>( (val & 0xffff'0000)  >> 16);
56:     };
57: 
58: 
59:     {
60:         auto F_T2 = transform(F_T1,f1);
61:         fmt::print("|Transformed 1 | {:x} | {:x}|\n",F_T1.val_, F_T2.val_);
62:         F_T1 = transform(F_T2, f2);
63:         fmt::print("| Transformed 2| {:x} | {:x} |\n", F_T2.val_,F_T1.val_);
64:     }
65: 
66: 
67:     {
68: 
69:         auto back = transform(
70:             transform(F_T1,f1),
71:             f2
72:             );
73:       fmt::print("|Transformed and back | {:x}|\n",back.val_);
74:     }
75: 
76:     {
77:         auto F_new = F_T1 | f1 | f2;
78:         fmt::print("|transformed pipe | {:x}|\n",F_new.val_);
79:     }
80: 
81:     {
82: 
83:         auto F2 = F<T1>{0xff'ff,false} | f1 | f2;
84:         fmt::print("|Unintialized | {} | {}|\n",F2.is_initialized, F2.val_);
85: 
86:     }
87: 
88:     return 0;
89: }
Transformed 1 ffff ffff0000
Transformed 2 ffff0000 ffff
Transformed and back ffff  
transformed pipe ffff  
Unintialized false 0

Though the syntax is abit weird. It would be better to use the | command, for that i used a operator (see line 37) which on the rhs passes a F<T1> and on the lhs a transform function f. Perhaps the operator should not include the transform function and instad

 1: #include <fmt/format.h>
 2: #include <cstdint>
 3: 
 4: struct Nothing{};
 5: 
 6: template<typename T=Nothing>
 7: struct F
 8: {
 9:     using type = T;
10:     T val_{};
11:     bool is_initialized{false};
12: };
13: 
14: template<typename Fn>
15: auto transform(Fn&& f)
16: {
17:     return [f](auto t1) -> auto
18:     {
19:         if(t1.is_initialized)
20:       {
21:           auto t = f(t1.val_);
22:           return F<decltype(t)>{t,true};
23:       }
24:       else
25:         {
26:             return F<decltype(f(t1.val_))>{.val_ =0,.is_initialized = false};
27:         }
28: 
29:     };
30: }
31: 
32: template<typename T, typename Fn>
33: auto operator|(const F<T>& f1,Fn fn)
34: {
35:     return fn(f1);
36: }
37: 
38: 
39:   int main(int argc, char *argv[])
40:   {
41:       using T1=uint16_t;
42: 
43:       F<T1> F_T1{0xff'ff,true};
44: 
45:       auto f1 = [](uint16_t val) -> uint32_t
46:       {
47:           return (val << 16) | 0x00'00;
48:       };
49: 
50:       auto f2 = [](auto val) ->uint16_t
51:       {
52:           return static_cast<uint16_t>( (val & 0xffff'0000)  >> 16);
53:       };
54: 
55:       auto F2 = F_T1 | transform(f1)
56:                      | transform(f2)
57:                      | transform([](auto val){ return val;});
58: 
59:       fmt::print("| F2 | {:x}|\n",F2.val_);
60: 
61: 
62:       return 0;
63:   }
F2 ffff

Monads

Now lets imagine that our transform function f1 returns a functor instead. This would lead that the transform function no longer can be used.

\begin{equation} f: T1 \rightarrow F< T2 > \\ \end{equation}

This would lead to that the transform function would return \(F>\) and the more transform you do the more nesting you get.

So a monad \(M\) is a functor that has more functionality. That functionality will remove one level of nesting

\begin{equation} join: M < M< T > > \rightarrow M< T > \end{equation}

Which mean you can do something like

  1: #include <fmt/format.h>
  2: #include <type_traits>
  3: using T1 = uint16_t;
  4: using T2 = uint32_t;
  5: 
  6: struct Nothing{};
  7: 
  8: template<typename T=Nothing>
  9: struct F
 10: {
 11:     using type = T;
 12:     T val_{};
 13:     bool is_initialized{false};
 14: };
 15: 
 16: template<typename Fn>
 17: auto transform(Fn&& f)
 18: {
 19:     return [f](auto t1) -> auto
 20:     {
 21:         if(t1.is_initialized)
 22:       {
 23:           auto t = f(t1.val_);
 24:           return F<decltype(t)>{t,true};
 25:       }
 26:       else
 27:         {
 28:             return F<decltype(f(t1.val_))>{.val_ =0,.is_initialized = false};
 29:         }
 30: 
 31:     };
 32: }
 33: 
 34: template<typename T, typename Fn>
 35: auto operator|(const F<T>& f1,Fn fn)
 36: {
 37:     return fn(f1);
 38: }
 39: 
 40: template<typename T>
 41: auto join( F<F<T>> F1_nest )
 42: {
 43:     if constexpr( std::is_same<T,T1>::value)
 44:     {
 45:         fmt::print("|Join<T1>|{}|\n",F1_nest.val_.val_);
 46:     }else
 47:     {
 48:         fmt::print("|Join<T2>|{}|\n",F1_nest.val_.val_);
 49:     }
 50:     return F1_nest.val_;
 51: }
 52: 
 53: 
 54: 
 55: 
 56: 
 57: 
 58: int main([[maybe_unused]]int argc, [[maybe_unused]]char *argv[])
 59: {
 60:     F<T1> F_T1{0xff'ff,true};
 61: 
 62:     // f: retuns F<m>
 63:     auto  f1= [](auto  m) ->F<T2>
 64:               {
 65:                   auto val = static_cast<T2>(m << 16);
 66:                   //F<T2> f2{val,true};
 67:                   fmt::print("| f1 | {} |\n",val);
 68:                   return F<T2>{val,true};
 69:                   //return F<>{val,true};
 70:               };
 71: 
 72:     auto f2 = [](auto m) -> F<T1>
 73:               {
 74:                   auto val = static_cast<T1>(m  >> 16 );
 75:                   fmt::print("| f2 | {}|\n",val);
 76:                   return F<T1>{val,true};
 77:               };
 78: 
 79:     {
 80: 
 81:         F<F<T2>> nest = transform(f1)(F_T1);
 82:         // Join removes one layer.
 83:         F<T2> back = join(nest);
 84: 
 85:         fmt::print("| F<T1>  | {} |\n | F<T2> | {}|\n|-\n|-\n",F_T1.val_,back.val_);
 86:     }
 87: 
 88:     {
 89:         auto f_f_t2 = F_T1 | transform(f1);
 90:         auto f_t2 = join(f_f_t2);
 91:         fmt::print("|F<T2>| {}|\n |-\n",f_t2.val_);
 92:     }
 93: 
 94:     {
 95:         // Returns  F<T1>   F<F<T2>>       F<T2>       F<F<T1>>       F<T1>
 96:         auto f_t1 = F_T1 | transform(f1) | join< T2 > | transform(f2) | join<T1>;
 97:         fmt::print("|F<T1>| {}|\n",f_t1.val_);
 98:         //fmt::print("|F<T2>| {}|\n",f_t2.val_);
 99:     }
100: 
101: 
102:     return 0;
103: }
104: 
105: 
f1 4294901760
Join<T2> 4294901760
F<T1> 65535
F<T2> 4294901760
f1 4294901760
Join<T2> 4294901760
F<T2> 4294901760
f1 4294901760
Join<T2> 4294901760
f2 65535
Join<T1> 65535
F<T1> 65535

So that was cool, but as we can see there is a pattern transform(f) | join keeps on re-occurring. So if we combine these two we get something like:

\begin{equation} construct: T \rightarrow M < T > \\ mbind: ( M< T1 >, T1 \rightarrow M < T2 >) \rightarrow M< T2 > \end{equation}

The construct is easy to understand , its just wrapping the type T to the monad .eg

template<typename T>
struct Monad
{
    Monad(T t): val_{t} {}
    T val_;
};

The mbind needs some explanation. mbind is a function that takes a monad M<T1> and a function (fn) for which the argument is a T1 , the output from that function (which is a transform function) is a new monad with a new wrapped type M<T2>.

So lets try it to make the mbind in c++.

template<typename T>
struct Monad
{
    Monad(T t): val_{t} {}
    T val_;
};

template<typename T, std::invocable<T> Fn >
auto mbind( Monad<T> m, Fn transform)
{
    // TODO: what are to be done here?
    return transform(m.val_);
}
 1: #include <cassert>
 2: #include <fmt/format.h>
 3: 
 4: template<typename T>
 5: struct Monad
 6: {
 7:     Monad(T t): val_{t} {}
 8:     T val_;
 9: };
10: 
11: template<typename T, std::invocable<T> Fn >
12: auto mbind( Monad<T> m, Fn transform)
13: {
14:     // TODO: what are to be done here?
15:     return transform(m.val_);
16: }
17: 
18: int main(int argc, char *argv[])
19: {
20:     using T1 = uint8_t;
21:     using T2 = bool;
22: 
23:     Monad<T1> monad_t{255};
24: 
25:     {
26:         auto transform = [](auto t)
27:         {
28:             return Monad<T2>{(t > 128) };
29:         };
30:         auto m_t2 = mbind(monad_t,transform);
31: 
32:         fmt::print("| M<T2> | {}|\n",m_t2.val_);
33:     }
34: 
35:     return 0;
36: }
37: 
M<T2> true

Monads on containers.

So lets try it out with a homemade container instead.

 1: struct Nothing
 2: {
 3: 
 4: };
 5: 
 6: 
 7: template<typename T, size_t SZ=10>
 8: struct Monad
 9: {
10: 
11:     constexpr static size_t sz = SZ;
12:     using Container_type = std::array<T, SZ>;
13:     using variant_type = std::variant<Container_type,Nothing>;
14:     using Monad_type = T;
15: 
16:     variant_type val;
17: };
18: 
19: 
20: template<typename T,typename... Ts, typename = std::enable_if_t<std::is_same_v<T,int>> >
21: constexpr auto construct(Ts... vals)
22: {
23:     typename Monad<T>::Container_type arr;
24:     std::size_t index{};
25:     ((arr[index++] = vals),...);
26:     Monad<T> monad{.val = arr };
27: 
28:     return monad;
29: }
30: 
31: 
32: //template<typename T, typename = std::enable_if_t<std::is_same_v<T,Nothing>> >
33: constexpr auto construct()
34: {
35:     Monad<Nothing> monad{.val = Nothing{}};
36: 
37:     return monad;
38: }
39: 

We also need somekind of constructor, \(constructor: T -> M< T >\)

template< typename T>
void do_something(const Monad<T>& monad )
{
    std::visit([](auto&& arg)
    {
        if constexpr (std::is_same_v<T,Nothing>)
                fmt::print("|Type |{}|\n","Nothing");
        if constexpr (std::is_same_v<T,int>)
            fmt::print("|Type| {} |\n","std::array<int>");

    },monad.val);
}

 1: #include <array>
 2: #include <fmt/format.h>
 3: #include <variant>
 4: #include <initializer_list>
 5: 
 6: struct Nothing
 7: {
 8: 
 9: };
10: 
11: 
12: template<typename T, size_t SZ=10>
13: struct Monad
14: {
15: 
16:     constexpr static size_t sz = SZ;
17:     using Container_type = std::array<T, SZ>;
18:     using variant_type = std::variant<Container_type,Nothing>;
19:     using Monad_type = T;
20: 
21:     variant_type val;
22: };
23: 
24: 
25: template<typename T,typename... Ts, typename = std::enable_if_t<std::is_same_v<T,int>> >
26: constexpr auto construct(Ts... vals)
27: {
28:     typename Monad<T>::Container_type arr;
29:     std::size_t index{};
30:     ((arr[index++] = vals),...);
31:     Monad<T> monad{.val = arr };
32: 
33:     return monad;
34: }
35: 
36: 
37: //template<typename T, typename = std::enable_if_t<std::is_same_v<T,Nothing>> >
38: constexpr auto construct()
39: {
40:     Monad<Nothing> monad{.val = Nothing{}};
41: 
42:     return monad;
43: }
44: 
45: 
46: template< typename T>
47: void do_something(const Monad<T>& monad )
48: {
49:     std::visit([](auto&& arg)
50:     {
51:         if constexpr (std::is_same_v<T,Nothing>)
52:                 fmt::print("|Type |{}|\n","Nothing");
53:         if constexpr (std::is_same_v<T,int>)
54:             fmt::print("|Type| {} |\n","std::array<int>");
55: 
56:     },monad.val);
57: }
58: 
59: int main()
60: {
61:     using T = int;
62: 
63:     uint16_t val = val;
64:     { // Constructing the monad with values 0-9
65:         auto m_t = construct<T>(0,1,2,3,4,5,6,7,8,9);
66: 
67:         if( auto *arr =std::get_if<Monad<T>::Container_type>(&m_t.val) )
68:         {
69:             fmt::print("|M<Int> | {} |\n",arr->size());
70:             do_something(m_t);
71:         }
72: 
73:     }
74:     {// Constructing a Nothing monad
75:         [[maybe_unused]] auto m_n = construct();
76:         fmt::print("| M<Nothing> | 0|\n");
77:         do_something(m_n);
78:     }
79: 
80: 
81:     return 0;
82: }
83: 
84: 
M<Int> 10
Type std::array<int>
M<Nothing> 0
Type Nothing

Expected

Expected is a really cool construct. Imagine that you want to have a sequence of functions where each function transforms one input to another . This will continue until the last. This is of course easy to do using if-then-else expressions. The problem is that it becomes quite messy. But by using Monads which is just a class consisting of other classes as template arguments together with mbind function, the code could easily be constructed in the divide-and-conquer order.

\begin{equation} fn(x) = gn(cn(x)) \end{equation}

In this way you can divide the problem into smaller bits and at the same time have full control what happens.. Lets do an example first.

Scenario

A number is received in form of a string.

  • Convert the string to a number if possible
    • If not possible Return error Enum INVALID_NUMBER
  • The number cannot be larger than uint32_t
    • If Number is larger return Enum NUMBER_OVERFLOW
  • The number needs to be converted to a byte array for little endian (MSB first)

Code without monads

#include <fmt/format.h>
#include <boost/lexical_cast.hpp>
#include <span>
#include <algorithm>
#include <ranges>
#include <variant>


enum class Error
{
    INVALID_NUMBER,
    NUMBER_OVERFLOW,
    CONVERSION_ERROR
};

bool is_num(const std::string& str, unsigned& val)
{
    try {
        val = boost::lexical_cast<int>(str);
    }catch(boost::bad_lexical_cast &)
    {
        return false;
    }
    return true;
}

bool validate_num( auto val)
{
    if( val < 0xff'ff'ff'ff)
    {
        return true;
    }
    else
    {
        return false;
    }
}

bool convert_byte_array([[maybe_unused]]uint32_t val, [[maybe_unused]] std::vector<uint8_t>& arr)
{
    // Do something useful
    arr.push_back(0xff);
    return true;
}

void print_out(std::span<uint8_t>  vals)
{
    std::ranges::for_each(vals, [](auto val){ fmt::print("{:08b} ",val);});

}


using Response = std::variant<std::vector<uint8_t>, Error>;


Response do_evaluate(std::string num)
{

    unsigned val{};
    if( is_num(num,val) )
    {
        if( validate_num(val) )
        {
            std::vector<uint8_t> arr;
            if( convert_byte_array(val,arr) )
            {
                return Response{arr};
            }else
            {
                return Response{Error::CONVERSION_ERROR};
            }
        }else
        {
            return Response{Error::NUMBER_OVERFLOW};
        }
    }else{
        return Response{Error::INVALID_NUMBER};
    }
}


int main(int argc, char *argv[])
{
    {
        auto resp = do_evaluate("1234");

        auto* resp_ptr= std::get_if<std::vector<uint8_t> >(&resp);
        if( resp_ptr != nullptr )
        {
            fmt::print("Response Valid\n ");
        }else
        {
            fmt::print("Response  Invalid\n");
        }
    }

    {
        auto resp = do_evaluate("12340000000000000000000000000000000000");

        auto* resp_ptr= std::get_if<std::vector<uint8_t> >(&resp);
        if( resp_ptr != nullptr )
        {
            fmt::print("Response Valid\n");
        }else
        {
            fmt::print("Response  Invalid\n");
        }
    }






        return 0;
    }



Response Valid
Response Invalid

This seems to work pretty good , the only problem is that its not very structural pleasing. If you for some reason needs to do more tests or do another transformation the whole if-then-else will soon become unbearable. So what can we do about it? Introducing expected

Code using monads

 1: #include <type_traits>
 2: #include <cstdint>
 3: #include <variant>
 4: #include <fmt/format.h>
 5: #include <optional>
 6: #include <cassert>
 7: 
 8: 
 9: template <typename T, typename Error_t>
10: struct expected
11: {
12:     using Value_type = T;
13:     using Error_type = Error_t;
14:     using OptVal = std::optional<T>;
15:     using OptErr = std::optional<Error_t>;
16:     expected(T value): val{value}{}
17:     expected(Error_t err): val{err}{}
18: 
19: 
20: 
21: 
22:     bool operator()() const {
23:         return ( std::get_if<T>(&val) != nullptr);
24:     }
25: 
26:     operator bool () const
27:     {
28:         return ( std::get_if<T>(&val) != nullptr);
29:     }
30: 
31:     OptVal get() const
32:     {
33:         const auto* value_ptr = std::get_if<T>(&val);
34:         return ( value_ptr != nullptr ?
35:                  std::make_optional(*value_ptr)
36:                  : std::nullopt ) ;
37:     }
38: 
39:     OptErr error() const
40:     {
41:         const auto* error_ptr = std::get_if<Error_t>(&val);
42:         return ( error_ptr != nullptr ?
43:                  std::make_optional(*error_ptr)
44:                  : std::nullopt );
45:     }
46: 
47:     std::variant<T, Error_t> val;
48: 
49: 
50: };
51: 
52: 
53: template <typename T, typename E, typename Fn>
54: auto operator|(const expected<T,E> e, Fn fn)
55: {
56:     return fn(e);
57: }
58: 
59: 
60: 
61: template<typename T,typename Error_t>
62: auto make_expected(auto val)
63: {
64:     return expected<T,Error_t>(val);
65: }
66: 
67: 
68: 
69: auto transform( auto f)
70: {
71:     return [&f](const expected<auto, auto>& exp) -> decltype(f(*exp.get()))
72:     {
73:         using ExType = decltype(f(*exp.get()));
74:         if (!exp)
75:         {
76:             return ExType(*exp.error());
77:         }
78: 
79:         return f(*exp.get());
80:     };
81: }
Valid response 2
Invalid response 0
Invalid response 1

You get the idea, the important takeaway here is the extendibility and readability. Its easy to just make more transformations and keep on going until the you reached the final transformation. This also makes it possible to focus on one transformation at a time, and its return value(s). What one function returns ( as Value T in the expected), is the input to the next function (Line 76). And if something fails during the transformation sequence, the transform function will return the same error without calling the function (Line 79), which means it will end where it broke, and the return value will indicate the problem.

Of course the there are many other ways to work with monads. But I find this construct really useful and beautiful.

Curried functions

Curried function is a function which returns another function if all arguments aren't avaialble.

For example

\begin{equation} f(x,y) = x + y \\ g(1,y) = 1 + y \\ g(1,2) = 1 + 2 \end{equation}

What im trying to explain above is the fact that if I provide just one argument to function f (but two is needed) it will return a new function. And if I provide yet another argument to the one that was provided, I would get the result.

The benefit with such construct is that it would store the provied values until all of them are satisfied.

One example would be to create a function which returns a lambda, as the following example shows

 1: #include <fmt/format.h>
 2: 
 3: auto get_add_fn(int x )
 4: {
 5:     return [x](int y)
 6:     {
 7: 
 8:         return x+y;
 9:     };
10: }
11: 
12: 
13: int main(int argc, char *argv[])
14: {
15:     auto gn = get_add_fn(1);
16:     int val = gn(2);
17:     fmt::print("| val | {}|\n",val);
18: 
19:     fmt::print("| val | {}|\n", get_add_fn(2)(2));
20:     return 0;
21: }
22: 
23: 
val 3
val 4

The problem with the above construct is that explicit to just one function. If we were to have several more arguments, it would mean we need to provide get_add_fn which returns a new function for each of the arguments. With that said we could create our own structure.

Curried functions with templates

What we want to do is something more generic that works with any funcions So for example if we have a function \(F(x,y,n)\) and provide only x we get a new function \(G(y,n)\) for each of the argument until have fulfilled all the arguments.

So lets dig into it.

  • [X] We know we need some kind of function f(…..)
  • [X] We know that to the function there will be a number of arguments
  • [X] We need a () operator which we can provide new arguments.
    • [X] The number of arguments needs to be variable and generic. (line 21)
    • [X] We need to create a new tuple from the new arguments (line 23).
      • [X] Those argumens needs to be concatenated with the ones already provided (line 24)
    • [X] We need to check if the argument matches the number of argument in the function (line 26).
      • [X] If Not, it needs to generate a new function with that argument captured (line 30).
      • [X] If all arguments are provided it need to provide the calculated answere by calling Function (line 27).
 1: 
 2: template<typename Function,typename...CaptureArgs>
 3: class curried
 4: {
 5:     using ArgTuple = std::tuple<std::decay_t<CaptureArgs>...>;
 6:   public:
 7:     curried(Function function, CaptureArgs...args):
 8:             function_(function),
 9:             args_captured_(capture_by_copy(std::move(args)...))
10:     {
11:     }
12: 
13: 
14:     curried(Function function,
15:            ArgTuple args)
16:             : function_(function)
17:             , args_captured_(std::move(args))
18:     {
19:     }
20: 
21:     template<typename...NewArgs>
22:     auto operator()(NewArgs&&...args) const
23:     {
24:         auto new_args  = capture_by_copy(std::forward<NewArgs>(args)...);
25:         auto all_args = std::tuple_cat(args_captured_, new_args);
26: 
27:         if constexpr (std::is_invocable_v<Function,CaptureArgs...,NewArgs...>){
28:             return std::apply(function_, all_args);
29:         }else
30:         {
31:             return curried<Function,CaptureArgs...,NewArgs...>(function_, all_args);
32:         }
33:     }
34: 
35:   private:
36:     template <typename... Args>
37:     static auto capture_by_copy(Args&&... args)
38:     {
39:         return std::tuple<std::decay_t<Args>...>(
40:                 std::forward<Args>(args)...);
41:     }
42: 
43:     Function function_;
44:     ArgTuple args_captured_;
45: 
46: };
47: 
48: 
49: int main(int argc, char *argv[])
50: {
51:     auto fn = [](auto x,auto y,auto z)
52:     {
53: 
54:         return x+y+z;
55:     };
56: 
57:     {
58:         auto f = curried(fn);
59:         fmt::print("|val | {}\n",f(1,2,3));
60:     }
61: 
62:     {
63:       auto f= curried(fn);
64:       auto f1 = f(1);
65:       auto f12 = f1(2);
66:       fmt::print("|val|{}|\n",f12(3));
67: 
68:     }
69:     return 0;
70: }
71: 
72: 
val 6
val 6

A note: Since the function is trivially copyable, there is no reason to do a move on the function. The reason is simple , since the currie class keeps the state there is no reason for the function to have any captures.

Date: 2022-09-18 Sun 00:00

Author: calle

Created: 2022-09-18 Sun 18:09

Validate