Hi, I’m Erika Rowland (a.k.a. erikareads). Hi, I’m Erika. I’m an Ops-shaped Software Engineer, Toolmaker, and Resilience Engineering fan. I like Elixir and Gleam, Reading, and Design. She/Her. Constellation Webring Published on

My Favorite Elixir Discovery - Access

A while ago, I read Hillel Wayne’ssource suggestion to “Search less, browse more”. Specifically to systemically go through the the information available for your chosen tools, rather than searching for specific answers to questions as they arise. This is the story of what I found when I started browsing through Elixir’s documentation.As found on HexDocs

But first, a diversion into:

GraphQL queries

One of the advantages of GraphQL is that you specify the structure that you’re querying with your query. You specify the edges and nodes of the data that you want, and it’s delivered in JSON with exactly that schema. However, this can get unwieldy if the accessible starting point is far away in the graph from the data you need.

For example, here is a query accessing <meetup.com>’s API searching for Online Elixir Events hosted by GroupsA lot of Elixir meetups went online during the pandemic, but Meetup’s search made them hard to find.Latitude: -48.876667 and Longitude: -123.393333 points to Point Nemo the most distant point of ocean from land.:

{
  keywordSearch(
    input: { first: 1000 }
    filter: {
      query: "elixir"
      lat: -48.876667
      lon: -123.393333
      source: GROUPS
      radius: 100
      eventType: ONLINE
    }
  ) {
    edges {
      node {
        result {
          ... on Group {
            name
            unifiedEvents {
              count
              edges {
                node {
                  title
                  isOnline
                  eventUrl
                  description
                  dateTime
                  timezone
                  group {
                    name
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Notice the nested curly braces at the end. I can get the data I need, and I know the structure that it’s in, but the data I care about is hidden by a nest of indirection.

Here’s how I first attempted to unpack the data I needed:

Unpacking GraphQL in Elixir

def main() do
  Neuron.Config.set(url: "https://api.meetup.com/gql")
  {:ok, query_results} = File.read!("elixir.gql") |> Neuron.query()

   results = query_results.body["data"]["keywordSearch"]["edges"] 
     |> Enum.filter(fn elem -> elem["node"]["result"]["unifiedEvents"]["count"] > 0 end) 
     |> Enum.map(fn elem -> elem["node"]["result"]["unifiedEvents"]["edges"] end) 
     |> Enum.concat()
     |> Enum.map(fn elem -> elem["node"] end)
end

This is only getting the maps of data that I care about, it doesn’t include additional work to do filtering that the API didn’t provide sufficient granularity to do before the queryNeuron is an Elixir GraphQL client: https://hexdocs.pm/neuron/readme.html.

Part of what makes it awkward is that we alternate between nodes, which are parsed as maps, and edges, which deserialize as lists.

Access, a better alternative?

This brings me back to browsing the Elixir documentation. My first inkling that there might be a better way arrived with Kernel.get_in/2Read more here. get_in/2 gets a value from a nested structure, I have one of those! It takes a data structure and a list of keys, and uses the Access behaviourI wondered why Access was implemented as a Behaviour and not a Protocol. It turns out that it comes down to performance, Access is invoked far more than any protocol functionality in Elixir, and it was becoming a bottleneck. So they switched it to a Behaviour, which is built right into the Erlang/OTP ecosystem, and is optimized with first-class support. to extract a value.

Access is what powers the thing["squareBracket"] syntax, so we were already using it. The real power of the module comes from special functions in the Access moduleread more about Access here. The get_in/2 function takes more than just atom or string keys, it can also take functions that can process our GraphQL Edges. Let’s talk about three:

Access.all/0

Access.all/0 accesses all elements of a list.

For example:

iex()> books = [
         %{
           name: "Naming Things", 
           url: "https://www.namingthings.co/"
         },
         %{
           name: "Essence of Software", 
           url: "https://essenceofsoftware.com/"
          }
        ]

iex()> get_in(books, [Access.all(), :name])
["Naming Things", "Essence of Software"]

Access.filter/1

Access.filter/1 access all elements of a list that match a provided predicate function.

For example:

iex()> books = [
         %{
           name: "Naming Things", 
           url: "https://www.namingthings.co/"
         },
         %{
           name: "Essence of Software", 
           url: "https://essenceofsoftware.com/"
          }
        ]

iex()> get_in(
  books, 
  [
    Access.filter(
      &String.starts_with?(&1.name, "N")
    ), 
    :name
  ]
)
["Naming Things"]

Access.key/2

Access.key/2 allows you to access the given key in a map or struct. This seems superfluous, but in Elixir, Structs don’t implement the Access behaviour by default. This means that you access keys with the . syntax, but not the [] syntax:

defmodule MyStruct do
  defstruct [:name]
end

iex()> my_struct = %MyStruct{name: "A name"}
iex()> my_struct.name
"A name"
iex()> my_struct[:name]
** (UndefinedFunctionError) function 
MyStruct.fetch/2 is undefined (MyStruct does not 
implement the Access behaviour. If you are using 
get_in/put_in/update_in, you can specify the 
field to be accessed using Access.key!/1)

The error message hints at the usefulness of Access.key/2, though it recommends the version that can throw an error: Access.key!/1

iex() get_in(my_struct, [Access.key(:name)])
"A name"

This is extremely useful for structs generated by other Elixir librariesI personally found this useful for access nested structures preloaded from Ecto.

Returning to GraphQL

With Access in hand, I can refactor my nested access call from earlierI can skip my earlier use of filter since Enum.concat/1 behaves nicely with empty lists.:

def main() do
  Neuron.Config.set(url: "https://api.meetup.com/gql")
  {:ok, query_results} = File.read!("elixir.gql") |> Neuron.query()

  results = get_in(
    query_results, 
    [
      Access.key(:body), 
      "data", 
      "keywordSearch", 
      "edges", 
      Access.all(), 
      "node", 
      "result", 
      "unifiedEvents", 
      "edges", 
      Access.all(), 
      "node"
     ]
  ) |> Enum.concat()
end

This shows us exactly how indirect the original data was. Now the structure is easy to see and modify.

Takeaways

Access provides a clean way to access data in nested data structures. Access.key provides a way to access structs. Access.all and Access.filter/1 provide ways to handle lists in the middle of a data structureI’ve heard that this style of accessors is referred to as Functional Lenses, and other programming languages have implementations or libraries supporting this kind access..

Here, I only needed to access the nested data, but Elixir’s Kernel provides functions for get_and_update_in/3, pop_in/2, put_in/3 for changing the data in that nested structure. Check them out: https://hexdocs.pm/elixir/1.15.0/Kernel.html


Constellation Webring