View Source Basics
This guide will show the basics of Pathex
. This is the best place to start learning Pathex
.
It won't take more than 5 minutes to read.
As README says, Pathex is a library for working with collections and nested key-value structures. Pathex uses a powerful functional abstraction called a lens (in Pathex we call it path or path-closure, you'll later see why). Path is a basically a path to a value in a nested structure, almost like filesystem path. With this path you can use some verbs to get, set, delete or update the value in the structure.
Create paths
First of all, we need to know how to create paths. A simple path can be created with the Pathex.path/2
macro.
For example:
p = path :x / :y
# Note the "/". That's why the library is called Pathex
This path specifies a way to get (or set, or delete or whatever) value 1
from %{x: %{y: 1}}
Note: You don't have to pin variables used in paths
x = 1 path :some_atom / x # Right path :some_atom / ^x # Wrong
Each type of value used in a path
corresponds to one or more types of Elixir collections
- Strings, tuples, maps, functions, lists are treated only as
Map
keys. - Atoms are treated as
Map
orKeyword
keys. - Positive integers are treated as
Map
keys orList
andTuple
indexes - Negative integers are treated as
Map
keys or first element inList
's view or prepending element inList
'supdate
orforce_set
operations - Variables are treated the same way as the type they have in runtime. For example, variables containing negative
integers are treated as negative integer values. Unfortunately, it is not possible to know variable type at compile-time,
that's why
Pathex
generates code for all possible types. See performance tips for more information.
Use paths
Alright, we have a path. Now, we need to know how to use it.
For example:
Let's take a value from nested structure
iex> first_username = path :usernames / 0
iex> Pathex.view(%{usernames: ["SubZero", "Scorpion"]}, first_username)
{:ok, "SubZero"}
Or let's change the value
iex> Pathex.set(%{usernames: ["SubZero", "Scorpion"]}, first_username, "Shao Khan")
{:ok, %{usernames: ["Shao Khan", "Scorpion"]}}
As you can see, every macro with the verbal name in Pathex
module is a macro for using the path.
There are a lot of other ways to use paths.
In all such macros, the first argument is an input structure and the second argument is a path-closure.
Note: Path-closure which are created inside path-using macros are optimised to have only one operation generated (instead of default three). You can read more about path-closures and operations here
As usual
- Macros without
!
always return{:ok, result}
or:error
- Macros with
!
returnresult
or raisePathex.Error
Compose paths
Path-closures can be composed together to create new path-closure, every path composition macro is a binary operator. Some compositions are optimized to generate one closure even if multiple closures are used.
You can concat paths with Pathex.~>/2
composition macro
For example:
iex> p1 = path :x
iex> p2 = path :y
iex> composed_path = p1 ~> p2
iex> 1 = Pathex.view!(%{x: [y: 1]}, composed_path)
You can think about path composition just like it's path concatenation in shell
For example:
iex> first_user = path :users / 0 # users/0 iex> name = path :name # name iex> first_user_name = first_user ~> name # users/0/name
Prebuilt paths
Pathex
provides some prebuilt paths for non-standard data manipulation. You
can find them in Pathex.Lenses
module. You can read more about them in
lenses guide
For example Pathex.Lenses.star/0
lens works just like *
in filesystem path.
Performance tips
General rule: the more data you provide to Pathex
at compile-time the better
For example:
path(1 / :x / :y, :json)
# works faster than
path(1 / :x / :y)
Because :json
mod optimizes the closure to one big case,
while the default :naive
mod generates nested cases.
And, in this example:
path(1 / :x / :y)
# works faster than
x = :x; path(1 / x / :y)
Because constants provide more information about available types.
This means that for :x
Pathex knows that this can be a Keyword
or Map
,
while x
means that this is a variable which could contain a List
/Tuple
index or a Map
/Keyword
value
And, in this example:
path(1 / x / :y)
# works faster than
path(1) ~> path(x) ~> path(:y)
Because path concatenation actually creates a path that calls all operands internally which increases the call stack and makes concatenated paths handle errors manually.