Closed anton-pt closed 2 years ago
I extended my code with a simple demonstration of lenses. Constructive criticism would be most welcome.
Here are the supporting classes:
#include <iostream>
#include <memory>
#include <functional>
#include "immer/map.hpp"
template <typename TField, const char* name>
class field_setter {
public:
explicit field_setter(std::shared_ptr<TField> value) : _value(std::move(value)) { }
immer::map<std::string, std::shared_ptr<void>> apply(immer::map<std::string, std::shared_ptr<void>> data) const {
return data.set(name, std::static_pointer_cast<void>(_value));
}
private:
const std::shared_ptr<TField> _value;
};
template<typename TRecord, typename TField>
class lens {
public:
lens(
std::function<std::shared_ptr<const TField>(TRecord)> get,
std::function<TRecord(TRecord, const std::shared_ptr<TField>&)> set)
: _get(std::move(get)), _set(std::move(set)) { }
std::shared_ptr<const TField> get(TRecord record) const { return _get(record); }
TRecord set(TRecord record, const std::shared_ptr<TField>& value) const { return _set(record, value); }
private:
std::function<std::shared_ptr<const TField>(TRecord)> _get;
std::function<TRecord(TRecord, std::shared_ptr<TField>)> _set;
};
template <typename TA, typename TB, typename TC>
lens<TA, TC> operator>>(lens<TA, TB> left, lens<TB, TC> right) {
return lens<TA, TC>(
[=](TA record) { return right.get(*left.get(record)); },
[=](TA record, const std::shared_ptr<TC>& value) {
return left.set(record, std::make_shared<TB>(right.set(*left.get(record), value)));
});
}
template <typename TField, const char* name>
immer::map<std::string, std::shared_ptr<void>> operator>>(immer::map<std::string, std::shared_ptr<void>> data, const field_setter<TField, name>& setter) {
return setter.apply(data);
}
template<typename TRecord, typename TField, const char* name>
class field {
public:
static std::shared_ptr<const TField> get(const immer::map<std::string, std::shared_ptr<void>>& data) {
return std::static_pointer_cast<const TField>(data[name]);
}
static TRecord set(immer::map<std::string, std::shared_ptr<void>> data, const std::shared_ptr<TField>& value) {
return TRecord(data.set(name, std::static_pointer_cast<void>(value)));
}
static field_setter<TField, name> setter(const std::shared_ptr<TField>& value) {
return field_setter<TField, name>(value);
}
static lens<TRecord, TField> optic() {
return lens<TRecord, TField>(
[](TRecord record) { return get(record.raw_data()); },
[](TRecord record, const std::shared_ptr<TField>& value) { return set(record.raw_data(), value); });
}
};
class record {
public:
using data = immer::map<std::string, std::shared_ptr<void>>;
explicit record(data data) : _data(std::move(data)) { }
data raw_data() { return _data; }
protected:
const data _data;
};
And here is some example application code built on top of them:
class contact : public record {
public:
contact(const std::shared_ptr<std::string>& telephone, const std::shared_ptr<std::string>& email)
: record(data() >> telephone::setter(telephone) >> email::setter(email)) { }
explicit contact(data data) : record(std::move(data)) { }
std::shared_ptr<const std::string> get_telephone() const { return telephone::get(_data); }
const contact set_telephone(const std::shared_ptr<std::string>& telephone) const { return telephone::set(_data, telephone); }
std::shared_ptr<const std::string> get_email() const { return email::get(_data); }
const contact set_email(const std::shared_ptr<std::string>& email) const { return email::set(_data, email); }
static lens<contact, std::string> telephone_() { return telephone::optic(); }
static lens<contact, std::string> email_() { return email::optic(); }
private:
static constexpr char _telephone[] { "telephone" };
using telephone = field<contact, std::string, _telephone>;
static constexpr char _email[] { "email" };
using email = field<contact, std::string, _email>;
};
constexpr char contact::_telephone[];
constexpr char contact::_email[];
class person : public record {
public:
person(const std::shared_ptr<std::string>& name, const std::shared_ptr<uint8_t>& age, const std::shared_ptr<contact>& contact)
: record(data() >> name::setter(name) >> age::setter(age) >> contact::setter(contact)) { }
explicit person(data data) : record(std::move(data)) { }
std::shared_ptr<const std::string> get_name() const { return name::get(_data); }
const person set_name(const std::shared_ptr<std::string>& name) const { return name::set(_data, name); }
std::shared_ptr<const uint8_t> get_age() const { return age::get(_data); }
const person set_age(const std::shared_ptr<uint8_t>& age) const { return age::set(_data, age); }
std::shared_ptr<const contact> get_contact() const { return contact::get(_data); }
const person set_contact(const std::shared_ptr<contact>& contact) const { return contact::set(_data, contact); }
static lens<person, std::string> name_() { return name::optic(); }
static lens<person, uint8_t> age_() { return age::optic(); }
static lens<person, contact> contact_() { return contact::optic(); }
private:
static constexpr char _name[] { "name" };
using name = field<person, std::string, _name>;
static constexpr char _age[] { "age" };
using age = field<person, uint8_t, _age>;
static constexpr char _contact[] { "contact" };
using contact = field<person, contact, _contact>;
};
constexpr char person::_name[];
constexpr char person::_age[];
constexpr char person::_contact[];
int main() {
auto john = person(
std::make_shared<std::string>("John Smith"),
std::make_shared<uint8_t>(42),
std::make_shared<contact>(
std::make_shared<std::string>("12345"),
std::make_shared<std::string>("j.smith@email.com")));
auto junior = john
.set_name(std::make_shared<std::string>("Johnny Junior"))
.set_age(std::make_shared<uint8_t>(12));
auto person_email = person::contact_() >> contact::email_();
auto junior_new_email = person_email.set(junior, std::make_shared<std::string>("junior@email.com"));
std::cout << *john.get_name() << ", age: " << +*john.get_age() << ", email: " << *john.get_contact()->get_email() << std::endl;
std::cout << *junior.get_name() << ", age: " << +*junior.get_age() << ", email: " << *junior.get_contact()->get_email() << std::endl;
std::cout << *junior_new_email.get_name() << ", age: " << +*junior_new_email.get_age() << ", email: " << *junior_new_email.get_contact()->get_email() << std::endl;
return 0;
}
Hi Anton!
I will study your code more carefully later. Just wanted to point out that I am indeed relatively familiar with lenses. I am specially interested in observable mutating lenses associated to stateful sources (like cursors in Om or reactive atoms and trackables in Reagent).
I wrote a library called Funken while working at Ableton tha tries to solve a similar problem in C++ (Funken is part of Atria that was inspired by another Clojure implementation by @brentshields, I can't find it online anymore though.) That might be an interesting place for you to look into. The documentation is scarce but reading the unit tests might help a bit. I am also working on a similar library right now for a client, but I am not sure if we will ever publish it.
Cheers!
JP
Thanks for the quick response, and I look forward to your feedback! I'll look into the libraries you mentioned. I've made a few improvements to the code, including hashing the strings at compile time and caching the result, and also a slightly more ergonomic API for construction with rvalue reference arguments, and so forth. The result is here.
Just thought I'd mention that I've made quite a lot of updates to the gist. If you think there's a place for something like this in immer, I'd be keen to get it up to the required quality.
This is now provided to some degree by Lager.
I'm a bit new to C++ and most of my experience is in FP languages, so please shoot me down if this is a terrible suggestion. However, here goes...
I recently watched the CppCon 2017 talk and greatly enjoyed it. One of the questions at the end got me thinking: can this library help with creating user-defined immutable data types? Furthermore, can we get some of the benefits of Clojure's dynamism by having the underlying data representation be arbitrarily extensible? And can we make it ergonomic by borrowing some more concepts from FP, namely lenses?
Here's the idea. Firstly, have the "record" type backed by a). Of course, writing is an immutable operation, so you get a new value as a result, with the benefits of structural sharing. Define a lens type so that for every field with a getter and setter, you can have a lens. The lenses can be compositional. This way, you could drill into arbitrarily deep hierarchies of these record types and manipulate the field you are interested in.
const immer::map<std::string, std::shared_ptr<void>>
. Define suitable constructors which take the possible combinations of arguments and initialise the map. Define helper functions which write to or read from the map in a strongly-typed manner (applying the necessary casts to and from shared_ptrHere's an example of the basic underlying implementation which I have in mind:
Then, in use:
I also wonder if, using a clever enough template, the record definition could be made to look something like:
With the combination of string interning for the field names, I think this could be made performant (certainly on par with Clojure).
What do you think?