Map ordered list of items to list with resetting position

Make me more functional. What follows is a hypothetical inspired by actual code.

Lets say I have a list of states: ["bar,", "bar,", "bar,", "baz,", "baz,", "foo,", "foo,", "foo,", "foo"] that I want to transform into a list of two-element tuples, where the second element is a counter that resets to one each time the state changes. That is, I want this:

[
  {"bar", 1},
  {"bar", 2},
  {"bar", 3},
  {"baz", 1},
  {"baz", 2},
  {"foo", 1},
  {"foo", 2},
  {"foo", 3},
  {"foo", 4}
]

Here is my solution. While it works, that map I’m using as the accumulator for reduce seems, I don’t know, a bit heavy handed. I suspect there’s a more functional or idiomatic way of going about this, is there? (Ignore the lack lack of pipes.)

things = ~W(bar bar bar baz baz foo foo foo foo)

indexed_things = Enum.reduce(things, %{previous_thing: nil, counter: 0, acc_things: []}, fn thing, acc ->
                   counter = if acc.previous_thing != thing, do: 0, else: acc.counter
                   counter = counter + 1

                   %{previous_thing: thing, counter: counter, acc_things: [{thing, counter} | acc.acc_things]}
                 end)

IO.inspect Enum.reverse(indexed_things.acc_things)

Thanks, Pete

I’m hardly aware of what idiomatic Elixir would look like (don’t have enough experience), but wanted to give it a try, so here’s what i came up with:

def duplicate_by_frequency do
  ~W(bar bar bar baz baz foo foo foo foo)
  |> Enum.frequencies()
  |> Enum.to_list()
  |> add(1, [])
  |> Enum.reverse()
end

defp add([{k, c} | _] = freq, counter, res) when counter <= c,
  do: add(freq, counter + 1, [{k, counter} | res])

defp add([{_, c} | tail], counter, res) when counter > c,
  do: add(tail, 1, res)

defp add([], _, res),
  do: res
1 Like

One way to split this problem up is:

  • first, create a list of lists containing common states. The function you want for this is Enum.chunk_by/2:
Enum.chunk_by(states, & &1)
# yields
[["bar", "bar", "bar"], ["baz", "baz"], ["foo", "foo", "foo", "foo"]]
  • for each of these lists, we need to convert each element to a tuple {element, index}. The function you want for that is Enum.with_index/2:
Enum.with_index(["foo", "foo", "foo"], 1)
# yields
[{"foo", 1}, {"foo", 2}, {"foo", 3}]

Combine these two together with Enum.map/2 and you ALMOST get the result you’re looking for:

iex(4)> Enum.chunk_by(states, & &1) |> Enum.map(& Enum.with_index(&1, 1))
[
  [{"bar", 1}, {"bar", 2}, {"bar", 3}],
  [{"baz", 1}, {"baz", 2}],
  [{"foo", 1}, {"foo", 2}, {"foo", 3}, {"foo", 4}]
]

This isn’t quite right because mapping over a list of lists yields a list of lists - we want to map over a list of lists and return a single flat list, so use Enum.flat_map/2 instead:

iex(5)> Enum.chunk_by(states, & &1) |> Enum.flat_map(& Enum.with_index(&1, 1))
[
  {"bar", 1},
  {"bar", 2},
  {"bar", 3},
  {"baz", 1},
  {"baz", 2},
  {"foo", 1},
  {"foo", 2},
  {"foo", 3},
  {"foo", 4}
]
2 Likes

Well, that certainly works. It’s clever and it’s functional, but to me, anyway, as a relative newb, it suffers from being somewhat obscure.

This is nice. Also functional, but simple and straight forward. Thanks. Your answer definitely helps me to think more functionally. Part of which seems to be simply becoming more familiar with the standard API.

Or you could use straight recursion:
Edit, I’ve just realised that intermediary states were supposed to be kept, editing accordingly
Edit 2 : made a few typos with missing parens, hope it’s fixed now.

def somefunc([], acc), do: Enum.reverse(acc)
def somefunc([head | rest], [{head, n} | _acc] = acc ) do
     somefunc(rest, [{head, n + 1} | acc])
end
def somefunc([head | rest], acc), do: somefunc(rest, [{head, 1} | acc])

Note, that I haven’t tested that so I might have made a typo or whatever, but the general idea is there.
Whether you prefer recursion or folds is up to you, but I wanted to point out recursion as a potential solution.

3 Likes