Iteraptor :: Iterating Nested Terms Like I’m Five

The library to easily map/each/reduce/map_reduce/filter deeply nested maps/keywords/lists, called Iteraptor has been released. Besides standard map-reduce operations, it allows flattening nested terms and deflattening them back, and provides a couple of helper methods for low-level traversing them.

There is a blog post describing the interface in details, and Hexdock.

We were using it in production for months, but since we were not able to catch more bugs on our limited set of nested structures, I would be glad to hear about other use-cases where it fails to traverse the nesteds properly.

Also, it’s extremely handy when one needs to filter out some deeply nested keys, or get just the second level of a nested map, or produce a flattened keyword list out of external config file :slight_smile:

12 Likes

This kind of functionality seems like the perfect fit for Property-based testing :smiley:

3 Likes

Indeed, that was my immediate thought as well.

@mudasobwa what is the reasoning behind the names of some of the operations? I would expect each to never return anything, for example, as well as for map to preserve the structure of the thing it’s mapping over. These are standard behaviors connected with the terminology in FP as far as I know and I assume most people familiar with it would expect the same.

3 Likes

I am not sure what did you mean by “map to preserve the structure of the thing it’s mapping over”—mine does to the extent it is able to.

For each we cannot return literally nothing, due to implicit returns in Elixir; the standard Enum.each/2 returns :ok but I found it more convenient to return the input as is (making it easy to inject into |> pipes as IO.inspect e.g. does.

the perfect fit for Property-based testing

Thanks for the suggestion, I’ll do. I am pretty sure though that there is no perfect testing environment where all the issues might be covered, that’s why I kindly asked for bug reports, if any.

1 Like

I mean exactly what I wrote. It’s expected that a map preserves the structure/shape/form of what it’s mapping over. You go against that expectation in your examples, hence my comment about your naming being somewhat odd.

Same goes for each. There is no nothing on the BEAM, so I wouldn’t expect you to return that, but to expect actual output from each is to go against expectation. If someone uses your each as a last expression in a function they won’t get what they were expecting.

I think your library seems good and it’s certainly something that has a use, but your names are not idiomatic. I don’t know if these are Ruby-isms, but I’d argue that you should either change the behavior or the names somewhat to match idiomatic FP.

I never went against that. neither did the code. There is a misunderstanding because this is AFAICT the first implementation of nested term mapping. You have likely mentioned this example:

%{a: %{b: %{c: 42}}}

Please note, that it’s a bare map having the only element in a nutshell. You should not be fooled by the fact that the first value is also a map. So, by what is expected—my map function should return a 1-sized map. That’s it. It does.

Even more, by default (without an explicit option yield: all passed,) the only leaves will be passed to mapper and what you originally expected would happen out of the box (mapper will be called only once with [:a, :b, :c], {:c, 42} as arguments.)

Sometimes, though, one wants to map deeply nested whatever (but not the leaf) to something else. And this library provides such a possibility by accepting yield: all option. This is an extended mode when the library consumer is allowed to map anything on any nesting level. The consistency on each level is still preserved: Enumerable of size N will be mapped to Enumerable of size N.

To make it clear with the aforementioned example:

foo = %{c: 42}
Iteraptor.map(%{a: %{b: foo}}, MAPPER)
#⇒ %{a: %{b: "YAY"}}

Is it looking more familiar now? Well, IMHO yes, because we mapped foo whatever it is to something new. I hope I made the intent/implementation clearer.

Regarding each I plain disagree, sorry for saying that. Since you said “return nothing” in the first place, I treat it as “the caller must not rely on what it returns.” There is no contract on what each should return. And I return the structure itself for easier piping (Erlang does not have pipe operator, that’s why it makes not much sense to return anything but ok atom from erlang mappers, but Elixir indeed does.) I don’t see any violation of expectations here.

1 Like

Naming is about social contracts and both of these functions (and a host of other ones) are associated with certain behaviors in all languages. None of what you’ve explained about your additional behavior changes that. To each his own, though.

1 Like

Iteraptor gets a new feature in v1.10.1: Iteraptor.jsonify/2 to deeply convert all the Keyword lists to Maps. Jason as well as some other json encoding libraries have issues with marshalling keyword lists, because internal representation of Keyword is eventually a list of tuples, and tuples are not encodeable.

Iteraptor.jsonify/2 traverses the deeply nested term and recursively converts all the keywords to maps.

iex> Iteraptor.jsonify([foo: [bar: [baz: :zoo], boo: 42]], values: true)
#⇒ %{"foo" => %{"bar" => %{"baz" => "zoo"}, "boo" => 42}}
1 Like