CList (Circular List v0.1.1)

Circular Lists (CList Module)

CList allow to work with circular lists. A circular lists is a finite list that can be traversed as if it were infinite. This is possible because in a circular list, when you reach the end of the list, you go back to the beginning and take the first element of the list as if it were next to the last one. In other words, we could assume that a copy of the original list is inserted at the end of the list, and so on ad infinitum.

Internally a CList is a map that store the original list (see below original and current list) and a pointer indicating the current position in base 1 (as Erlang lists).

Although internally a CList is a map, its visual representation (IO.inspect) is:

iex> CList.new([1, 2, 3, 4, 5])
#CList[1, 2, 3, 4, 5]/1

As you can see a list and a pointer will always be displayed. If you move the pointer the representation will look different, but internally the original list will be the same.

iex> a = CList.new([1, 2, 3, 4, 5])
#CList[1, 2, 3, 4, 5]/1

iex> b = CList.forward(a)
#CList[2, 3, 4, 5, 1]/2

iex> CList.equals?(a, b)
true

You can see that the list and the pointer of a and b differs, but the CLists are equals because their sequential lists are the same.

And talking about the concept of equal on CLists, it is important to note that 2 CLists are considered equal if they have the same size and their traversal sequence is exactly the same.

iex> a = CList.new [1, 2, 3, 4, 5, 6]
#CList[1, 2, 3, 4, 5, 6]/1

iex> b = CList.new [5, 6, 1, 2, 3, 4]
#CList[5, 6, 1, 2, 3, 4]/1

iex> CList.equals?(a, b)
true

iex> b = CList.new [6, 5, 1, 2, 3, 4]
#CList[6, 5, 1, 2, 3, 4]/1

iex> CList.equals?(a, b)
false

Original and current list

What CList stores at all times is what we call the current list (i.e., the rotated list) but implicitly also store what we will call the original list, which is the current list when the pointer is equal to 1.

iex> a = CList.new([:a, :b, :c, :d, :e])
#CList[:a, :b, :c, :d, :e]/1 
## Original list: [:a, :b, :c, :d, :e]
## Current list: [:a, :b, :c, :d, :e]

iex> CList.forward(a, 3)
#CList[:d, :e, :a, :b, :c]/4
## Original list: [:a, :b, :c, :d, :e]
## Current list: [:d, :e, :a, :b, :c]

The pointer

Now is time to explain what the pointer value means. When the pointer is, for example, 2, it means that the first value in the current list is equivalent to the value with index 2 in the original list (remember, indexes in CList work on a base 1).

## When pointer is 1, you see the original list
#CList[1, 2, 3, 4, 5]/1
          ^
          |
          +----------------------------------------------+
                                                         |
## When pointer is not 1, you see the list rotatated     |
#CList[2, 3, 4, 5, 1]/2                                  |
                      |                                  |
                      +----------------------------------+

Enumerable

CList implements Enumerable, so you can play with Streams or use Enum.* functions.

iex> a = CList.new([1, 2, 3, 4, 5])
#CList[1, 2, 3, 4, 5]/1

iex> Enum.take(a, 20)
[1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5]

iex> a 
  |> Stream.map(fn v -> v * 3 end) 
  |> Stream.map(fn v -> v - 1 end) 
  |> Enum.take(22)
[2, 5, 8, 11, 14, 2, 5, 8, 11, 14, 2, 5, 8, 11, 14, 2, 5, 8, 11, 14, 2, 5]

Since a CList could be iterated ad infinitum, the Enum.* functions that has not stop condition will take the current list of the CList as enumerable input.

iex> a = CList.new([1, 2, 3, 4, 5])
#CList[1, 2, 3, 4, 5]/1

iex> Enum.map(a, fn v -> v * 2 end)
[2, 4, 6, 8, 10]

iex> rl = CList.forward(a, 3)
#CList[4, 5, 1, 2, 3]/4

iex> Enum.map(a, fn v -> v * 2 end)
[8, 10, 2, 4, 6]

TODO

One million tests ¯\_(ツ)_/¯

How to use

defmodule TestCList do
  use CList

  def hello do
    ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d", "! "]
      |> CList.new()
      |> say_hello_but_in_a_cool_way()
  end

  def say_hello_but_in_a_cool_way(cl, count \\ 5)
  def say_hello_but_in_a_cool_way(_, 0), do: IO.write("\n")
  def say_hello_but_in_a_cool_way(CList.match([c | l]), count) do
    IO.write(c)
    :timer.sleep(200)
    say_hello_but_in_a_cool_way(CList.forward(l), count - (c == "! " && 1 || 0))
  end
end

You may also want to import match/1 and forward/1 to avoid having to explicitly specify CList...

defmodule TestCList do
  use CList
  import CList, only: [match: 1, forward: 1]
  ...

  def say_hello_but_in_a_cool_way( match([c | l]), count) do
    IO.write(c)
    :timer.sleep(200)
    say_hello_but_in_a_cool_way( forward(l), count - (c == "! " && 1 || 0))
  end

  ...
end

Installation

If available in Hex, the package can be installed by adding clist to your list of dependencies in mix.exs:

def deps do
  [
    {:clist, "~> 0.1.0"}
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/clist.

Summary

Functions

Compare 2 CList and return true if the sequence obtained when traversing both lists are equal regardless of whether the original list is the same or not.

Take a CList, move de pointer count units and return the new CList.

Just an alias for new(range).

Used to match CList in the style of erlang list pattern. Ex

Create a new CList from a list or a range. The initial list/range will be the original list and the pointer of the CList will be initialized to 1.

Take a CList, extract the first value of the current list and move de pointer 1 unit. Return the value and the new CList.

Return the current value of the pointer.

Set the current value of the pointer, adjust the list according to the pointer value and returns the new list.

Reset the pointer of the list to 1 and return the new CList. It is equivalent to call ptr(rlist, 1).

Return the size of the original list.

It is the same that call Enum.take(rlist, count).

Take a CList and return the current list (not the original list).

Return the current list as a tuple.

Types

t()

@type t() :: %CList{list: term(), ptr: term()}

Functions

equals?(left, right)

@spec equals?(left :: t(), right :: t()) :: boolean()

Compare 2 CList and return true if the sequence obtained when traversing both lists are equal regardless of whether the original list is the same or not.

forward(clist, count \\ 1)

@spec forward(clist :: t(), count :: non_neg_integer()) :: t()

Take a CList, move de pointer count units and return the new CList.

iex> rl = CList.new([1, 2, 3, 4, 5])
#CList[1, 2, 3, 4, 5]/1

iex> rl = CList.forward(rl)
#CList[2, 3, 4, 5, 1]/2

iex> rl = CList.forward(rl, 3)
#CList[5, 1, 2, 3, 4]/5

from_range(range)

@spec from_range(range :: Range.t()) :: t()

Just an alias for new(range).

match(list)

(macro)

Used to match CList in the style of erlang list pattern. Ex:

defmodule Tests do
  use CList
  import CList

  def test(match [h | t]) do
    IO.puts "H: #{h}"
    IO.inspect t, label: "T"
    :timer.sleep(1000)
    test(forward t)
  end
end

rl = CList.new([1,2,3])
Tests.test(rl)
## Output:
H: 1
T: #CList[1, 2, 3]/1
H: 2
T: #CList[2, 3, 1]/2
H: 3
T: #CList[3, 1, 2]/3
H: 1
T: #CList[1, 2, 3]/1
H: 2
T: #CList[2, 3, 1]/2
...
...

Combining match and forward/1 you can go through a CList in a very similar way to how you do it with Erlang lists. You must not forget that, since CLists are endless, you must provide an exit condition when appropriate.

new(list)

@spec new(list :: list() | Range.t()) :: t()

Create a new CList from a list or a range. The initial list/range will be the original list and the pointer of the CList will be initialized to 1.

iex> rl = CList.new([1, 2, 3, 4, 5])
#CList[1, 2, 3, 4, 5]/1

next(clist)

@spec next(clist :: t()) :: {term(), t()}

Take a CList, extract the first value of the current list and move de pointer 1 unit. Return the value and the new CList.

iex> rl = CList.new([1, 2, 3, 4, 5])
#CList[1, 2, 3, 4, 5]/1

iex> {value, rl} = CList.next(rl)
{1, #CList[2, 3, 4, 5, 1]/2}

iex> {value, rl} = CList.next(rl)
{2, #CList[3, 4, 5, 1, 2]/3}

ptr(clist)

@spec ptr(clist :: t()) :: non_neg_integer()

Return the current value of the pointer.

ptr(clist, new_ptr)

@spec ptr(clist :: t(), ptr :: non_neg_integer()) :: t()

Set the current value of the pointer, adjust the list according to the pointer value and returns the new list.

reset(clist)

@spec reset(clist :: t()) :: t()

Reset the pointer of the list to 1 and return the new CList. It is equivalent to call ptr(rlist, 1).

size(clist)

@spec size(clist :: t()) :: non_neg_integer()

Return the size of the original list.

take(clist, count)

@spec take(clist :: t(), count :: non_neg_integer()) :: list()

It is the same that call Enum.take(rlist, count).

to_list(clist)

@spec to_list(clist :: t()) :: list()

Take a CList and return the current list (not the original list).

to_tuple(clist)

@spec to_tuple(clist :: t()) :: tuple()

Return the current list as a tuple.