Search code examples
c++c++17immutabilitymonadsalgebraic-data-types

Modifying immutable substructures


Suppose I have an immutable wrapper:

template<class T>
struct immut {
  T const& get() const {return *state;}
  immut modify( std::function<T(T)> f ) const { return immut{f(*state)}; }
  immut(T in):state(std::make_shared<T>(std::move(in))){}
private:
  std::shared_ptr<T const> state;
};

if I have an immut<Bob> b, I can turn a Bob(Bob) operation into something that can replace my b.

template<class T>
std::function<immut<T>(immut<T>)> on_immut( std::function<void(T&)> f ){
  return [=](auto&&in){ return in.modify( [&](auto t){ f(t); return t; } ); };
}

So if Bob is int x,y;, I can turn naive mutable code [](auto& b){ b.x++; } into a immut<Bob> updater.


Now, what if Bob in turn has immut<Alice> members, which in turn have immut<Charlie> members.

Suppose I have a Charlie updater, void(Charlie&). And I know where the Charlie I want to update is. In mutable land it would look like :

void update( Bob& b ){
  modify_charlie(b.a.c[77]);
}

and I might split it into:

template<class S, class M>
using get=std::function<M&(S&)>;
template<class X>
using update=std::function<void(X&)>;
template<class X>
using produce=std::function<X&()>;

void update( produce<Bob> b, get<Bob, Alice> a, get<Alice, Charlie> c, update<Charlie> u ){
  u(c(a(b())));
}

or even go algebraic and have (pseudo code):

get<A,C> operator|(get<A,B>,get<B,C>);
update<A> operator|(get<A,B>,update<B>);
produce<B> operator|(produce<A>,get<A,B>);
void operator|(produce<A>, update<A>);

letting us chain together operations as needed.

void update( produce<Bob> b, get<Bob, Alice> a, get<Alice, Charlie> c, update<Charlie> u ){std::
  u(c(a(b())));
}

becomes

b|a|c|u;

where the steps can be stitched together where-ever they need to be.


What is the equivalent with immuts, and is there a name for it? Ideally I want the steps to be as isolated yet as composable as in the mutable case, with the naive "leaf code" being on mutable state structs.


Solution

  • What is the equivalent with immuts, and is there a name for it?

    IDK whether there is a universal name for sub-state manipulation. But in Haskell, there is a Lens which offers exactly what you want.

    In C++, you can consider a lens as a pair of getter and setter functions, both of which only focus on its direct subpart. Then there is a composer to combine two lens together to focus on a deeper subpart of a structure.

    // lens for `A` field in `D`
    template<class D, class A>
    using get = std::function<A const &(D const &)>;
    
    template<class D, class A>
    using set = std::function<D(D const &, A)>;
    
    template<class D, class A>
    using lens = std::pair<get<D, A>, set<D, A>>;
    
    // compose (D, A) lens with an inner (A, B) lens,
    // return a (D, B) lens
    template<class D, class A, class B>
    lens<D, B>
    lens_composer(lens<D, A> da, lens<A, B> ab) {
        auto abgetter = ab.first;
        auto absetter = ab.second;
        auto dagetter = da.first;
        auto dasetter = da.second;
    
        get<D, B> getter = [abgetter, dagetter]
            (D const &d) -> B const&
        {
            return abgetter(dagetter(d));
        };
    
        set<D, B> setter = [dagetter, absetter, dasetter]
            (D const &d, B newb) -> D
        {
            A const &a = dagetter(d);
            A newa = absetter(a, newb);
            return dasetter(d, newa);
        };
    
        return {getter, setter};
    };
    

    You can write a basic lens like this:

    struct Bob {
        immut<Alice> alice;
        immut<Anna>  anna;
    };
    auto bob_alice_lens
    = lens<Bob, Alice> {
        [] (Bob const& b) -> Alice const & {
            return b.alice.get();
        },
        [] (Bob const& b, Alice newAlice) -> Bob {
            return { immut{newAlice}, b.anna };
        }
    };
    

    Note this process can be automated by macro.

    Then if Bob contains immut<Alice>, Alice contains immut<Charlie>, you can write 2 lens (Bob to Alice and Alice to Charlie), the Bob to Charlie lens can be composed by:

    auto bob_charlie_lens = 
    lens_composer(bob_alice_lens, alice_charlie_lens);
    

    Live Demo

    NOTE:

    Below is a more complete example with linear memory growth w.r.t depth, without type-erasure overhead (std::function), and with full compile time type check. It also uses macro to generate basic lens.

    #include <type_traits>
    #include <utility>
    
    template<class T>
    using GetFromType = typename T::FromType;
    
    template<class T>
    using GetToType = typename T::ToType;
    
    // `get` and `set` are fundamental operations for Lens,
    // this Mixin will add utilities based on `get` and `set`
    template<class Derived>
    struct LensMixin {
        Derived &self() { return static_cast<Derived&>(*this); }
    
        // f has type: A& -> void
        template<class D, class Mutation>
        auto modify(D const &d, Mutation f)
        {
            auto a = self().get(d);
            f(a);
            return self().set(d, a);
        }
    };
    
    template<
        class Getter, class Setter,
        class D, class A
        >
    struct SimpleLens : LensMixin<SimpleLens<Getter, Setter, D, A>> {
        static_assert(std::is_same<
                std::invoke_result_t<Getter, const D&>, const A&>{},
                "Getter should return const A& for (const D&)");
    
        static_assert(std::is_same<
                std::invoke_result_t<Setter, const D&, A>, D>{},
                "Setter should return D for (const D&, A)");
        using FromType = D;
        using ToType = A;
    
        SimpleLens(Getter getter, Setter setter)
            : getter(getter)
            , setter(setter)
        {}
    
        A const &get(D const &d) { return getter(d); }
        D set(D const &d, A newa) { return setter(d, newa); }
    private:
        Getter getter;
        Setter setter;
    };
    
    template<
        class LensDA, class LensAB
        >
    struct ComposedLens : LensMixin<ComposedLens<LensDA, LensAB>> {
        static_assert(std::is_same<
                GetToType<LensDA>, GetFromType<LensAB>
            >{}, "Cannot compose two Lens with wrong intermediate type");
    
        using FromType = GetFromType<LensDA>;
        using ToType = GetToType<LensAB>;
    
    private:
        using intermediateType = GetToType<LensDA>;
        using D = FromType;
        using B = ToType;
        LensDA da;
        LensAB ab;
    public:
    
        ComposedLens(LensDA da, LensAB ab) : da(da), ab(ab) {}
    
        B const &get(D const &d) { return ab.get(da.get(d)); }
        D set(D const &d, B newb) {
            const auto &a = da.get(d);
            auto newa = ab.set(a, newb);
            return da.set(d, newa);
        }
    };
    
    namespace detail {
        template<class LensDA, class LensAB>
        auto MakeComposedLens(LensDA da, LensAB ab) {
            return ComposedLens<LensDA, LensAB> { da, ab };
        }
    
        template<class D, class A, class Getter, class Setter>
        auto MakeSimpleLens(Getter getter, Setter setter)
        {
            return SimpleLens<Getter, Setter, D, A> {
                getter, setter
            };
        }
    }
    
    template<class LensDA, class LensAB>
    auto lens_composer(LensDA da, LensAB ab) {
        return detail::MakeComposedLens (da, ab);
    }
    
    
    #include <memory>
    
    template<class T>
    struct immut {
      T const& get() const {return *state;}
      immut(T in):state(std::make_shared<T>(std::move(in))){}
    private:
      std::shared_ptr<T const> state;
    };
    
    #define MAKE_SIMPLE_LENS(D, A, Aname)   \
        detail::MakeSimpleLens<D, A>(       \
            +[] (D const &d) -> A const & { \
                return d . Aname . get();   \
            },                              \
            +[] (D const &d, A newa) -> D { \
                D newd = d;                 \
                newd . Aname = newa;        \
                return newd;                \
            })
    
    
    
    
    struct Charlie {
        int id = 0;
    };
    
    struct Alice{
        immut<Charlie> charlie;
    };
    struct Anna {};
    struct Bob {
        immut<Alice> alice;
        immut<Anna>  anna;
    };
    
    auto alice_charlie_lens = MAKE_SIMPLE_LENS(Alice, Charlie, charlie);
    auto bob_alice_lens     = MAKE_SIMPLE_LENS(Bob, Alice, alice);
    auto bob_charlie_lens   = lens_composer(bob_alice_lens, alice_charlie_lens);
    
    static_assert(std::is_same<GetFromType<decltype(bob_charlie_lens)>, Bob>{});
    static_assert(std::is_same<GetToType<decltype(bob_charlie_lens)>, Charlie>{});
    
    #include <iostream>
    
    int main() {
        immut<Charlie> charlie{Charlie{77}};
        immut<Alice> alice{Alice{charlie}};
        immut<Anna>  anna{Anna{}};
    
        Bob bob{alice, anna};
        std::cout << "bob     -> anna: " << static_cast<void const*>(&bob.anna.get()) << "\n";
        std::cout << "bob     -> charlie: " << bob_charlie_lens.get(bob).id << "\n";
    
        // Bob newbob = bob_charlie_lens.set(bob, Charlie{148});
        Bob newbob = bob_charlie_lens.modify(bob, [] (auto &charlie) {
                charlie.id += (148 - 77);
                });
        std::cout << "new bob -> anna: " << static_cast<void const*>(&bob.anna.get()) << "\n";
        std::cout << "old bob -> charlie: " << bob_charlie_lens.get(bob).id << "\n";
        std::cout << "new bob -> charlie: " << bob_charlie_lens.get(newbob).id << "\n";
    }