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.)
$ gem install accessory
require 'accessory'
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)]
Existing LensPath
s 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" LensPath
s.
LensPath
s 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]
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]
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]>
By using Accessory
, a .lens
method is added to all Object
s, 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}}
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}>]}