{"id":25195023,"url":"https://github.com/rvarago/absent","last_synced_at":"2025-07-30T12:33:01.112Z","repository":{"id":56638425,"uuid":"186154060","full_name":"rvarago/absent","owner":"rvarago","description":"A small C++17 library meant to simplify the composition of nullable types in a generic, type-safe, and declarative way.","archived":false,"fork":false,"pushed_at":"2023-01-04T16:47:52.000Z","size":284,"stargazers_count":43,"open_issues_count":4,"forks_count":8,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-05-08T02:40:20.445Z","etag":null,"topics":["cpp","cpp-library","cpp17","declarative-programming","functional-programming","modern-cpp","monad-library","monadic-interface","nullable-type","optional-chaining","optional-type"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rvarago.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-05-11T16:06:33.000Z","updated_at":"2025-04-18T19:29:22.000Z","dependencies_parsed_at":"2023-02-02T19:01:05.458Z","dependency_job_id":null,"html_url":"https://github.com/rvarago/absent","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/rvarago/absent","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rvarago%2Fabsent","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rvarago%2Fabsent/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rvarago%2Fabsent/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rvarago%2Fabsent/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rvarago","download_url":"https://codeload.github.com/rvarago/absent/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rvarago%2Fabsent/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267867803,"owners_count":24157357,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-30T02:00:09.044Z","response_time":70,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cpp","cpp-library","cpp17","declarative-programming","functional-programming","modern-cpp","monad-library","monadic-interface","nullable-type","optional-chaining","optional-type"],"created_at":"2025-02-10T00:40:50.685Z","updated_at":"2025-07-30T12:33:01.073Z","avatar_url":"https://github.com/rvarago.png","language":"C++","readme":"# absent\n\n[![Build Status](https://travis-ci.org/rvarago/absent.svg?branch=master)](https://travis-ci.org/rvarago/absent)\n\n[![Build Status](https://dev.azure.com/rvarago/absent/_apis/build/status/rvarago.absent?branchName=master)](https://dev.azure.com/rvarago/absent/_build/latest?definitionId=1\u0026branchName=master)\n\n_absent_ is a C++17 small header-only library meant to simplify the functional composition of operations on nullable (i.e. optional-like) types\nused to represent computations that may fail.\n\n## Description\n\nHandling nullable types has always been forcing us to write a significant amount of boilerplate and sometimes it even obfuscates\nthe business logic that we are trying to express in our code.\n\nConsider the following API that uses `std::optional\u003cA\u003e` as a nullable type to represent computations that may fail:\n\n```Cpp\nstd::optional\u003cperson\u003e find_person() const;\nstd::optional\u003caddress\u003e find_address(person const\u0026) const;\nzip_code get_zip_code(address const\u0026) const;\n```\n\nA fairly common pattern in C++ would then be:\n\n```Cpp\nstd::optional\u003cperson\u003e person_opt = find_person();\nif (!person_opt) return;\n\nstd::optional\u003caddress\u003e address_opt = find_address(person_opt.value());\nif (!address_opt) return;\n\nzip_code code = get_zip_code(address_opt.value());\n```\n\nWe have mixed business logic with error-handling, and it'd be nice to have these two concerns more clearly separated from each other.\n\nFurthermore, we had to make several calls to `std::optional\u003cT\u003e` accessor `value()`. And for each call,\nwe had to make sure we’d checked that the `std::optional\u003cT\u003e` at hand was not empty before accessing its value.\nOtherwise, it would've triggered a `bad_optional_access`.\n\nThus, it’d be better to minimize the number of direct calls to `value()` by\nwrapping intermediary calls inside a function that checks for emptiness and then accesses the value.\nHence, we would only make a direct call to `value()` from our application at the very end of the chain of operations.\n\nNow, compare that against the code that does not make use of nullable types at all:\n\n```Cpp\nzip_code code = get_zip_code(find_address(find_person()));\n```\n\nThat is possibly simpler to read and therefore to understand.\n\nFurthermore, we can leverage function composition to reduce the pipeline of function applications:\n\n```\n(void -\u003e person) compose (person -\u003e address) compose (address -\u003e zip_code)\n```\n\nWhere _compose_ means the usual function composition, which applies the first function and then feeds its result into the second function:\n\n```\nf: A -\u003e B, g: B -\u003e C =\u003e (f compose g): A -\u003e C = g(f(x)), forall x in A\n```\n\nSince the types compose (source and target types match), we can reduce the pipeline of functions into a function composition:\n\n```\n(void -\u003e zip_code)\n```\n\nHowever, for nullable types we can't do the same:\n\n```\n(void -\u003e optional\u003cperson\u003e) compose (person -\u003e optional\u003caddress\u003e) compose (address -\u003e zip_code)\n```\n\nThis chain of expression can't be composed or reduced, because the types don't match anymore, so _compose_ isn't powerful enough to be used here. We can't\nsimply feed an `std::optional\u003cperson\u003e` into a function that expects a `person`.\n\nSo, in essence, the problem lies in the observation that nullable types break our ability to compose functions using the\nusual function composition operator.\n\nWe want to have a way to combine both:\n\n* Type-safety brought by nullable types.\n* Expressiveness achieved by composing simple functions as we can do for non-nullable types.\n\n### Composition with _absent_\n\nInspired by Haskell, _absent_ provides building-blocks based on functional programming to help us to compose computations that may fail.\n\nIt abstracts away some details of an \"error-as-value\" API by encapsulating common patterns into a small set of\nhigher-order functions that encapsulates repetitive pieces of logic. Therefore, it aims to reduce the syntactic noise that arises from the composition of nullable types and increase safety.\n\nIt worth mentioning that _absent_ does **NOT** provide any implementation of nullable types.\nIt rather tries to be generic and leverage existing implementations:\n\n\u003e Up to some extent, _absent_ is agnostic regarding the concrete implementation of a nullable type that one may use,\nas long as it adheres to the **concept** of a nullable type expected by the library.\n\nThe main example of a nullable type that models this concept is: `std::optional\u003cT\u003e`, which may get a [monadic interface](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0798r3.html) in the future.\n\nMeanwhile, _absent_ may be used to fill the gap. And even after, since it brings different utilities and it's also generic regarding the concrete nullable type implementation,\nalso working for optional-like types other than `std::optional\u003cT\u003e`.\n\nFor instance, a function may fail due to several reasons and you might want to provide more information to explain why a\nparticular function call has failed. Perhaps by returning not an `std::optional\u003cA\u003e`, but rather a `types::either\u003cA, E\u003e`. Where `types::either\u003cA, E\u003e` is an alias for `std::variant\u003cA, E\u003e`,\nand, by convention, `E` represents an error. `types::either\u003cA, E\u003e` is provided by _absent_ and it supports a whole set\nof combinators.\n\n### Getting started\n\n_absent_ is packaged as a header-only library and, once installed, to get started with it you simply have to include the\nrelevant headers.\n\n### Rewriting the person/address/zip_code example using absent\n\nUsing a prefix notation, we can rewrite the *zip_code* example using _absent_ as:\n\n```Cpp\nstd::optional\u003czip_code\u003e code_opt = transform(and_then(find_person(), find_address), get_zip_code);\n````\n\nAnd that solves the initial problem of lack of compositionality for nullable types.\n\nNow we express the pipeline as:\n\n```\n(void -\u003e optional\u003cperson\u003e) and_then (person -\u003e optional\u003caddress\u003e) transform (address -\u003e zip_code)\n```\n\nAnd that's _functionally_ equivalent to:\n\n```\n(void -\u003e optional\u003czip_code\u003e)\n```\n\nFor convenience, an alternative infix notation based on operator overloading is also available:\n\n```Cpp\nstd::optional\u003czip_code\u003e code_opt = find_person() \u003e\u003e find_address | get_zip_code;\n````\n\nWhich is closer to the notation used to express the pipeline:\n\n```\n(void -\u003e optional\u003cperson\u003e) \u003e\u003e (person -\u003e optional\u003caddress\u003e) | (address -\u003e zip_code)\n```\n\nHopefully, it's almost as easy to read as the version without using nullable types and with the expressiveness and type-safety\nthat we wanted to achieve.\n\n## Combinators\n\n* [`transform`](#transform)\n* [`and_then`](#and_then)\n* [`eval`](#eval)\n* [`attempt`](#attempt)\n* [`for_each`](#for_each)\n* [`from_variant`](#from_variant)\n\n### \u003cA name=\"transform\"/\u003e`transform`\n\n`transform` is used when we want to apply a function to a value that is wrapped in a nullable type if such nullable\nisn't empty.\n\n\u003e  Given a nullable _N\u0026lt;A\u0026gt;_ and a function _f: A -\u003e B_, `transform` uses _f_ to map over _N\u0026lt;A\u0026gt;_, yielding another nullable\n_N\u0026lt;B\u0026gt;_. If the input nullable is empty, `transform` does nothing, and simply returns a brand new empty nullable _N\u0026lt;B\u0026gt;_.\n\nExample:\n\n```Cpp\nauto int2str = [](auto x){ return std::to_string(x); };\n\nstd::optional\u003cint\u003e one{1};\nstd::optional\u003cstd::string\u003e one_str = transform(one, int2str); // std::optional{\"1\"}\n\nstd::optional\u003cint\u003e none = std::nullopt;\nstd::optional\u003cstd::string\u003e none_str = transform(none, int2str); // std::nullopt\n```\n\nTo simplify the act of chaining multiple operations, an infix notation of `transform` is provided by `operator|`:\n\n```Cpp\nauto int2str = [](auto x){ return std::to_string(x); };\n\nstd::optional\u003cint\u003e one{1};\nstd::optional\u003cstd::string\u003e one_str = one | int2str; // std::optional{\"1\"}\n```\n\n### \u003cA name=\"and_then\"/\u003e`and_then`\n\n`and_then` allows the application of functions that themselves return nullable types.\n\n\u003e Given a nullable _N\u0026lt;A\u0026gt;_ and a function _f: A -\u003e N\u0026lt;B\u0026gt;_, `and_then` uses _f_ to map over _N\u0026lt;A\u0026gt;_, yielding another nullable\n_N\u0026lt;B\u0026gt;_.\n\nThe main difference if compared to `transform` is that if you apply _f_ using `transform` you end up with _N\u0026lt;N\u0026lt;B\u0026gt;\u0026gt;_\nthat would need to be flattened.\nWhereas `and_then` knows how to flatten _N\u0026lt;N\u0026lt;B\u0026gt;\u003e_ into _N\u0026lt;B\u0026gt;_ after the function _f_ has been applied.\n\nSuppose a scenario where you invoke a function that may fail and you use an empty nullable type to represent such failure.\nAnd then you use the value inside the obtained nullable as the input of another function that itself may fail with an empty nullable.\nThat's where `and_then` comes in handy.\n\nExample:\n\n```Cpp\nauto int2str_opt = [](auto x){ return std::optional{std::to_string(x)}; };\n\nstd::optional\u003cint\u003e one{1};\nstd::optional\u003cstd::string\u003e one_str = and_then(one, int2str_opt); // std::optional{\"1\"}\n\nstd::optional\u003cint\u003e none = std::nullopt;\nstd::optional\u003cstd::string\u003e none_str = and_then(none, int2str_opt); // std::nullopt\n```\n\nTo simplify the act of chaining multiple operations, an infix notation of `and_then` is provided by `operator\u003e\u003e`:\n\n```Cpp\nauto int2str_opt = [](auto x){ return std::optional{std::to_string(x)}; };\n\nstd::optional\u003cint\u003e one{1};\nstd::optional\u003cstd::string\u003e one_str = one \u003e\u003e int2str_opt; // std::optional{\"1\"}\n```\n\n### \u003cA name=\"eval\"/\u003e`eval`\n\n`eval` returns the wrapped value inside a nullable if present or evaluates the\nfallback function and returns its result in case the nullable is empty. Thus, it provides a \"lazy variant\" of `std::optional\u003cT\u003e::value_or`.\n\n\u003e Given a nullable _N\u0026lt;A\u0026gt;_ and a function _f: void -\u003e A_, `eval` returns the un-wrapped _A_ inside _N\u0026lt;A\u0026gt;_ if it's not empty,\nor evaluates _f_ that returns a fallback, or default, instance for _A_.\n\nHere, lazy roughly means that the evaluation of the fallback is deferred to point when it must happen,\nwhich is: inside `eval` when the nullable is, in fact, empty.\n\nTherefore, it avoids wasting computations as it happens with `std::optional\u003cT\u003e::value_or`, where, the function\nargument is evaluated *before* reaching `std::optional\u003cT\u003e::value_or`, even if the nullable is not empty, in which case\nthe value is simply discarded.\n\nMaybe even more seriously case is when the fallback triggers side-effects that would only make sense when the nullable is indeed empty.\n\nExample:\n\n```Cpp\nrole get_default_role();\n\nstd::optional\u003crole\u003e role_opt = find_role();\nrole my_role = eval(role_opt, get_default_role);\n```\n\n### \u003cA name=\"attempt\"/\u003e`attempt`\n\nSometimes we have to interface nullable types with code that throws exceptions, for instance, by wrapping exceptions into empty nullable\ntypes. This can be done with `attempt`.\n\nExample:\n\n```Cpp\nint may_throw_an_exception();\n\nstd::optional\u003cint\u003e result = attempt\u003cstd::optional, std::logic_error\u003e(may_throw_an_exception);\n```\n\n`may_throw_an_exception` returns either a value of type `int`, and then `result` will be an `std::optional\u003cint\u003e`\nthat wraps the returned value, or it throws an exception derived from `std::logic_error`, and then `result` will be an empty `std::optional\u003cint\u003e`.\n\n### \u003cA name=\"for_each\"/\u003e`for_each`\n\n`for_each` allows running a function that does not return any value, but only executes an \naction when supplied with a value, where such value is wrapped in a nullable type.\n\nSince the action does not return anything meaningful, it's only executed because of\nits side-effect, e.g. logging a message to the console, saving an entity in the database, etc.\n\n\u003e Given a nullable _N\u0026lt;A\u0026gt;_ and a function _f: A -\u003e void_, `for_each` executes _f_  providing _A_ from _N\u0026lt;A\u0026gt;_ as the argument to _f_.\nIf _N\u0026lt;A\u0026gt;_ is empty, then `for_each` does nothing.\n\nExample:\n\n```Cpp\nvoid log(event const\u0026) const;\n\nstd::optional\u003cevent\u003e event_opt = get_last_event();\nfor_each(event_opt, log);\n```\n\n### \u003cA name=\"from_variant\"/\u003e`from_variant`\n\n`from_variant` allows us to go from an `std::variant\u003cAs...\u003e` to a \"simpler\" nullable type, such as `std::optional\u003cA\u003e`, holding a value\nof type `A` if the variant holds a value of such type, or empty otherwise.\n\n```Cpp\nstd::variant\u003cint, std::string\u003e int_or_str = 1;\nstd::optional\u003cint\u003e int_opt = from_variant\u003cint\u003e(int_or_str); // std::optional{1}\n\nint_or_str = std::string{\"42\"}\nstd::optional\u003cint\u003e int_opt = from_variant\u003cint\u003e(int_or_str); // std::nullopt\n```\n\n## Multiple error-handling\n\nOne way to do multiple error-handling is by threading a sequence of\ncomputations that return `std::optional\u003cT\u003e` to represent success or failure,\nand the chain of computations should stop as soon as the first one returns an empty\n`std::optional`, meaning that it failed. For instance:\n\n```Cpp\nstd::optional\u003cblank\u003e first();\nstd::optional\u003cblank\u003e second();\n\nauto const ok = first() \u003e\u003e sink(second);\n\nif (ok) {\n    // handle success\n}\nelse {\n    // handle failure\n}\n```\n\nWhere:\n\n* `support::blank` is a type that conveys the idea of a _unit_, i.e. it can have only one possible value.\n* `support::sink` wraps a callable that should receive parameters in another callable, but discards the whatever arguments\nit receives.\n\nIt's also possible to raise the level of abstraction by using the alias `support::execution_status` for\n`std::optional\u003cblank\u003e`, as well as the compile-time constants of type `execution_status`:\n\n* `success` for an `execution_status` filled with a `unit`.\n* `failure` for an `execution_status` filled with a `std::nullopt`.\n\nFor example:\n\n```Cpp\nexecution_status first() {\n    // ...\n    return success;\n}\n\nexecution_status second() {\n    // ...\n    return failure;\n}\n\nauto const ok = first() \u003e\u003e sink(second);\n\nif (ok) {\n    // handle success\n}\nelse {\n    // handle failure\n}\n```\n\n## Obvious drawbacks\n\n1. Abuse of operator-overloading: We give different meanings to some operators, e.g. `operator\u003e\u003e` means `and_then`, instead of extracting from an input stream.\n2. Lack of interface coherence: We may overload operators (e.g. `operator\u003e\u003e`) for types that we don't own (e.g. `std::optional\u003cT\u003e`), and therefore code may break if the true owner of a given type happens to define the operator in the future.\n\n## Requirements\n\n### Mandatory\n\n* C++17\n\n### Optional\n\n* CMake\n* Make\n* Conan\n* Docker\n\n## Build\n\nThe _Makefile_ conveniently wraps the commands to fetch the dependencies need to compile the tests using Conan, invoke\nCMake to build, execute the tests, etc.\n\n* Compile:\n\n```\nmake # BUILD_TESTS=OFF to skip tests\n```\n\n* To run the tests:\n\n```\nmake test\n```\n\n### Build inside a Docker container\n\nOptionally, it's also possible to build and run the tests inside a Docker container by executing:\n\n```\nmake env-test\n```\n\n## Installing on the system\n\nTo install _absent_:\n\n```\nmake install\n```\n\nThis will install _absent_ into _${CMAKE_INSTALL_PREFIX}/include/absent_ and make it available into your CMake local package\nrepository.\n\nThen, it's possible to import _absent_ into external CMake projects, say in a target _myExample_, by adding the\nfollowing commands into the _CMakeLists.txt_:\n\n```\nfind_package(absent REQUIRED)\ntarget_link_libraries(myExample rvarago::absent)\n```\n\n## Package managers\n\n_absent_ is also integrated into the following package managers:\n\n1. [Conan](https://github.com/conan-io/conan-center-index/tree/master/recipes/absent)\n2. [Vcpkg](https://github.com/microsoft/vcpkg/tree/master/ports/absent)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frvarago%2Fabsent","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frvarago%2Fabsent","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frvarago%2Fabsent/lists"}