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.
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";
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With