NAME

Data::Focus - generic getter/setter/traverser for complex data structures

SYNOPSIS

use Data::Focus qw(focus);

my $target = [
    "hoge",
    {
        foo => "bar",
        quux => ["x", "y", "z"]
    }
];

my $z = focus($target)->get(1, "quux", 2);
my @xyz = focus($target)->list(1, "quux", [0,1,2]);
## $z: "z"
## @xyz: qw(x y z)

focus($target)->set(1, "foo",  10);
focus($target)->set(1, "quux", 11);
## $target: ["hoge", {foo => 10, quux => 11}]

focus($target)->over(1, ["foo", "quux"], sub { $_[0] * $_[0] });
## $target: ["hoge", {foo => 100, quux => 121}]

DESCRIPTION

tl;dr: This is a port of Haskell's lens-family-core package.

Data::Focus provides a way to access data elements in a deep, complex and nested data structure. So it's just a complicated version of Data::Diver, but Data::Focus has the following notable features.

  • It provides a generic way to access any type of objects as long as they have appropriate "lenses". It's like DBI of accessing nested data structures.

  • It makes it easy to update immutable objects. Strictly speaking, that means creating partially modified copies of immutable objects.

Concept

Data::Focus focuses on some parts of a complex data structure. The complex data is called the target. The parts it focuses on are called focal points. With Data::Focus, you can get/set/modify the data at the focal points within the target.

Data::Focus uses objects called lenses to focus on data parts. Lenses are like DBD::* modules in DBI framework. They know how to focus on the data parts in the target. Different lenses are used to focus into different types of targets.

For example, consider the following code.

my $target = ["hoge", { foo => "bar" }];
my $part = $target->[1]{foo};
$target->[1]{foo} = "buzz";

In Perl, we can access the data part ("bar") in the $target by the subscripts ->[1]{foo}. A lens's job is exactly what ->[1]{foo} does here.

With Data::Focus we can rewrite the above example to:

use Data::Focus qw(focus);
use Data::Focus::Lens::HashArray::Index;

my $target = ["hoge", { foo => "bar" }];
my $lens_1   = Data::Focus::Lens::HashArray::Index->new(index => 1);
my $lens_foo = Data::Focus::Lens::HashArray::Index->new(index => "foo");
my $part = focus($target)->get($lens_1, $lens_foo);
focus($target)->set($lens_1, $lens_foo, "buzz");

(I'm sure you don't wanna write this amount of code just to access an element in the $target. Don't worry. I'll shorten them below.)

Anyway, the point is, focus() function wraps the $target in a Data::Focus object, and methods of the Data::Focus object use lenses to access data parts at the focal points.

Lenses

Every lens is a subclass of Data::Focus::Lens class. Lenses included in this distribution are:

Data::Focus::Lens::HashArray::Index

Index access to a hash/array. It's like $hash->{$i}, $array->[$i], @{$hash}{$i1, $i2}, @{$array}[$i1, $i2].

Data::Focus::Lens::HashArray::All

Access all values in a hash/array. It's like values(%$hash), @$array.

Data::Focus::Lens::HashArray::Recurse

Recursively traverse all values in a tree of hashes and arrays.

Data::Focus::Lens::Accessor

Call an accessor method of a blessed object. It's like $obj->method.

Data::Focus::Lens::Composite

Composition of multiple lenses.

Data::Focus::Lens::Dynamic

A front-end lens that dynamically creates an appropriate lens for you.

All Data::Focus::HashArray::* modules optionally support immutable update. See individual documents for detail.

Lens Coercion

If you pass something that's not a Data::Focus::Lens object to Data::Focus's methods, it is coerced (cast) to a lens.

The passed value is used to create Data::Focus::Lens::Dynamic lens. Then that lens creates an appropriate lens for the given target with the passed value. This means we can rewrite the above example to:

use Data::Focus qw(focus);

my $target = ["hoge", { foo => "bar" }];
my $part = focus($target)->get(1, "foo");
focus($target)->set(1, foo => "buzz");

The above is possible because Data::Focus::Lens::Dynamic creates Data::Focus::Lens::HashArray::Index lenses under the hood. See Data::Focus::Lens::Dynamic for detail.

Traversals

As you might already notice, a lens can have more than one focal points. This is like slices and traversals.

To obtain all elements at the focal points, use list() method.

my $target = ["a", "b", "c"];
my @abc = focus($target)->list([0, 1, 2]);

Sometimes a lens has no focal point. In that case, you cannot set value to the target.

Lens Composition

You can compose two lenses to create a composite lens by "." operator.

my $target = ["hoge", { foo => "bar" }];
my $lens_1   = Data::Focus::Lens::HashArray::Index->new(index => 1);
my $lens_foo = Data::Focus::Lens::HashArray::Index->new(index => "foo");

my $composite = $lens_1 . $lens_foo;

my $part = focus($target)->get($composite);
focus($target)->set($composite, "buzz");

To compose two or more lenses at once, use Data::Focus::Lens::Composite.

EXPORTABLE FUNCTIONS

These functions are exported only by request.

$focused = focus($target, @lenses)

Alias of Data::Focus->new(target => $target, lens => \@lenses). It creates a Data::Focus object. @lenses are optional.

CLASS METHODS

$focused = Data::Focus->new(%args)

The constructor. Fields in %args are:

target => SCALAR (mandatory)

The target object it focuses into.

lens => LENS or ARRAYREF_OF_LENSES (optional)

A lens or an array-ref of lenses used for focusing. If some of the lenses are not Data::Focus::Lens objects, they are coerced. See "Lens Coercion" for detail.

$lens = Data::Focus->coerce_to_lens($maybe_lens)

Coerce $maybe_lens to a Data::Focus::Lens object.

If $maybe_lens is already a Data::Focus::Lens, it returns $maybe_lens. Otherwise, it creates a lens out of $maybe_lens. See "Lens Coercion" for detail.

OBJECT METHODS

$deeper_focused = $focused->into(@lenses)

Focus more deeply with the given @lenses and return the Data::Focus object.

$deeper_focused is a new Data::Focus object. $focused remains unchanged.

my $result1 = $focused->into("foo", "bar")->get();
my $result2 = $focused->into("foo")->get("bar");
my $result3 = $focused->get("foo", "bar");

## $result1 == $result2 == $result3

$datum = $focused->get(@lenses)

Get the focused $datum.

The arguments @lenses are optional. If supplied, @lenses are used to focus more deeply into the target to return $datum.

If it focuses on nothing (zero focal point), it returns undef.

If it focuses on more than one values (multiple focal points), it returns the first value.

@data = $focused->list(@lenses)

Get the focused @data.

The arguments @lenses are optional. If supplied, @lenses are used to focus more deeply into the target to return @data.

If it focuses on nothing (zero focal point), it returns an empty list.

If it focuses on more than one values (multiple focal points), it returns all of them.

$modified_target = $focused->set(@lenses, $datum)

Set the value of the focused element to $datum, and return the $modified_target.

The arguments @lenses are optional. If supplied, @lenses are used to focus more deeply into the target.

If it focuses on nothing (zero focal point), it modifies nothing. $modified_target is usually the same instance as the target, or its clone (it depends on the lenses used).

If it focuses on more than one values (multiple focal points), it sets all of them to $datum.

$modified_target = $focused->over(@lenses, $updater)

Update the value of the focused element by $updater, and return the $modified_target.

The arguments @lenses are optional. If supplied, @lenses are used to focus more deeply into the target.

$updater is a code-ref. It is called like

$modified_datum = $updater->($focused_datum)

where $focused_datum is a datum at one of the focal points in the target. $modified_datum replaces the $focused_datum in the $modified_target.

If it focuses on nothing (zero focal point), $updater is never called. $modified_target is usually the same instance as the target, or its clone (it depends on the lenses used).

If it focuses on more than one values (multiple focal points), $updater is repeatedly called for each of them.

HOW TO CREATE A LENS

To create your own lens, you have to write a subclass of Data::Focus::Lens that implements its abstract methods. However, writing your own Lens class from scratch is currently discouraged. Instead we recommend using Data::Focus::LensMaker.

Data::Focus::LensTester provides some common tests for lenses.

MAKE YOUR OWN CLASS LENS-READY

You can associate your own class with a specific lens object by implementing Lens() method in your class. See Data::Focus::Lens::Dynamic for detail.

Once Lens() method is implemented, you can focus into objects of that class without explicitly creating lens objects for it.

package My::Class;

...

sub Lens {
    my ($self, $param);
    my $lens = ...; ## create a Data::Focus::Lens object
    return $lens;
}


package main;
use Data::Focus qw(focus);

my $obj = My::Class->new(...);

focus($obj)->get("hoge");
focus($obj)->set(foo => "bar");

RELATIONSHIP TO HASKELL

Data::Focus's API and implementation are based on Haskell packages lens-family-core and lens.

For those familiar with Haskell's lens libraries, here is the Haskell-to-Perl mapping of terminology.

Traversal

The Traversal type corrensponds to Data::Focus::Lens. Currently there's no strict counterpart for Lens, Prism or Iso type.

(^.)

No counterpart in Data::Focus.

(^?)

get() method of Data::Focus.

(^..)

list() method of Data::Focus.

(.~)

set() method of Data::Focus.

(%~)

over() method of Data::Focus.

Applicative

Applicative typeclass corrensponds to Data::Focus::Applicative.

SEE ALSO

There are tons of modules in CPAN for data access and traversal.

REPOSITORY

https://github.com/debug-ito/Data-Focus

BUGS AND FEATURE REQUESTS

Please report bugs and feature requests to my Github issues https://github.com/debug-ito/Data-Focus/issues.

Although I prefer Github, non-Github users can use CPAN RT https://rt.cpan.org/Public/Dist/Display.html?Name=Data-Focus. Please send email to bug-Data-Focus at rt.cpan.org to report bugs if you do not have CPAN RT account.

AUTHOR

Toshio Ito, <toshioito at cpan.org>

LICENSE AND COPYRIGHT

Copyright 2015 Toshio Ito.

This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.