All Projects → jkrukoff → optic

jkrukoff / optic

Licence: Apache-2.0 license
An Erlang/OTP library for reading and updating deeply nested immutable data.

Programming Languages

erlang
1774 projects
Makefile
30231 projects
Dockerfile
14818 projects

Projects that are alternatives of or similar to optic

Setfield.jl
Update deeply nested immutable structs.
Stars: ✭ 90 (+164.71%)
Mutual labels:  immutable, lens
digraph export
File conversion and export support for graphs created using the Erlang digraph module.
Stars: ✭ 33 (-2.94%)
Mutual labels:  erlang-otp, erlang-library
Partial.lenses
Partial lenses is a comprehensive, high-performance optics library for JavaScript
Stars: ✭ 846 (+2388.24%)
Mutual labels:  immutable, lens
sockerl
Sockerl is an advanced Erlang/Elixir socket framework for TCP protocols and provides fast, useful and easy-to-use API for implementing servers, clients and client connection pools.
Stars: ✭ 26 (-23.53%)
Mutual labels:  erlang-otp, erlang-library
Optics Ts
Type-safe, ergonomic, polymorphic optics for TypeScript
Stars: ✭ 132 (+288.24%)
Mutual labels:  immutable, lens
forseti
Process balancer and distributor for Erlang/OTP
Stars: ✭ 27 (-20.59%)
Mutual labels:  erlang-otp, erlang-library
Python Lenses
A python lens library for manipulating deeply nested immutable structures
Stars: ✭ 179 (+426.47%)
Mutual labels:  immutable, lens
Accessors.jl
Update immutable data
Stars: ✭ 73 (+114.71%)
Mutual labels:  immutable, lens
boutique
Immutable data storage
Stars: ✭ 44 (+29.41%)
Mutual labels:  immutable
shoki
Purely functional data structures in Java
Stars: ✭ 30 (-11.76%)
Mutual labels:  immutable
react-antd
基于react + redux + immutable + less + ES6/7 + webpack2.0 + fetch + react-router + antd实现的SPA后台管理系统模板
Stars: ✭ 320 (+841.18%)
Mutual labels:  immutable
dot-wild
Use powerful dot notation (dot path + wildcard) to manipulate properties of JSON
Stars: ✭ 28 (-17.65%)
Mutual labels:  immutable
next-react-boilerplate
🔥 NextJS with additional tech feature like react-boilerplate. Demo >>
Stars: ✭ 20 (-41.18%)
Mutual labels:  immutable
nim-contra
Lightweight Self-Documenting Design by Contract Programming and Security Hardened mode.
Stars: ✭ 46 (+35.29%)
Mutual labels:  immutable
gelidum
Freeze your objects in python
Stars: ✭ 50 (+47.06%)
Mutual labels:  immutable
babl
JSON templating on steroids
Stars: ✭ 29 (-14.71%)
Mutual labels:  immutable
PortalView
A (potentially) cross-platform, declarative and immutable Swift library for building user interfaces
Stars: ✭ 15 (-55.88%)
Mutual labels:  immutable
relite
a redux-like library for managing state with simpler api
Stars: ✭ 60 (+76.47%)
Mutual labels:  immutable
putting-lenses-to-work
A presentation for BayHac 2017 on how I uses lenses at work
Stars: ✭ 73 (+114.71%)
Mutual labels:  lens
dobux
🍃 Lightweight responsive state management solution.
Stars: ✭ 75 (+120.59%)
Mutual labels:  immutable

optic

Camera Lenses

Overview

This is an Erlang/OTP library for retrieving and modifying nested values, in the spirit of Haskell's lens library. Functional selectors for deeply nested values are constructed by composing "optics", each of which specifies a way to focus on a particular kind of value.

For example, say we had a list of deserialized JSON entities representing pets for sale that we wanted to modify.

> Pets = [#{
    <<"id">> => 628178654,
    <<"name">> => <<"spot">>,
    <<"status">> => <<"available">>,
    <<"category">> => #{
      <<"id">> => 3216199393,
      <<"name">> => <<"dog">>
    }}].

We could then update all pets to a new status by:

> optic:put([optic_lists:all(), optic_maps:key(<<"status">>)],
            <<"sold">>,
            Pets).
{ok,[#{
  <<"id">> => 628178654,
  <<"name">> => <<"spot">>,
  <<"status">> => <<"sold">>,
  <<"category">> => #{
    <<"id">> => 3216199393,
    <<"name">> => <<"dog">>
  }}]}

Modules

optic
optic_array
optic_dict
optic_gb_sets
optic_gb_trees
optic_generic
optic_lists
optic_maps
optic_orddict
optic_ordsets
optic_path
optic_proplists
optic_sets
optic_tuples

Getting Started

This library is published to hex.pm as optic. If you're using rebar3 as your build tool, it can be added as a dependency to your rebar.config as follows:

{deps, [{optic}]}.

Usage

The library is expected to be used by first creating a list of optics which represent operations on particular kinds of containers. That list can then be used with a variety of different common operations in the library for reading or writing values in nested data structures in different ways.

Optics

An optic is an opaque record that represents a method for traversing over and modifying a particular container. Many different pre-created optics are included in the library for standard Erlang containers as well as a method for creating new optics.

The fundamental operation supported is a mapfold, though a fold may also be implemented (and is recommended) for better performance for read only operations. Additionally convenience functions for getting and putting static values are included and are built on top of the more powerful primitive operations.

Optics which are associative and idempotent are well behaved. These optics can be easily combined in any way this library supports without surprise. Optics which do not have these properties will require careful attention to how they are used and the order in which they are applied.

Most of the optics in this library support the same set of options to control how they behave. Those options are passed as a map or proplist:

  • strict: When true, this causes an error to be returned when asked to traverse over a container of an unexpected type. When false or not given, unexpected containers will be silently skipped.
  • create: When given, this causes a new container of the expected type to be created when no container is present or when asked to traverse over a container of an unexpected type. If the traversal type needs to create individual values, the value of the create property is used as a template to create the needed default objects. Setting this property causes the optic to no longer be well behaved.
  • filter: When given, expects a value as a filter function to determine if the traversed element should be focused or skipped. The filter function should take a single arbitrary element and return a boolean true/false. If the criteria the filter function uses to select an element is modified, the filtered optic will no longer be well behaved.
  • require: When given, expects a value as a filter function to determine if the traversed element can be focused. If the element can not be focused, {error, required} will be returned. The filter function should take a single arbitrary element and return a boolean true/false. If the criteria the filter function uses to select an element is modified, the filtered optic will no longer be well behaved.

Examples

Let's look at some examples of how optics can be used. We'll start with a list of maps and demonstrate various ways it can be processed:

> Data = [#{name => first}, #{name => second}, #{name => third}].

We can extract a single value using one of the lists optics to extract the head of the list, and combine it with an optic to extract the value of the "name" key from the map.

> optic:get([optic_lists:head(), optic_maps:key(name)], Data).
{ok,[first]}

This shows how optics are applied from left to right.

If we wanted to traverse over all the members of the list, we can simply change the list optic used to do so:

> optic:get([optic_lists:all(), optic_maps:key(name)], Data).
{ok,[first,second,third]}

Optics can also be composed using optic:merge/1 in order to select multiple elements at the same level. For instance, to select only the first two maps, we could do this:

> FirstTwo = optic:merge([optic_lists:nth(1), optic_lists:nth(2)]),
> optic:get([FirstTwo, optic_maps:key(name)], Data).
{ok,[first,second]}

When presented with heterogeneous data, the strict option can be used to control how it is handled. We may want to simply skip it when such is expected, or we may want to report an error. The only change is in how the optic is constructed:

> Data = [#{name => first},#{id => second},#{name => third}].
[#{name => first},#{id => second},#{name => third}].
> optic:get([optic_lists:all(), optic_maps:key(name)], Data).
{ok,[first,third]}
> optic:get([optic_lists:all(), optic_maps:key(name, #{strict => false})], Data).
{ok,[first,third]}
> optic:get([optic_lists:all(), optic_maps:key(name, #{strict => true})], Data).
{error,undefined}

We can see here that we get lax handling of unexpected types by default.

The create option is closely related, as creation is only triggered in cases where strict would have returned an error. In that case a container is instead either constructed or modified to allow the operation to succeed.

Let's explore the variations in the context of modifying a container:

> Data = [#{name => first},#{id => second},#{name => third}].
[#{name => first},#{id => second},#{name => third}].
> optic:put([optic_lists:all(), optic_maps:key(name)], example, Data).
{ok,[#{name => example},#{id => second},#{name => example}]}
> optic:put([optic_lists:all(), optic_maps:key(name, #{strict=>true})], example, Data).
{error,undefined}
> optic:put([optic_lists:all(), optic_maps:key(name, #{create=>undefined})], example, Data).
{ok,[#{name => example},
     #{id => second,name => example},
     #{name => example}]}

The value given to create is used as a template when additional values need to be constructed. In many cases, it is then immediately overwritten. The behaviour is context dependent and will vary based on the optic used.

> optic:put([optic_lists:nth(3, #{create => #{}}),
             optic_maps:key(name, #{create => undefined})], example, []).
{ok,[#{},#{},#{name => example}]}

Finally, the filter option can be used to control which elements are selected based on their values. For instance, say we wanted to only collect the status of maps with a particular name:

> Data = [#{name => first, status => ready},
          #{name => second, status => ready},
          #{name => third, status => delayed}].
[#{name => first,status => ready},
 #{name => second,status => ready},
 #{name => third,status => delayed}]
> optic:get([optic_lists:all([{filter,
                               fun (Elem) -> maps:get(name, Elem) == third end}]),
             optic_maps:key(status)], Data).
{ok,[delayed]}

More complicated operations should refer to the fold/4, map/3 and mapfold/4 functions to allow for values to be computed during the traversal.

Paths

For traversing JSON like structures of maps and lists, the optic_path module provides a simplified interface for optic construction. It parses a list of path components into a list of optics that can be used with any of the optic traversal functions.

> Data = #{<<"result">> => [#{<<"name">> => <<"example">>}]},
> Path = optic_path:new([<<"result">>, '*', <<"name">>]),
> optic:get(Path, Data).
{ok,[<<"example">>]}
> optic:put(Path, <<"modified">>, Data).
{ok,#{<<"result">> => [#{<<"name">> => <<"modified">>}]}}

Notable restrictions for paths are they require map-like keys to be strings or binaries. No provision is made for the usual optic options, instead traversal is always lax and creation is never done.

Contributing

Please fork the repo and submit a PR. Tests are run via:

rebar3 eunit

Documentation is autogenerated using edown and edoc via:

rebar3 as markdown edoc

The application has only been tested with Erlang/OTP 21 on Windows 10. Reports of success (or failure!) on other versions and operating systems are appreciated.

Lineage

I first encountered lenses via the Datum library, but wanted a version that supported traversables instead of lenses in order to support updating multiple elements. As lenses are a special case of traversables, this allowed for using a single type to represent both.

There are a number of good introductions to lenses, this was the most accessible for me.

This library was initially conceived with the intention of making it easy to modify deeply nested JSON, so JSON related data structures were the first implemented.

Attribution

Image by Bill Ebbesen

CC BY-SA 3.0 https://creativecommons.org/licenses/by/3.0/deed.en

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].