diff --git a/protos/bento/protos/graph.proto b/protos/bento/protos/graph.proto index ed6a983..a5d2a92 100644 --- a/protos/bento/protos/graph.proto +++ b/protos/bento/protos/graph.proto @@ -22,8 +22,8 @@ message Node { /* Component access, mutation */ // Retrieve - node that retrieves a value from an entity's component message Retrieve { - // Reference to the attribute to retrieve - AttributeRef retrieve_attr = 1; + // Reference to the value to retrieve + ValueRef retrieve_value = 1; } // Mutate - node mutate the value in an entity's component based @@ -33,8 +33,8 @@ message Node { // attribute's value to Node to_node = 1; - // Reference to the attribute to mutate with value - AttributeRef mutate_attr = 2; + // Reference to the value to mutate + ValueRef mutate_ref = 2; } /* Control flow */ diff --git a/protos/bento/protos/references.proto b/protos/bento/protos/references.proto index be7de47..2507ba2 100644 --- a/protos/bento/protos/references.proto +++ b/protos/bento/protos/references.proto @@ -7,14 +7,16 @@ syntax = "proto3"; package bento.protos; -// Fully-qualified reference to an component's attribute +// Fully-qualified reference to a value stored in a component // Assumes that the component is bound the entity and attribute exists in the // component type -message AttributeRef { - // id of the entity to the retrieve from +message ValueRef { + // id of the entity to retrieve from uint32 entity_id = 1; // Name of the component type to retrieve from entity string component = 2; // Name of the attribute to retrieve from component string attribute = 3; + // A list of indexes + repeated int64 attr_indexes = 4; } diff --git a/protos/bento/protos/services.proto b/protos/bento/protos/services.proto index 1a7fc19..fe27926 100644 --- a/protos/bento/protos/services.proto +++ b/protos/bento/protos/services.proto @@ -24,9 +24,9 @@ service EngineService { // Run one step of the simulation rpc StepSimulation(StepSimulationReq) returns (StepSimulationResp); - // Set, Get component's Attributes - rpc GetAttribute(GetAttributeReq) returns (GetAttributeResp); - rpc SetAttribute(SetAttributeReq) returns (SetAttributeResp); + // Set, Get values in components + rpc GetValue(GetValueReq) returns (GetValueResp); + rpc SetValue(SetValueReq) returns (SetValueResp); } message GetVersionReq {} @@ -72,23 +72,23 @@ message StepSimulationReq { } message StepSimulationResp {} -message GetAttributeReq { - // Name of the simulation to retrieve the attribute from +message GetValueReq { + // Name of the simulation to retrieve the value from string sim_name = 1; - // Reference to the attribute to retrieve - AttributeRef attribute = 2; + // Reference to the value to retrieve + ValueRef value_ref = 2; } -message GetAttributeResp { - // Stored value of the requested attribute +message GetValueResp { + // Stored value of the requested value Value value = 1; } -message SetAttributeReq { - // Name of the simulation to set the attribute +message SetValueReq { + // Name of the simulation to set the value string sim_name = 1; - // Reference to the target attribute to set - AttributeRef attribute = 2; - // The value to the set the target attribute to + // Reference to the target value to set + ValueRef value_ref = 2; + // The value to the set the target ValueRef to Value value = 3; } -message SetAttributeResp {} +message SetValueResp {} diff --git a/sim/lib/core/CMakeLists.txt b/sim/lib/core/CMakeLists.txt index 06ebe8a..be140b7 100644 --- a/sim/lib/core/CMakeLists.txt +++ b/sim/lib/core/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(core-test src/ics/util/composable.test.cpp src/ics/util/setIntersection.test.cpp src/ics/util/typeMap.test.cpp + src/optics/lens.test.cpp ) target_link_libraries(core-test PRIVATE core diff --git a/sim/lib/core/include/core/optics/lens.hpp b/sim/lib/core/include/core/optics/lens.hpp new file mode 100644 index 0000000..4d37695 --- /dev/null +++ b/sim/lib/core/include/core/optics/lens.hpp @@ -0,0 +1,179 @@ +#ifndef BENTOBOX_LENS_HPP +#define BENTOBOX_LENS_HPP + +#include +#include + +namespace { +template +struct is_same_args { + // This static constexpr function compares the given type arguments and + // ensures that they are the same as Args + template + static constexpr bool _as() { + // constexpr is required here because N is a non-type template + // argument + if constexpr (N == sizeof...(OtherArgs)) { + return true; + } else if (std::is_same_v< + std::remove_reference_t< + std::tuple_element_t>>, + std::remove_reference_t< + std::tuple_element_t>>>) { + return _as(); + } else { + return false; + } + } + + // Alias to the recursive comparison constexpr function + template + static constexpr bool as() { + return _as<0, OtherArgs...>(); + } +}; +} // namespace + +namespace optics { + +// Note on std::move vs static_cast&&> +// Later in the code, you will see that I have used static_cast instead of +// std::move. The reason is that we need to cast to an rvalue reference +// because that's the type of the function's parameter. +// std::move is not used because it is semantically incorrect, though it +// does the same thing. + +template +class Getter { + protected: + public: + const std::function&&...)> getter; + // A getter cannot be constructed without a getter function + Getter() = delete; + explicit Getter(std::function&& getter) + : getter(std::move(getter)) {} + explicit Getter(const std::function& getter) + : getter(getter) {} + + template + requires(is_same_args::template as()) Return + get(RArgs&&... args) const { + return getter(static_cast>(args)...); + } + + template + Getter compose( + std::function&& composeGetter) const { + // Make a copy of the getter so that if the current Getter is destroyed + // The returned getter is still usable + std::function getterCopy(getter); + return Getter( + [getterCopy, composeGetter](Args&&... args) { + return std::invoke( + composeGetter, + std::invoke(getterCopy, std::forward(args)...)); + }); + } + + // Oddly, C++ does not recognise lambdas when std::function is used here. + // So, C++20 requirements are used. + // The attempted definition of a std::function is: + // template + // Getter + // precompose(std::function>(NewArgs...)> precomposeGetter) { + template + // Require that the current getter only needs one argument + requires(sizeof...(Args) == 1) && + // Require that the type Fn is something invocable with the NewArgs and + // returns the first argument + std::is_invocable_r_v< + std::tuple_element_t<0, std::tuple>, Fn, + NewArgs...> Getter precompose(Fn&& precomposeGetter) + const { + // Make a copy of the getter so that if the current Getter is destroyed + // The returned getter is still usable + std::function getterCopy(getter); + return Getter( + [getterCopy, precomposeGetter](NewArgs&&... args) { + return std::invoke(getterCopy, + std::invoke(precomposeGetter, + std::forward(args)...)); + }); + } +}; + +template +class Lens { + private: + typedef std::function SetterFn; + // When writing the getter as a lambda, it is important to specify the + // arguments as rvalue references and the return type as an lvalue + // reference. + typedef std::function GetterFn; + + public: + const SetterFn setter; + const GetterFn getter; + // A lens cannot be constructed without any functions + Lens() = delete; + Lens(GetterFn&& getter, SetterFn&& setter) + : getter(std::move(getter)), setter(std::move(setter)) {} + Lens(const GetterFn& getter, const SetterFn& setter) + : getter(getter), setter(setter) {} + + template + requires(is_same_args::template as()) Return& get( + RArgs&&... args) const { + return getter(static_cast&&>(args)...); + } + + template + // Ensure that the arguments are the correct type + requires(is_same_args::template as()) + // Ensure that RReturn is the correct type + && std::is_same_v, Return> void set( + RReturn&& val, RArgs&&... args) const { + auto& ref = get(std::forward(args)...); + this->setter(ref, static_cast&&>(val)); + } + + template + Lens compose( + const Lens& otherLens) const { + std::function get = + [ + getter = this->getter, otherGetter = otherLens.getter + ](PArgs && ... args) + ->NewReturn& { + return otherGetter( + static_cast(getter(std::forward(args)...))); + }; + + return Lens(get, otherLens.setter); + } + + template + requires(sizeof...(Args) == 1) && + std::is_same_v< + std::tuple_element_t<0, std::tuple>, + FirstArg> Lens precompose(const Lens + otherLens) const { + std::function get = + [ + getter = this->getter, otherGetter = otherLens.getter + ](PArgs && ... args) + ->Return& { + return getter(static_cast( + otherGetter(std::forward(args)...))); + }; + + return Lens(get, this->setter); + } +}; + +} // namespace optics + +#endif // BENTOBOX_LENS_HPP diff --git a/sim/lib/core/src/optics/lens.test.cpp b/sim/lib/core/src/optics/lens.test.cpp new file mode 100644 index 0000000..ac90bb8 --- /dev/null +++ b/sim/lib/core/src/optics/lens.test.cpp @@ -0,0 +1,135 @@ +#include +#include + +#define TEST_SUITE Lenstest + +using namespace optics; + +TEST(TEST_SUITE, GetterFromLambda) { + // From rvalue + { + auto getter = Getter([](int j, int k) { return 'a'; }); + ASSERT_EQ(getter.get(5, 7), 'a'); + } + + // From lvalue + { + auto lambdaGetter = [](T&& j, U&& k) { + return j + k; + }; + auto getter = Getter(lambdaGetter); + // This is to test that get can accept an lvalue + auto val = 9; + ASSERT_EQ(getter.get(val, 10), 19); + } +} + +TEST(TEST_SUITE, ComposeGetters) { + auto getter = Getter([](int j, int k) { return j + k; }); + auto composedGetter = getter.compose([](int h) { return h + 20; }); + + ASSERT_EQ(composedGetter.get(1, 2), 23); +} + +TEST(TEST_SUITE, PrecomposeGetters) { + auto getter = Getter([](int j) { return j * 10; }); + auto precomposedGetter = + getter.precompose([](int h) -> int { return h + 20; }); + + ASSERT_EQ(precomposedGetter.get(1), 210); +} + +TEST(TEST_SUITE, LensFromLambda) { + // From rvalue + { + int storedVal = 3; + auto lens = + Lens([](int&& j) -> int& { return j; }, + [](int& j, int newVal) -> void { j = newVal - 10; }); + ASSERT_EQ(lens.get(3), 3); + lens.set(14, storedVal); + ASSERT_EQ(storedVal, 4); + } + + // From lvalue + { + int storedVal = 3; + auto getter = [](int&& j, int&& k) -> int& { return j; }; + auto setter = [](int& j, int newVal) { j = newVal - 10; }; + auto lens = Lens(getter, setter); + // This is to test that get can accept an lvalue + ASSERT_EQ(lens.get(storedVal, 10), 3); + lens.set(14, storedVal, 5); + ASSERT_EQ(storedVal, 4); + } +} + +struct B { + int height = 0; +}; + +struct A { + B b; +}; + +TEST(TEST_SUITE, ComposeLens) { + auto storedVal = A{B{10}}; + auto getter = [](A&& a) -> B& { return a.b; }; + auto setter = [](B& b, B&& newB) { b = newB; }; + auto lens = Lens(getter, setter); + + ASSERT_EQ(lens.get(storedVal).height, 10); + + auto getterComp = [](B&& b) -> int& { return b.height; }; + auto setterComp = [](int& height, int&& newHeight) { height = newHeight; }; + auto lensComp = Lens(getterComp, setterComp); + + auto composedLens = lens.compose(lensComp); + ASSERT_EQ(composedLens.get(storedVal), 10); + composedLens.set(20, storedVal); + ASSERT_EQ(composedLens.get(storedVal), 20); +} + +TEST(TEST_SUITE, PrecomposeLens) { + auto storedVal = A{B{10}}; + auto getter = [](A&& a) -> B& { return a.b; }; + auto setter = [](B& b, B&& newB) { b = newB; }; + auto lens = Lens(getter, setter); + + ASSERT_EQ(lens.get(storedVal).height, 10); + + auto getterComp = [](B&& b) -> int& { return b.height; }; + auto setterComp = [](int& height, int&& newHeight) { height = newHeight; }; + auto lensComp = Lens(getterComp, setterComp); + + auto composedLens = lensComp.precompose(lens); + ASSERT_EQ(composedLens.get(storedVal), 10); + composedLens.set(20, storedVal); + ASSERT_EQ(composedLens.get(storedVal), 20); +} + +TEST(TEST_SUITE, CompositionCopiesLenses) { + // Create the lenses + auto getter = [](A&& a) -> B& { return a.b; }; + auto setter = [](B& b, B&& newB) { b = newB; }; + auto lensAPtr = new Lens(getter, setter); + + auto getterComp = [](B&& b) -> int& { return b.height; }; + auto setterComp = [](int& height, int&& newHeight) { height = newHeight; }; + auto lensBPtr = new Lens(getterComp, setterComp); + + auto composedLens = lensAPtr->compose(*lensBPtr); + + // Delete the lenses + delete lensAPtr; + lensAPtr = nullptr; + delete lensBPtr; + lensBPtr = nullptr; + + // Use the composed lens + auto storedVal = A{B{10}}; + ASSERT_EQ(composedLens.get(storedVal), 10); + auto j = 20; + composedLens.set(j, storedVal); + ASSERT_EQ(composedLens.get(storedVal), 20); +}