Project

accessory

0.0
The project is in a healthy, maintained state
Functional lenses for Ruby, borrowed from Elixir
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
 Project Readme

Accessory

See gem 'accessory' on Rubygems Yard Docs Documentation coverage

Accessory is a Ruby re-interpretation of Elixir's particular implementation of functional lenses.

  • Like Elixir, Accessory provides functions called get_in, update_in, pop_in, put_in, get_and_update_in, etc., that each take a "lens path" and traverse it.

  • Like Elixir, Accessory's lens paths are composed of "accessors" — you'll find most of the same ones from Elixir's Access module (insofar as they make sense in Ruby), and also some uniquely-Ruby accessors as well.

  • Like Elixir, these lens paths are detached from any source, and so are reusable (they can be e.g. set as module-level constants.)

  • Unlike Elixir, where lenses are plain lists and accessors are closures, Accessory's lenses and accessors are objects. The traversal functions are all methods on the Accessory::LensPath.

  • Unlike Elixir, Accessory's mutative traversals (update_in, put_in, etc.) modify the input document in place. (Non-in-place accessors are also planned.)

Installation

$ gem install accessory

Usage

require 'accessory'

LensPath

An Accessory::LensPath is a "free-floating" lens (not bound to a subject document.)

Accessory::LensPath[:foo, "bar", 0]

Accessors are classes named like Accessory::FooAccessor. You can create and use accessors directly in a LensPath:

Accessory::LensPath[Accessory::SubscriptAccessor.new(:foo)]

...or use the convenience module-functions on Accessory::Access:

include Accessory
LensPath[Access.subscript(:foo)] # equivalent to the above

Also, as an additional convenience, LensPath will wrap any raw objects (objects not descended from Accessory::Accessor) in a SubscriptAccessor. So another way to write the above is:

Accessory::LensPath[:foo]

You can define your own accessor classes by inheriting from Accessory::Accessor, and use them in a LensPath as normal.

class MyAccessor < Accessory::Accessor
  # ...
end

Accessory::LensPath[MyAccessor.new(:bar)]

Extending LensPaths

Existing LensPaths may be "extended" with additional path-components using .then:

lp = LensPath[Access.first, :foo, Access.all]
lp.then("bar") # => #LensPath[Access.first, :foo, Access.all, "bar"]

LensPath instances are created frozen; each use of .then produces a new child LensPath, rather than affecting the original. This allows you to reuse "base" LensPaths.

LensPaths may also be concatenated using +, again producing a new LensPath instance:

lp1 = LensPath[Access.first, :foo]
lp2 = LensPath[Access.all, "bar"]
lp1 + lp2 # => #LensPath[Access.first, :foo, Access.all, "bar"]

+ also allows "bald" accessors, or plain arrays of accessors:

LensPath[:foo] + :bar + [:baz, :quux] # => #LensPath[:foo, :baz, :baz, :quux]

Another name for + is /. This allows for a traveral syntax similar to Pathname instances:

LensPath.empty / :foo / :bar # => #LensPath[:foo, :bar]

Fluent API

Methods with the same names as the module-functions in Access are included in LensPath. Calling these methods has the same effect as calling the relevant module-function and passing it to .then.

These methods allow you to construct a LensPath through a chain of method-calls that closely resembles a concrete traversal of a container-object.

include Accessory

# the following are equivalent:
LensPath[Access.first, :foo, Access.all, "bar"]

LensPath.empty.first[:foo].all["bar"]

You can combine your own accessors with the fluent methods by using .then:

Accessor::LensPath.empty[:foo].first.then(MyAccessor.new)[:baz]

Lens

A LensPath may be bound to a subject document with LensPath#on to produce a Lens:

doc = {foo: 1}
doc_foo = LensPath[:foo].on(doc) # => #<Lens on={:foo=>1} [:foo]>

Alternately, you can use Lens.on(doc) to create an identity Lens:

doc = {foo: 1}
Lens.on(doc) # => #<Lens on={:foo=>1} []>

A Lens exposes the same traversal methods as a LensPath, but does not require that you pass in a document, as it already has its own:

doc_foo.get_in # => 1
doc_foo.put_in(2) # => {:foo=>2}
doc # => {:foo=>2}

A Lens also exposes all the extension methods of its LensPath. Like a LensPath, a Lens is frozen, so these methods return a new Lens wrapping a new LensPath:

doc = {}
doc_root = Lens.on(doc)     # => #<Lens on={} []>
doc_foo  = doc_root / :foo  # => #<Lens on={} [:foo]>

The .lens refinement

By using Accessory, a .lens method is added to all Objects, which has the same meaning as passing the object to Lens.on.

using Accessory
{}.lens[:foo][:bar].put_in(5) # => {:foo=>{:bar=>5}}

The .lens method also accepts additional variadic arguments, representing either a LensPath or a list of accessors to use to construct one:

using Accessory

{}.lens(:foo, :bar).put_in(5) # => {:foo=>{:bar=>5}}

foo_bar = LensPath[:foo, :bar]
{}.lens(foo_bar).put_in(3) # => {:foo=>{:bar=>3}}

Default inference for intermediate accessors

Every accessor knows how to construct a valid, empty value of the type it expects to receive as input. For example, the AllAccessor expects to operate on Enumerables, and so defines a default constructor of Array.new.

When a LensPath is created, the default constructors for each accessor are "fed back" through to their predecessor accessor. The predecessor stores the default constructor to use as a fall-back default (i.e. a default for when you didn't explicitly specify a default.)

This means that you usually don't need to specify defaults in your accessors, because sensible values are inferred from the next operation in the LensPath traversal chain.

Let's annotate a LensPath with the inferred defaults for each traversal-step:

LensPath[
  :foo,               # Array.new (FirstAccessor)
  Access.first,       # OpenStruct.new (AttributeAccessor)
  Access.attr(:name), # Hash.new (SubscriptAccessor)
  :bar                # nil (no successor)
]

Using put_in with this lens will have the result you'd expect:

doc = {}
(doc.lens / :foo / Access.first / Access.attr(:name) / :bar).put_in(1)
doc # => {:foo=>[#<OpenStruct name={:bar=>1}>]}