├── .gitignore ├── .github └── workflows │ └── ccpp.yml ├── CMakeLists.txt ├── LICENSE ├── .clang-format ├── example.cpp ├── include └── elm-architecture │ └── elm-architecture.hpp └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | *~ -------------------------------------------------------------------------------- /.github/workflows/ccpp.yml: -------------------------------------------------------------------------------- 1 | name: C/C++ CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: configure 13 | run: mkdir build && cd build && cmake -DEA_BUILD_EXAMPLE=ON .. 14 | - name: build 15 | run: cd build && make -j$(nproc) 16 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | project(elm-architecture VERSION 1.0.0 LANGUAGES CXX) 3 | 4 | option(EA_BUILD_EXAMPLE "Build the example application" OFF) 5 | 6 | find_package(Threads REQUIRED) 7 | 8 | add_library(elm-architecture INTERFACE) 9 | target_include_directories(elm-architecture INTERFACE $ $) 10 | target_compile_features(elm-architecture INTERFACE cxx_std_17) 11 | target_link_libraries(elm-architecture INTERFACE Threads::Threads) 12 | 13 | if(EA_BUILD_EXAMPLE) 14 | add_executable(elm-architecture-example example.cpp) 15 | target_link_libraries(elm-architecture-example elm-architecture) 16 | endif() 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Simon the Sourcerer AB 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: WebKit 3 | AlignAfterOpenBracket: AlwaysBreak 4 | AlignConsecutiveAssignments: 'true' 5 | AlignConsecutiveDeclarations: 'true' 6 | AlignEscapedNewlines: Left 7 | AlignOperands: 'true' 8 | AlignTrailingComments: 'true' 9 | AllowAllParametersOfDeclarationOnNextLine: 'true' 10 | AllowShortBlocksOnASingleLine: 'true' 11 | AllowShortCaseLabelsOnASingleLine: 'false' 12 | AllowShortFunctionsOnASingleLine: None 13 | AllowShortIfStatementsOnASingleLine: 'false' 14 | AllowShortLoopsOnASingleLine: 'false' 15 | AlwaysBreakAfterDefinitionReturnType: All 16 | AlwaysBreakAfterReturnType: AllDefinitions 17 | AlwaysBreakTemplateDeclarations: 'true' 18 | BinPackArguments: 'true' 19 | BinPackParameters: 'true' 20 | BreakBeforeBraces: Attach 21 | BreakConstructorInitializers: BeforeComma 22 | ColumnLimit: 120 23 | ConstructorInitializerAllOnOneLineOrOnePerLine: 'false' 24 | Cpp11BracedListStyle: 'true' 25 | DerivePointerAlignment: 'false' 26 | FixNamespaceComments: 'true' 27 | IncludeBlocks: Regroup 28 | IndentCaseLabels: 'true' 29 | IndentPPDirectives: AfterHash 30 | IndentWrappedFunctionNames: 'false' 31 | KeepEmptyLinesAtTheStartOfBlocks: 'false' 32 | Language: Cpp 33 | NamespaceIndentation: Inner 34 | PointerAlignment: Left 35 | ReflowComments: 'true' 36 | SortIncludes: 'true' 37 | SortUsingDeclarations: 'true' 38 | SpaceAfterTemplateKeyword: 'true' 39 | SpaceBeforeAssignmentOperators: 'true' 40 | SpaceBeforeParens: Never 41 | SpaceInEmptyParentheses: 'true' 42 | SpacesInAngles: 'false' 43 | SpacesInContainerLiterals: 'false' 44 | SpacesInParentheses: 'false' 45 | SpacesInSquareBrackets: 'false' 46 | Standard: Cpp11 47 | UseTab: Never 48 | 49 | ... 50 | -------------------------------------------------------------------------------- /example.cpp: -------------------------------------------------------------------------------- 1 | #include "elm-architecture/elm-architecture.hpp" 2 | 3 | #include 4 | 5 | namespace elm = elm_architecture; 6 | 7 | // Model 8 | 9 | struct model_type { 10 | int counter = 0; 11 | }; 12 | 13 | // Msg 14 | 15 | struct increase {}; 16 | 17 | struct decrease {}; 18 | 19 | struct user_increase { 20 | int value; 21 | }; 22 | 23 | using message_type = std::variant; 24 | 25 | std::shared_future 26 | delayed_increase(std::chrono::milliseconds delay) { 27 | return std::async( 28 | std::launch::async, 29 | [delay]( ) -> message_type { 30 | std::this_thread::sleep_for(delay); 31 | return increase {}; 32 | }) 33 | .share( ); 34 | } 35 | 36 | std::shared_future 37 | delayed_decrease(std::chrono::milliseconds delay) { 38 | return std::async( 39 | std::launch::async, 40 | [delay]( ) -> message_type { 41 | std::this_thread::sleep_for(delay); 42 | return decrease {}; 43 | }) 44 | .share( ); 45 | } 46 | 47 | std::shared_future 48 | ask_user( ) { 49 | return std::async( 50 | std::launch::async, 51 | []( ) -> message_type { 52 | int amount = 0; 53 | std::cin >> amount; 54 | return user_increase {amount}; 55 | }) 56 | .share( ); 57 | } 58 | 59 | // Update 60 | 61 | struct update_fn { 62 | using return_type = elm::return_type; 63 | 64 | static auto 65 | update(const model_type& mod, const increase&) -> return_type { 66 | auto next = mod; 67 | next.counter += 1; 68 | std::cout << "Increasing counter from " << mod.counter << " to " << next.counter << std::endl; 69 | return {next, {}}; 70 | } 71 | 72 | static auto 73 | update(const model_type& mod, const decrease&) -> return_type { 74 | auto next = mod; 75 | next.counter -= 1; 76 | std::cout << "Decreasing counter from " << mod.counter << " to " << next.counter << std::endl; 77 | return {next, {}}; 78 | } 79 | 80 | static auto 81 | update(const model_type& mod, const user_increase& msg) -> return_type { 82 | auto next = mod; 83 | next.counter += msg.value; 84 | std::cout << "User increasing counter from " << mod.counter << " to " << next.counter << std::endl; 85 | return {next, {ask_user( )}}; 86 | } 87 | }; 88 | 89 | // Event Loop 90 | 91 | int 92 | main( ) { 93 | elm::start_eventloop({ 94 | increase {}, 95 | delayed_increase(std::chrono::milliseconds {1500}), 96 | delayed_decrease(std::chrono::milliseconds {1000}), 97 | delayed_increase(std::chrono::milliseconds {400}), 98 | ask_user( ), 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /include/elm-architecture/elm-architecture.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace elm_architecture { 10 | 11 | // A command can either be a deferred command (shared_future) or 12 | // invoked directly. 13 | template 14 | using command_type = std::variant, Msg>; 15 | 16 | // The return type for the update functions, a new model and a 17 | // list of actions to take after. 18 | template 19 | using return_type = std::tuple>>; 20 | 21 | template 22 | using view_return_type = std::vector>; 23 | 24 | // Start the eventloop with a given list of initial actions to take 25 | template 26 | auto 27 | start_eventloop(const std::vector>& init = {}) { 28 | auto model = Model {}; 29 | std::deque> pending {init.begin( ), init.end( )}; 30 | std::vector> in_progress; 31 | auto sleep_duration_us = 1024; 32 | const auto max_sleep_duration_us = sleep_duration_us * 16; 33 | 34 | while(pending.size( ) > 0 || in_progress.size( ) > 0) { 35 | // Step One: Apply all pending events and remove them 36 | while(pending.size( ) > 0) { 37 | const auto& item = pending.front( ); 38 | if(std::holds_alternative>(item)) { 39 | in_progress.push_back(std::get>(item)); 40 | } else { 41 | const Msg& msg = std::get(item); 42 | const auto update_visitor = [&model](const auto& msg) { return Update::update(model, msg); }; 43 | return_type result = std::visit(update_visitor, msg); 44 | model = std::get(result); 45 | const auto& commands = std::get>>(result); 46 | std::copy(commands.begin( ), commands.end( ), std::back_inserter(pending)); 47 | if constexpr(!std::is_same_v) { 48 | const auto view_visitor = [&model](const auto& msg) { return View::view(model, msg); }; 49 | std::vector> commands = std::visit(view_visitor, msg); 50 | std::copy(commands.begin( ), commands.end( ), std::back_inserter(pending)); 51 | } 52 | } 53 | pending.pop_front( ); 54 | } 55 | 56 | // Step Two: Process all the finished IO tasks and push their resulting 57 | // messages to the message queue 58 | const auto remove_from = std::remove_if(in_progress.begin( ), in_progress.end( ), [&](auto& future) { 59 | const auto status = future.wait_for(std::chrono::microseconds {sleep_duration_us}); 60 | if(status == std::future_status::ready) { 61 | pending.push_back(future.get( )); 62 | return true; 63 | } 64 | return false; 65 | }); 66 | const auto removed_tasks = std::distance(remove_from, in_progress.end( )); 67 | in_progress.erase(remove_from, in_progress.end( )); 68 | 69 | // Step Three: Adjust the sleep duration so that on higher loads we 70 | // sleep less (increasing throughput), and on lower loads we sleep more 71 | // (decreasing CPU load). 72 | sleep_duration_us = removed_tasks ? std::max(1, sleep_duration_us / 2) 73 | : std::min(sleep_duration_us * 2, max_sleep_duration_us); 74 | 75 | std::this_thread::yield( ); 76 | } 77 | } 78 | } // namespace elm_architecture 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elm Architecture for C++17 2 | 3 | Elm is a pure functional language for the front-end. It enforces an architecture that allows programs to stay pure in an event-based setting. 4 | 5 | This simple header-only library implements a variant of the the Elm Architecture for C++17, its small footprint and simplicity makes it easy to understand and its quick to get started. 6 | 7 | ## Features 8 | 9 | The architecture supports running commands in parallel as asynchronous tasks, aswell as in a immediate fashion. 10 | 11 | ## Example Program 12 | 13 | This example utilizes both direct and deferred action modes. It initializes by immediately increasing a conuter, then it manipulates the counter at a later time by deferring commands for later execution. To demonstrate the asynchronicity of the library the user can also enter a number in order to increase or decrease the counter. 14 | 15 | ```c++ 16 | #include "elm-architecture/elm-architecture.hpp" 17 | 18 | #include 19 | 20 | namespace elm = elm_architecture; 21 | 22 | // Model 23 | 24 | struct model_type { 25 | int counter = 0; 26 | }; 27 | 28 | // Msg 29 | 30 | struct increase {}; 31 | 32 | struct decrease {}; 33 | 34 | struct user_increase { 35 | int value; 36 | }; 37 | 38 | using message_type = std::variant; 39 | 40 | std::shared_future 41 | delayed_increase(std::chrono::milliseconds delay) { 42 | return std::async( 43 | std::launch::async, 44 | [delay]( ) -> message_type { 45 | std::this_thread::sleep_for(delay); 46 | return increase {}; 47 | }) 48 | .share( ); 49 | } 50 | 51 | std::shared_future 52 | delayed_decrease(std::chrono::milliseconds delay) { 53 | return std::async( 54 | std::launch::async, 55 | [delay]( ) -> message_type { 56 | std::this_thread::sleep_for(delay); 57 | return decrease {}; 58 | }) 59 | .share( ); 60 | } 61 | 62 | std::shared_future 63 | ask_user( ) { 64 | return std::async( 65 | std::launch::async, 66 | []( ) -> message_type { 67 | int amount = 0; 68 | std::cin >> amount; 69 | return user_increase {amount}; 70 | }) 71 | .share( ); 72 | } 73 | 74 | // Update 75 | 76 | struct update_fn { 77 | using return_type = elm::return_type; 78 | 79 | static auto 80 | update(const model_type& mod, const increase&) -> return_type { 81 | auto next = mod; 82 | next.counter += 1; 83 | std::cout << "Increasing counter from " << mod.counter << " to " << next.counter << std::endl; 84 | return {next, {}}; 85 | } 86 | 87 | static auto 88 | update(const model_type& mod, const decrease&) -> return_type { 89 | auto next = mod; 90 | next.counter -= 1; 91 | std::cout << "Decreasing counter from " << mod.counter << " to " << next.counter << std::endl; 92 | return {next, {}}; 93 | } 94 | 95 | static auto 96 | update(const model_type& mod, const user_increase& msg) -> return_type { 97 | auto next = mod; 98 | next.counter += msg.value; 99 | std::cout << "User increasing counter from " << mod.counter << " to " << next.counter << std::endl; 100 | return {next, {ask_user( )}}; 101 | } 102 | }; 103 | 104 | // Event Loop 105 | 106 | int 107 | main( ) { 108 | elm::start_eventloop({ 109 | increase {}, 110 | delayed_increase(std::chrono::milliseconds {1500}), 111 | delayed_decrease(std::chrono::milliseconds {1000}), 112 | delayed_increase(std::chrono::milliseconds {400}), 113 | ask_user( ), 114 | }); 115 | } 116 | ``` 117 | 118 | ## Usage 119 | 120 | Simply copy the header file into your project and include it, or add the whole project folder and do a `add_subdirectory(elm-architecture)` and `target_link_library(your-target elm-architecture)` in CMake. 121 | 122 | ## Credits 123 | 124 | The library is heavily inspired by [elm-architecture-haskell](https://github.com/lazamar/elm-architecture-haskell), so I encourage you to check that out aswell. 125 | --------------------------------------------------------------------------------