GithubHelp home page GithubHelp logo

hissssst / pathex Goto Github PK

View Code? Open in Web Editor NEW
317.0 6.0 15.0 267 KB

Fastest tool to access data in Elixir

License: BSD 2-Clause "Simplified" License

Elixir 99.50% Nix 0.50%
elixir macros lenses nested-structures lens

pathex's Introduction

Pathex

Speed or composability? Choose both!

What is Pathex?

Pathex is a library for performing fast actions with nested data structures in Elixir. With pathex you can trivially set, get and update values in structures in a functional manner. It provides all necessary logic to manipulate data structures in different ways using flexible functional lens pattern.

Why another library?

Existing methods of accesssing data in nested structures are either slow (like Focus) or do not provide enough functionality (like Access). For example setting the value in structure with Pathex is 70-160x faster than Focus or 2-3x faster than put_in and get_in

You can checkout benchmarks at https://github.com/hissssst/pathex_bench

Usage

Pathex is really simple and straightforward to use (almost like Enum). You don't need to learn any specific language, just create paths with path and use verbs with them.

Add it to your module

# This will import path macro and operators and require Pathex
use Pathex

Or just import Pathex

Create the path

path_to_streets = path :user / :private / :addresses / 0 / :street
path_in_json = path "users" / 1 / "street", :json

This creates closure which can get, set, update and delete values in this path

Use the path

{:ok, "6th avenue" = street} =
  %{
    user: %{
      id: 1,
      name: "hissssst",
      private: %{
        phone: "123-456-789",
        addresses: [
           [city: "City", street: "6th avenue", mail_index: 123456]
        ]
      }
    }
  }
  |> Pathex.view(path_to_streets)

%{
  "users" => %{
    1 => %{"street" => "6th avenue"}
  }
} = Pathex.force_set!(%{}, path_in_json, street)

Features

Pathex has a lot of different features and can even compete with code written by hand in terms of efficiency. Pathex significantly reduces the time to write a code which manipulates nested structure, while providing efficiency and composability. No more functions like get_users, set_users, update_users! No more XPaths, JSONPaths, CSS Selectors!

Easy to use

It's not harder to use than Map or Enum! Check out the cheatsheet for common tasks.

Pathex also provides more information about errors than any other tool.

iex(1)> field = :email
iex(2)> Pathex.view!(%{}, path(:users) ~> all() ~> path(:personal / field))
** (Pathex.Error)
  Couldn't find element

    Path:      path(:users) ~> all() ~> path(:personal / :email)

    Structure: %{}

Fast

Paths are just a set of pattern-matching cases. This is done to extract maximum efficiency from BEAM's pattern-matching compiler.

# Code for viewing variables for path
path(1 / "y", :map)

# Almost equals to
case input do
  %{1 => %{"y" => res}} ->
    {:ok, res}

  _ ->
    :error
end

Reusable

One path can be used to update, get, set, delete or update a value in the structure! And these paths can even be composed together. This composition is very efficient, there's no need to concatenate lists like Access does.

# User structure
user = %User{
  personal: %{fname: "Kabs", sname: "Rocks"},
  phone: "123-456-789"
}

# Path to username in user structure
username = path(:personal / :fname)

# Get a username
{:ok, "Kabs"} = Pathex.view(user, username)

# Set a username
another_user =
  %User{
    personal: %{fname: "Blabs", sname: "Rocks"},
    phone: "123-456-789"
  } = Pathex.set!(user, username, "Blabs")

# Get all usernames!
import Pathex.Lenses
["Kabs", "Blabs"] =
  [
    user,
    another_user
  ]
  |> Pathex.view!(all() ~> username)

Pathex can be used to manipulate different nested data structures. From GenServer state to HTML or Elixir's AST!

Extensible

Pathex is built around simple primitive called path-closure, which is a simple closure with clearly defined specification. Anything complying with Pathex.t() spec can be used within Pathex.

Installation

def deps do
  [
    {:pathex, "~> 2.0"}
  ]
end

Pathex is significantly faster with OTP 26

Contributions

Welcome! If you want to get your hands dirty, you can check existing TODO's.

By the way

If you have any suggestions or want to change something in this library don't hesitate to open an issue. If you have any whitepapers about functional lenses, you can add them in a PR to the bottom of this readme

pathex's People

Contributors

hissssst avatar jonathanpglick avatar kianmeng avatar mathieuprog avatar petermm avatar seb3s avatar sirikid avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

pathex's Issues

All lens deletion bug

As @ffloyd mentioned in #20, I see some weird behavior with all/0 lens:

import Pathex

data = [
  %{ "a" => 1 },
  %{ "a" => 2 }
]

all_a = Pathex.Lenses.all() ~> path("a")

Pathex.get(data, all_a) |> IO.inspect(label: "get")
Pathex.delete!(data, all_a) |> IO.inspect(label: "delete!")

And as result get/2 works, but delete/2 fails:

** (Pathex.Error) 
  Couldn't find element

    Path:      all() ~> path("a")

    Structure: [%{"a" => 1}, %{"a" => 2}]

    (stdlib 3.17.2) erl_eval.erl:685: :erl_eval.do_apply/6
    (stdlib 3.17.2) erl_eval.erl:893: :erl_eval.expr_list/6
    (stdlib 3.17.2) erl_eval.erl:408: :erl_eval.expr/5
    (elixir 1.14.0) lib/module/parallel_checker.ex:100: Module.ParallelChecker.verify/1

Is it possible to define collection type when using from_list?

Hi there, apologies if I missed this in the documentation, but is it possible to define the collection type when creating a path via from_list?

e.g. I want to create something similar to path :alpha / (0 :: :list), but dynamically.

I attempted to do something like from_list([:alpha, 0 :: :list]), however, I got an error saying that :: was not a defined function.

The use case for this is that I want to use the above path to force_set and create if it doesn't exist, but I want it to create a list rather than a map. From testing it seems that defining the collection type correctly results in a list being created for the 0 path.

Any assistance would be much appreciated!

Thanks!

Delete a key?

Is it possible to add a function to delete a key?

p = path "hey" / "you"

%{"hey" => %{"x" => 2}} ==  delete %{"hey" => %{"you" => 1, "x" => 2}}, p
p = path "hey" / 1

%{"hey" => [0, 2]} ==  delete %{"hey" => [0, 1, 2]}, p

path(y :: :list, :json)

iex> path(y :: :list, :json)
** (FunctionClauseError) no function clause matching in Pathex.Builder.ForceUpdater.reduce_into/2

    The following arguments were given to Pathex.Builder.ForceUpdater.reduce_into/2:

        # 1
        []

        # 2
        {{:|>, [context: Pathex.Builder.ForceUpdater, imports: [{2, Kernel}]],
          [
            {{:., [], [{:function, [], Elixir}]}, [], []},
            {:case, [],
             [
               [
                 do: [
                   {:->, [],
                    [
                      [ok: {:value, [], Pathex.Builder.ForceUpdater}],
                      {:value, [], Pathex.Builder.ForceUpdater}
                    ]},
                   {:->, [],
                    [
                      [:error],
                      {:throw,
                       [context: Pathex.Builder.ForceUpdater, imports: [{1, Kernel}]],
                       [:path_not_found]}
                    ]}
                 ]
               ]
             ]}
          ]}, {:default, [], Elixir}}

    Attempted function clauses (showing 1 out of 1):

        defp reduce_into([path_item | _] = path_items, {acc_code, acc_items})

    (pathex 2.5.1) lib/pathex/builder/updaters/force_updater.ex:26: Pathex.Builder.ForceUpdater.reduce_into/2
    (elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
    (pathex 2.5.1) lib/pathex/builder/updaters/force_updater.ex:21: Pathex.Builder.ForceUpdater.build/1
    (pathex 2.5.1) lib/pathex/builder.ex:47: anonymous fn/2 in Pathex.Builder.build/2
    (elixir 1.14.3) lib/enum.ex:1662: anonymous fn/3 in Enum.map/2
    (stdlib 4.2) maps.erl:411: :maps.fold_1/3
    (elixir 1.14.3) lib/enum.ex:2480: Enum.map/2
    (pathex 2.5.1) lib/pathex/builder.ex:46: Pathex.Builder.build/2

Error when using tuple with variable in path creation

Great Library!

using a variable inside a tuple key fails, also I think the error message about the pin operator is now invalid.

iex > val = "xxx"
iex > path  :x / {:foo, val}
** (CompileError) cannot use variable val as map key inside a pattern. Map keys in patterns can only be literals (such as atoms, strings, tuples, and the like) or an existing variable matched with the pin operator (such as ^some_var)

but wrapping the tuple in a variable works just fine

iex > key = {:foo, val}
{:foo, "xxx"}
iex > path :x / key
#Function<43.65746770/2 in :erl_eval.expr/5>

Dialyzer complains about deletion functions

When using (some) functions which rely on the magic :delete_me atom, Dialyzer complains about a pattern that can never match the type (this error is for the most generic case where no types can be inferred at the place of use):

The pattern 
          'delete_me' can never match the type 
          'error' | {'ok', [any()] | tuple() | map()}

Affected functions (for me) are delete/2, delete!/2, and pop!/1. pop/1 Isn't affected, and I'm not sure why.

Reproduction:

Paste this module into any project which has pathex dependency installed, and run Dialyzer.

defmodule PathexTest do
  use Pathex

  def pop_key_from_my_map(%{} = my_map, key) do
    Pathex.delete(my_map, path(key)) # The pattern 'delete_me' can never match the type 'error' | {'ok',map()}
    Pathex.delete!(my_map, path(key)) # The pattern 'delete_me' can never match the type 'error' | {'ok',map()}
    Pathex.pop!(my_map, path(key)) # The pattern 'delete_me' can never match the type 'error' | {'ok',map()}
    Pathex.pop(my_map, path(key)) # No warning for some reason?
  end
end

For me this is reproducable on a mix new project with two dependencies, but also causes a warning in VSCode even without the dialyxir dependency:

[
  {:pathex, "~> 2.4.2"},
  {:dialyxir, "~> 1.0", only: [:dev], runtime: false}
]

My environment:

> elixir --version
Erlang/OTP 25 [erts-13.0.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]

Elixir 1.14.1 (compiled with Erlang/OTP 25)

Pathex does not compile with elixir 1.14.0-rc0

== Compilation error in file lib/pathex/accessibility.ex ==
** (ArgumentError) cannot pipe value into function.()
|> case do
{:ok, value} -> value
:error -> throw(:path_not_found)
end, the :|> operator can only take two arguments
(elixir 1.14.0-rc.0) lib/macro.ex:323: Macro.pipe/3
(elixir 1.14.0-rc.0) expanding macro: Kernel.|>/2
lib/pathex/accessibility.ex:144: Pathex.Accessibility.from_list/2
(elixir 1.14.0-rc.0) expanding macro: Kernel.|>/2
lib/pathex/accessibility.ex:144: Pathex.Accessibility.from_list/2
expanding macro: Pathex.path/2
lib/pathex/accessibility.ex:144: Pathex.Accessibility.from_list/2

great library, this could be elixir issue

How should I create a path from a list?

I want to create a dynamic path.

data = [{:a, []}, {:b, [{:c, []}]}]
assert {:ok, {:c, []}} = view(data, path 1 / 1 / 0)

This is good, but what if I don't know [1, 1, 0] at compile time. I couldn't see anything documented about passing a list as a path, so I tried the following:

def make_path(elems) do
  elems
  |> Enum.map(fn elem -> path elem end)
  |> Enum.reduce(fn p1, p2 -> p1 ~> p2 end)
end

assert {:ok, {:c, []}} = view(data, make_path([1, 1, 0]))
# fails with :error

Any idea what I am missing here?

Thanks.

Matt

Invalid behaviour of `Pathex.delete!` when using concatenation and/or `all` lense

Let's say I have the following data:

data = %{
  "key" => "value",
  "list" => [
    %{
      "a" => 1,
      "b" => 2
    },
    %{
      "a" => 3,
      "b" => 4
    }
  ]
}

and my goal is to delete all "a" keys from all maps in the inner list. So, I create a path for them:

all_a = path("list") ~> Pathex.Lenses.all() ~> path("a")

Pathex.get(data, all_a)
# result:
[1, 3]

But when I use delete! it just wipes all items in the list:

Pathex.delete!(data, all_a)
# result:
%{"key" => "value", "list" => []}

Then I tried to not use the lens, but still do concatenation:

first_a_concat = path("list") ~> path(0) ~> path("a")

Pathex.get(data, first_a_concat)
# result:
1

Pathex.delete!(data, first_a_concat)
# result:
%{"key" => "value", "list" => [%{"a" => 3, "b" => 4}]} # it removes first item from the list!

When I use path without concatenation it works as it should:

first_a_path = path("list" / 0 / "a")

Pathex.get(data, first_a_path)
# result:
1

Pathex.delete!(data, first_a_path)
# result:
%{"key" => "value", "list" => [%{"b" => 2}, %{"a" => 3, "b" => 4}]}

Then I tried to check what will happen with some deeeep data:

deep_data = %{
  "list" => [
    %{
      "list" => [
        %{
          "list" => [
            %{
              "a" => 1,
              "b" => 2
            }
          ]
        }
      ]
    }
  ]
}

deep_dive_path =
  path("list")
  ~> Pathex.Lenses.all()
  ~> path("list")
  ~> Pathex.Lenses.all()
  ~> path("list")
  ~> Pathex.Lenses.all()
  ~> path("a")

Pathex.get(deep_data, deep_dive_path)
# result:
[[[1]]] # btw, shouldn't it be a flat list here?

Pathex.delete!(deep_data, deep_dive_path)
# result:
** (CaseClauseError) no case clause matching: :delete_me
    (pathex 2.4.0) lib/pathex/lenses/all.ex:111: anonymous fn/3 in Pathex.Lenses.All.all/0
    (elixir 1.14.0) lib/enum.ex:4751: Enumerable.List.reduce/3
    (elixir 1.14.0) lib/enum.ex:2514: Enum.reduce_while/3
    (pathex 2.4.0) lib/pathex/lenses/all.ex:110: anonymous fn/2 in Pathex.Lenses.All.all/0
    (stdlib 3.17.2) erl_eval.erl:672: :erl_eval.do_apply/5
    (stdlib 3.17.2) erl_eval.erl:270: :erl_eval.expr/5
    (pathex 2.4.0) lib/pathex/lenses/all.ex:111: anonymous fn/3 in Pathex.Lenses.All.all/0
    (elixir 1.14.0) lib/enum.ex:4751: Enumerable.List.reduce/3

So, it looks like delete!/2 has some real problems with lenses and concatenation. =)

exists?/2 bug

iex> s = [
  %{
    children: [
      %{
        children: [],
        id: "fcfb1066-6e16-4a84-81e0-7746f12b7e76",
        tag: "test1",
        type: "section"
      },
      %{
        id: "035ca737-2cc2-488b-83f5-fe7cc2becc5e",
        parent_id: "a9963499-f6a4-4a32-bafa-aa22a9b059bc"
      },
      %{
        children: [
          %{
            other: [
              %{
                id: "035ca737-2cc2-488b-83f5-fe7cc2becc5e",
                parent_id: "a9963499-f6a4-4a32-bafa-aa22a9b059bc"
              }
            ],
            tag: "test4"
          }
        ],
        id: "8a9f0126-33a0-4c03-8c4a-5507f0562c8a",
        index: 2,
        parent: "layout",
        parent_id: "a9963499-f6a4-4a32-bafa-aa22a9b059bc",
        tag: "test2",
        type: "section"
      }
    ],
    parent: "dragLocation",
    parent_id: "dragLocation",
    tag: "test3"
  }
]

iex> Pathex.view s, tag_lens ~> matching("test2")
{:ok, "test2"}

iex> Pathex.exists? s, tag_lens ~> matching("test2")
** (CaseClauseError) no case clause matching: true
    (pathex 2.5.0) lib/pathex/lenses/some.ex:186: Pathex.Lenses.Some.map_view/2
    iex:6: (file)
    iex:6: (file)
    iex:6: (file)
    iex:9: (file)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.