{"id":24977704,"url":"https://github.com/adzz/maybex","last_synced_at":"2025-08-12T13:06:41.858Z","repository":{"id":48289669,"uuid":"113489997","full_name":"Adzz/maybex","owner":"Adzz","description":"An Implementation of the Maybe monad in Elixir","archived":false,"fork":false,"pushed_at":"2021-08-03T05:40:25.000Z","size":47,"stargazers_count":5,"open_issues_count":4,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-16T09:04:53.048Z","etag":null,"topics":["elixir","functional-programming","maybe","monad","monads","protocols"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Adzz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-12-07T19:26:24.000Z","updated_at":"2021-03-05T22:20:19.000Z","dependencies_parsed_at":"2022-07-24T23:46:27.605Z","dependency_job_id":null,"html_url":"https://github.com/Adzz/maybex","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Adzz/maybex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adzz%2Fmaybex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adzz%2Fmaybex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adzz%2Fmaybex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adzz%2Fmaybex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Adzz","download_url":"https://codeload.github.com/Adzz/maybex/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adzz%2Fmaybex/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270065429,"owners_count":24520946,"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-08-12T02:00:09.011Z","response_time":80,"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":["elixir","functional-programming","maybe","monad","monads","protocols"],"created_at":"2025-02-03T23:08:48.130Z","updated_at":"2025-08-12T13:06:41.806Z","avatar_url":"https://github.com/Adzz.png","language":"Elixir","readme":"# Maybex\n\nThis is a pragmatic implementation of the Maybe monad. It allows you to pipe together functions with abandon, even if they return error values.\n\n### Why would I use it?\n\n\u003cdetails\u003e\n  \u003csummary\u003eLet's look at a completely contrived example.\u003c/summary\u003e\nImagine you get some data and you want to turn it to json then save somewhere:\n\n```elixir\n{:ok, %{valid?: true, data: \"DATA!\"}}\n|\u003e turn_into_json()\n|\u003e save_to_the_db()\n```\n\nLet's say they are implemented like this:\n\n```elixir\ndef turn_into_json(%{valid?: false}), do: {:error, \"Nope\"}\ndef turn_into_json(data), do: {:ok, Jason.encode!(data)}\n\ndef save_to_the_db(json), do: DB.save(json)\n```\n\nNotice the problem? The return from `turn_into_json` doesn't match what `save_to_the_db` expects. So we have two options.\n\n  1. define `save_to_the_db` such that it can handle an okay / error tuple.\n  2. use  `with`.\n\nThe first approach would look like this:\n\n```elixir\ndef save_to_the_db({:ok, json}), do: DB.save(json)\ndef save_to_the_db({:error, json}), do: {:error, json}\ndef save_to_the_db(json), do: DB.save(json)\n```\n\nThere are lots of reasons why it feels wrong. It's not the concern of `save_to_the_db` what `turn_into_json` returns. If `turn_into_json` changes we shouldn't have to also change `save_to_the_db`, so if we do 1. we've introduced coupling that we do not want. Worse than that if we add more functions in between `save_to_the_db` and `turn_into_json` they would also all have to handle an okay / error tuple, which adds overhead. `save_to_the_db` can't handle all of the possible inputs it might get and it shouldn't. In elixir this is easy to do because of pattern matching so is often tempting, but should be avoided.\n\nOption 2 looks like this:\n\n```elixir\ndata = {:ok, %{valid?: true, data: \"DATA!\"}}\n\nwith {:ok, next} \u003c- turn_into_json(data) do\n  save_to_the_db(next)\nelse\n  {:error, \"Nope\"} -\u003e {:error, \"Nope\"}\nend\n```\n\nThat's much more reasonable, but even this can get unwieldy quickly. If we add more functions, we have to handle them each in the `else` clause, some may return error tuples, some may return nil:\n\n```elixir\ndata = {:ok, %{valid?: true, data: \"DATA!\"}}\n\nwith {:ok, next} \u003c- turn_into_json(data),\n  result \u003c- spin_it_around_a_bit(next),\n  x when not is_nil(x) \u003c- nullable_fun(result) do\n  save_to_the_db(x)\nelse\n  nil -\u003e {:error, \"Nope\"}\n  {:error, \"Nope\"} -\u003e {:error, \"Nope\"}\nend\n```\nWhich again may be fine in small doses, but Maybex offers an alternative:\n\n```elixir\n{:ok, %{valid?: true, data: \"DATA!\"}}\n|\u003e Maybe.map(\u0026turn_into_json/1)\n|\u003e Maybe.map(\u0026save_to_the_db/1)\n```\n\nOr even:\n\n```elixir\nimport Maybe.Pipe\n\n{:ok, %{valid?: true, data: \"DATA!\"}}\n~\u003e \u0026turn_into_json/1\n~\u003e \u0026save_to_the_db/1\n```\n\n\u003c/details\u003e\n\n### How would I use it?\n\u003cdetails\u003e\n\u003csummary\u003eHere's how it works.\u003c/summary\u003e\n\nGenerally there are two types of things, there are error things and non error things. You can define for yourself what specifically counts as an error, and what isn't, but Maybex provides a few for you. We define the following:\n\n| Error              |   Non Error      |\n| -------------------|------------------|\n| `{:error, _}`      | `{:ok, _}`       |\n| `%Maybe.Error{value: _}` | `%Maybe.Ok{value: _}`  |\n\n\nIf we pass `{:ok, thing}` into `Maybe.map/2` we will pass `thing` into the mapping function, and return that result wrapped in an okay tuple. If we map over an `{:error, thing}` we wont do anything, and will just return the error tuple:\n\n```elixir\niex\u003e {:ok, 10} |\u003e Maybe.map(fn x -\u003e x * 10 end)\n{:ok, 100}\n\niex\u003e {:error, 10} |\u003e Maybe.map(fn x -\u003e x * 10 end)\n{:error, 10}\n\niex\u003e {:ok, 10}\n...\u003e |\u003e Maybe.map(fn x -\u003e x * 10 end)\n...\u003e |\u003e Maybe.map(fn _x -\u003e {:error, \"Nope!\"} end)\n...\u003e |\u003e Maybe.map(fn x -\u003e x * 10 end)\n{:error, \"Nope!\"}\n\niex\u003e %Maybe.Ok{value: 10} |\u003e Maybe.map(fn x -\u003e x * 10 end)\n%Maybe.Ok{value: 100}\n\niex\u003e %Maybe.Error{value: 10} |\u003e Maybe.map(fn x -\u003e x * 10 end)\n%Maybe.Error{value: 10}\n\niex\u003e %Maybe.Ok{value: 10}\n...\u003e |\u003e Maybe.map(fn x -\u003e x * 10 end)\n...\u003e |\u003e Maybe.map(fn _x -\u003e %Maybe.Error{value: \"Nope!\"} end)\n...\u003e |\u003e Maybe.map(fn x -\u003e x * 10 end)\n%Maybe.Error{value: \"Nope!\"}\n\niex\u003e Maybe.unwrap(%Maybe.Ok{value: 10})\n10\n\niex\u003e Maybe.unwrap(%Maybe.Error{value: 10})\n10\n\niex\u003e Maybe.map_error(%Maybe.Error{value: 10}, fn x -\u003e x * 10 end)\n%Maybe.Error{value: 100}\n```\n\nThere is also an infix version of the `map` function which looks like this `~\u003e`\n\n```elixir\nimport Maybe.Pipe\n\niex\u003e {:ok, 10} ~\u003e fn x -\u003e x * 10 end\n{:ok, 100}\n\niex\u003e {:error, 10} ~\u003e fn x -\u003e x * 10 end\n{:error, 10}\n\niex\u003e {:ok, 10}\n...\u003e ~\u003e fn x -\u003e x * 10 end\n...\u003e ~\u003e fn _x -\u003e {:error, \"Nope!\"} end\n...\u003e ~\u003e fn x -\u003e x * 10 end\n{:error, \"Nope!\"}\n\n```\n\u003c/details\u003e\n\n### Implementing your own Maybe Type\n\n\u003cdetails\u003e\n  \u003csummary\u003eBecause Maybex is implemented with protocols you can extend it by implementing Maybe for your own data type.\u003c/summary\u003e\nLets do it for an Ecto.Changeset:\n\n```elixir\n\ndefmodule Test do\n  use Ecto.Schema\n\n  embedded_schema do\n    field(:thing, :integer)\n  end\nend\n\ndefimpl Maybe, for: Ecto.Changeset do\n  def map(changeset = %{valid?: true}, fun), do: fun.(changeset)\n  def map(changeset, _), do: changeset\n\n  def map_error(changeset = %{valid?: true}, _), do: changeset\n  def map_error(changeset, fun), do: fun.(changeset)\n\n  def unwrap!(changeset), do: Ecto.Changeset.apply_action!(changeset, :unwrap)\n\n  def unwrap(changeset) do\n    with {:ok, ch} \u003c- Ecto.Changeset.apply_action(changeset, :unwrap) do\n      ch\n    else\n      {:error, ch} -\u003e ch\n    end\n  end\n\n  def unwrap_or_else(changeset = %{valid?: true}, _), do: changeset\n  def unwrap_or_else(changeset, fun), do: fun.(changeset)\n\n  def is_error?(%{valid?: true}), do: false\n  def is_error?(%{valid?: _}), do: true\n\n  def is_ok?(%{valid?: true}), do: true\n  def is_ok?(%{valid?: _}), do: false\nend\n```\n```sh\niex\u003e %Test{} |\u003e Ecto.Changeset.cast(%{thing: \"1\"}, [:thing]) |\u003e Maybe.map_error(fn ch -\u003e\n  Logger.warn(fn -\u003e \"Insert failed: #{inspect(ch)}\" end)\nend)\n#Ecto.Changeset\u003c\n  action: nil,\n  changes: %{thing: 1},\n  errors: [],\n  data: #Test\u003c\u003e,\n  valid?: true\n\u003e\n\niex\u003e %Test{} |\u003e Ecto.Changeset.cast(%{thing: false}, [:thing]) |\u003e Maybe.map_error(fn ch -\u003e\n  Logger.warn(fn -\u003e \"Insert failed: #{inspect(ch)}\" end)\nend)\n[warn]  Insert failed: #Ecto.Changeset\u003caction: nil, changes: %{}, errors: [thing: {\"is invalid\", [type: :integer, validation: :cast]}], data: #Test\u003c\u003e, valid?: false\u003e\n```\n\u003c/details\u003e\n\n### The Maybe functions\n\nThe Maybe protocol exposes several functions to help working with optional values. Check the docs but here are some more examples:\n\n```elixir\niex\u003e Maybe.unwrap({:ok, 10})\n10\n\niex\u003e Maybe.unwrap!({:ok, 10})\n10\n\niex\u003e Maybe.unwrap({:error, 10})\n10\n\niex\u003e Maybe.unwrap!({:error, 10})\n(RuntimeError) Error: 10\n\niex\u003e Maybe.map_error({:error, 10}, fn x -\u003e x * 10 end)\n{:error, 100}\n\niex\u003e Maybe.map_error({:ok, 10}, fn x -\u003e x * 10 end)\n{:ok, 10}\n\niex\u003e Maybe.unwrap_or_else({:ok, 10}, fn x -\u003e x * 10 end)\n10\n\niex\u003e Maybe.unwrap_or_else({:error, 10}, fn x -\u003e x * 10 end)\n100\n\niex\u003e {:ok, 10} ~\u003e fn x -\u003e x * 10 end |\u003e Maybe.unwrap()\n100\n```\n\nThere are a list of functions that behave similarly check [the docs](https://hexdocs.pm/maybex) for more thorough examples.\n\n\n## Installation\n\nThis is [available in Hex](https://hex.pm/packages/maybex), the package can be installed\nby adding `maybex` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:maybex, \"~\u003e 1.0.0\"}\n  ]\nend\n```\n\nDocumentation can be found at [https://hexdocs.pm/maybex](https://hexdocs.pm/maybex).\n\n## Tests\n\nTo run tests, at the root of the project run `mix test`\n\n## Contributing\n\nPull requests and issues are welcome!\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadzz%2Fmaybex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadzz%2Fmaybex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadzz%2Fmaybex/lists"}