chunky v0.11.4 Chunky.Sequence View Source

Create and manipulate mathematical sequences.

New values in the sequences are created only when requested, allowing iteration of finite and infinite data in a lazy fashion:

iex> data = Sequence.create(Sequence.Basic, :whole_numbers) |> Sequence.take(6)
iex> data |> Tuple.to_list() |> List.first()
[1, 2, 3, 4, 5, 6]

Available Sequences

The Chunky.Sequence modules have sequences available in:

About Sequences

Sequences are stateful - sequence manipulations will always return a %Sequence{} object along with any other requested data. Repeated calls to the same instance of a Sequence struct will always yield the same result:

iex> seq = Sequence.create(Sequence.Basic, :whole_numbers, [start: 100])
iex> (seq |> Sequence.next!()).value
100
iex> (seq |> Sequence.next!()).value
100

The updated Sequence struct needs to be maintained, and passed along to other functions, to create new sequence values:

iex> seq_a = Sequence.create(Sequence.Basic, :whole_numbers, [start: 100])
iex> seq_b = seq_a |> Sequence.next!()
iex> seq_b.value
100
iex> seq_c = seq_b |> Sequence.next!()
iex> seq_c.value
101

or

iex> seq_a = Sequence.create(Sequence.Basic, :whole_numbers, [start: 100])
iex> seq_n = seq_a |> Sequence.next!() |> Sequence.next!() |> Sequence.next!()
iex> seq_n.value
102

Sequences count indices as they go - the first value will always be at index 0:

iex> {_v, seq} = Sequence.create(Sequence.Basic, :whole_numbers) |> Sequence.next()
iex> seq.value
1
iex> seq.index
0

Some sequences start at an index other than 0, like the Sum of Odd Divisors (A000593):

iex> seq = Sequence.create(Sequence.OEIS.Core, :a000593) |> Sequence.next!()
iex> seq.index
1

Working with Sequences

Creating Sequences

  • create/3 - Create a new sequence instance

Finding and Inspecting Sequences

  • available/0 - List all loaded sequences from all loaded applications and modules
  • available/1 - List available sequences from a specific module
  • has_next?/1 - Check that a sequence has at least one more available value
  • is_available?/2 - Check if a specific sequence is available
  • is_finite?/1 - Check of a sequence is finite or infinite in the Sequence library implementation
  • is_instance?/2 - Check if a sequence is an instance of a specific sequence identifier
  • is_instance?/3 - Check if a sequence is an instance of a specific sequence identifier
  • get_references/1 - Retrieve reference sources and links for a sequence
  • has_reference?/2 - Check if a sequence has a specific reference source
  • readable_name/1 - Find the human readable name of a sequence

Enumerating / Manipulating Sequences

  • at/2 - Access the n-th value of a sequence from the current location
  • drop/2 - Drop N values from the front of the sequence
  • map/2 - Apply a function to values in a sequence, and collect the result
  • next/1 - Retrieve the next sequence value and updated sequence struct as a tuple
  • next!/1 - Retrieve the next sequence as just an updated sequence struct
  • restart!/1 - Restart a sequence at it's initial value/index
  • start/1 - Start iteration of a sequence
  • take/2 - Like Enum.take/2 - retrieve a list of values from a sequence
  • take!/2 - Like take/2, but only return the updated sequence struct

Developing Sequences

Developing New Sequences

Sequences can be built in three different ways:

  • Verbose Iterators (See Sequence.Basic.create_sequence_whole_numbers/1)
  • Simple Functions (See sequence_for_function/1 and Sequence.OEIS.Core.create_sequence_a000045/1)
  • Static Lists (See sequence_for_list/1 and Sequence.Basic.create_sequence_empty/1)

All three ways of developing sequences have things in common:

  • All will have a create_sequence_*/1 function - this is how the Sequence package finds and instantiates sequences
  • All will have common function attributes (via data @doc attributes) for the create_sequence_*/1 function

Sequence Attributes

Top level create_sequence_* functions should have the following attributes:

  • sequence - Readable name of the sequence
  • references - List of tuples of the form {:source, :short_name, "URI"}

along with the required attributes, the following optional attributes can be used:

  • offset - Integer. Default 0. Start a sequence at an index other than 0.

Verbose Iterators

The most expressive way to write sequences is using the Verbose Iterators technique. In this style, the create_sequence_*/1 function returns a map that is used to build out the %Sequence{} struct. Let's take apart the :whole_numbers sequence in the Sequence.Basic module.

The create function looks like:

@doc sequence: "Whole numbers: [1, 2, 3, 4, 5, ...]", references: [{:wolfram, :positive_integer, "http://mathworld.wolfram.com/PositiveInteger.html"}, {:wikipedia, :natural_number, "https://en.wikipedia.org/wiki/Natural_number"}]
def create_sequence_whole_numbers(opts \ []) do
    start = opts |> Keyword.get(:start, 1)
    
   %{
       next_fn: &seq_whole_number_next/3,
       data: %{
           start: start
       }
   } 
end   

Starting with the @doc attributes, and working through the whole function:

  1. The @doc attribute defines the sequence readable name with the sequence: attribute
  2. The @doc attribute defines references for the sequence with the references: attribute (See get_references/1)
  3. The function signature create_sequence_whole_numbers/1 defines the sequence identifier whole_numbers - this is automatically translated by the Sequence module functions
  4. For this sequence, we check for a start option - what value our sequence should start at
  5. We return a Map with two keys, the next_fn which is a function with arity 3, and a data attribute, which we can use to store any data we like

Every next_fn (or iterator function) for a sequence can be called in two ways; :init mode and :next mode. The :init mode is used to prime the value and our state, the data attribute we set in our create function. In :next mode we generate the next value in our sequence. Both modes can update the data state map, which is maintained by the sequence functions. This allows more complex information needed to generate values to be stored.

Let's take a look at how the seq_whole_number_next/3 function we referenced handles the two modes for the :whole_numbers sequence:

defp seq_whole_number_next(:init, data, _value) do
    %{
        data: data,
        value: data.start - 1
    }
end

defp seq_whole_number_next(:next, data, value) do
   {
       :continue,
       %{data: data, value: value + 1}
   } 
end   

In this case we use two different functions to handle the :init and :next states. In the :init we update our value, and return our state (the data attribute) as is. When generating the next iteration value in :next mode, we refernce the value parameter, which was the last calculated value for the sequence. In this case we return a slightly different structure: we explicitly let the iterator know that our sequence can continue further - we have more values.

The return value of the next_fn iterator can be of three forms. Map form:

%{data: %{}, value: _}

Continue tuple form:

{
    :continue,
    %{data: %{}, value: _}
}

or Last tuple form:

{
    :last,
    %{data: %{}, value: _}
}

If the :last tuple form is returned, the sequence is marked as finished, and no further values will be generated.

Simple Functions

Some sequences can be generated from a single function, like many mathematical progressions. The Verbose Iterator way of writing a sequence would be overkill - we can use the sequence_for_function/1 utility for wrapping a single function in a sequence. See sequence_for_function/1 for more details.

As a simple example we can look at how the OEIS Fibonacci sequence is implemented:

@doc sequence: "OEIS A000045 - Fibonacci Numbers [0, 1, 1, 2, 3, 5, ...]", references: [{:oeis, :a000045, "https://oeis.org/A000045"}]
def create_sequence_fibonacci(_opts) do
    sequence_for_function(&seq_a000045/3)
end

def seq_a000045(idx, a, b) do
    case idx do
        0 -> 0
        1 -> 1
        _ -> a + b
    end
end  

Like when writing a sequence as a Verbose Iterator, we have a create_sequence_*/1 function with @doc attributes that define the sequence name and references. Then we provide the sequence_for_function/1 function with a reference to the function that generates values - in this case a fibonacci function that uses the index and last two calculated values to determine the next value in the sequence.

Static Lists

For sufficiently small finite lists, we can use the sequence_for_list/1 utility function to wrap a list of static values as a sequence. See sequence_for_list/1 for more details.

We can take apart the :decimal_digits sequence:

@doc sequence: "Decimal digits: [0, 1, 2, 3, 4, ...]", references: [{:wikipedia, :decimal_digit, "https://en.wikipedia.org/wiki/Numerical_digit"}]
def create_sequence_decimal_digits(_opts) do
    sequence_for_list([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
end   

This sequence has the expected sequence: and references: doc attributes, and then a call to the sequence_for_list/1 function with a list of data.

Link to this section Summary

Functions

Find a value in the sequence offset from the current location.

Scan all loaded applications and modules for sequences.

List available sequence in a specific module.

Instantiate an instance of a sequence from a sequence bundle map.

Instantiate an instance of a sequence.

Drop values from the front of the sequence, returning the updated sequence struct. Like Enum.drop/2.

Retrieve sequence reference data from a sequence description dictionary, like those returned by available/1 and available/0.

Check that a sequence has more values.

Check if a sequence instance has a reference to a specific source.

Determine if a specific sequence is available.

Determine if a sequence is finite or infinite.

Check if a sequence struct is an instance of a specific sequence.

Check if a sequence struct is an instance of a specific sequence.

Apply a function to every value in a Sequence. Like Enum.map/2.

Iterate a sequence, returning the sequence value and updated sequence struct.

Iterate a sequence, returning only the updated Sequence structure.

Get a human readable name for a sequence.

Restart a sequence that has already been iterated.

Create a new sequence from a simple function and an information function.

Create a sequence that references a static list of data.

Start iteration of a created sequence. This is an alias of next!/1.

Retrieve the first N values from a sequence, like Enum.take/2.

Retrieve the next count values from a sequence, without retaining sequence state.

Link to this section Functions

Link to this function

at(seq, idx, opts \\ [])

View Source

Find a value in the sequence offset from the current location.

Like Enum.at, this function retrieves the value at the given index, offset from the current location in the sequence. This does not advance the sequence iterator, and so provides different functionality than simply calling drop(n) |> take(1).

If a sequence is finite. and the index provided is beyond the end of the sequence, a nil value is returned.

This function uses a timeout during processing - if iterating to and retrieving the target value takes longer than the timeout, the return value from the function will be :timeout. The default timeout is 1,000 milliseconds.

Options

  • timeout. Integer. Default 1_000. Number of milliseconds to iterate for a value before stopping

Examples

iex> seq = Sequence.create(Sequence.Basic, :whole_numbers) |> Sequence.start()
iex> seq |> Sequence.at(10)
11
iex> seq.index
0
iex> seq.value
1

iex> seq = Sequence.create(Sequence.Basic, :decimal_digits) |> Sequence.start()
iex> seq |> Sequence.at(20)
nil
iex> seq.index
0
iex> seq.value
0

iex> seq = Sequence.create(Sequence.Basic, :whole_numbers) |> Sequence.start()
iex> seq |> Sequence.at(1_000_000, timeout: 5)
:timeout

Scan all loaded applications and modules for sequences.

This is almost identical to the available/1 function, but it scans a selection of loaded applications and modules, excluding the core libraries, for sequences. The return values are the same as for available/1.

List available sequence in a specific module.

Sequences are organized into modules, with sequences of different sources or shapes in different modules. Sequences built or included via alternate packages can also be queried this way.

Example

 iex> Sequence.available(Sequence.Basic) |> length()
 3

 iex> Sequence.available(Sequence.Basic) |> List.last()
 %{description: "Whole numbers: [1, 2, 3, 4, 5, ...]", name: "Whole Numbers", sequence: :whole_numbers, module: Sequence.Basic}

Instantiate an instance of a sequence from a sequence bundle map.

The sequence bundle map is the set of data returned for each sequence by functions like Sequence.available/0 or Sequence.OEIS.find_sequence!/1. Actual instance initialization and creation is handed off to Sequence.create/3.

Examples

iex> seq = Sequence.OEIS.find_sequence!(:a000012) |> Sequence.create()
iex> seq |> Sequence.readable_name()
"The simplest sequence of positive numbers: the all 1's sequence."
Link to this function

create(module, seq_name, opts \\ [])

View Source

Instantiate an instance of a sequence.

iex> seq = Sequence.create(Sequence.Basic, :empty)
iex> seq |> Sequence.readable_name()
"Empty sequence: []"

iex> seq = Sequence.create(Sequence.OEIS.Core, :fibonacci)
iex> seq |> Sequence.take!(10)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Some sequences accept options, which change the progression or shape of the generated sequence data:

iex> Sequence.create(Sequence.Basic, :whole_numbers) |> Sequence.take!(5)
[1, 2, 3, 4, 5]

iex> Sequence.create(Sequence.Basic, :whole_numbers, [start: 13]) |> Sequence.take!(5)
[13, 14, 15, 16, 17]

Finding Available Sequences

Sequences are referenced by Module name and a sequence identifier.

The documentation for the Sequence sub-modules contains reference data for all available sequences. See:

Available sequences can be found with available/1:

iex> Sequence.available(Sequence.Basic) |> List.first()
%{description: "Decimal digits: [0, 1, 2, 3, 4, ...]", name: "Decimal Digits", sequence: :decimal_digits, module: Sequence.Basic}

The function name within a module can also be used to identify a sequence, as all sequence generation functions follow the format create_sequence_*. The OEIS fibonacci sequence is indirectly created via the function Chunky.Sequence.OEIS.Core.create_sequence_fibonacci/1.

Drop values from the front of the sequence, returning the updated sequence struct. Like Enum.drop/2.

Examples

iex> seq = Sequence.create(Sequence.Basic, :whole_numbers)
iex> seq |> Sequence.drop(10) |> Sequence.take!(10)
[11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
Link to this function

get_references(sequence)

View Source

Retrieve sequence reference data from a sequence description dictionary, like those returned by available/1 and available/0.

Examples

iex> %{module: Chunky.Sequence.OEIS.Factors, sequence: :a052486} |> Sequence.get_references()
[{:oeis, :a052486, "https://oeis.org/A052486"}, {:wikipedia, :achilles_number, "https://en.wikipedia.org/wiki/Achilles_number"}]

Check that a sequence has more values.

Some sequences are finite, and have distinct end points. For finite sequences it can make sense to check for additional values before trying to access more data:

iex> seq = Sequence.create(Sequence.Basic, :empty) |> Sequence.next!()
iex> seq |> Sequence.has_next?()
false

iex> seq = Sequence.create(Sequence.Basic, :whole_numbers) |> Sequence.next!()
iex> seq |> Sequence.has_next?()
true
Link to this function

has_reference?(sequence, seq_type)

View Source

Check if a sequence instance has a reference to a specific source.

See also get_references/1.

Examples

iex> Sequence.create(Sequence.Basic, :whole_numbers) |> Sequence.has_reference?(:wolfram)
true

iex> Sequence.create(Sequence.OEIS.Core, :a000045) |> Sequence.has_reference?(:oeis)
true

iex> Sequence.create(Sequence.OEIS.Core, :a000045) |> Sequence.has_reference?(:wolfram)
false
Link to this function

is_available?(module, seq_name)

View Source

Determine if a specific sequence is available.

Example

iex> Sequence.is_available?(Sequence.OEIS.Core, :fibonacci)
true

iex> Sequence.is_available?(Sequence.OEIS.Core, :quadronacci)
false

Determine if a sequence is finite or infinite.

A sequence being finite or infinite is not a reflection of the absolute computability or theoretical limits of a sequence, but is determined by the quantity of values available in the Sequence library implementation.

Examples

iex> Sequence.create(Sequence.OEIS.Core, :fibonacci) |> Sequence.is_finite?()
false

iex> Sequence.create(Sequence.OEIS.Core, :a000001) |> Sequence.is_finite?()
true
Link to this function

is_instance?(sequence, arg)

View Source

Check if a sequence struct is an instance of a specific sequence.

See also is_instance?/3.

Example

iex> seq = Sequence.create(Sequence.OEIS.Core, :a000045)
iex> seq |> Sequence.is_instance?({Sequence.OEIS, :a000066})
false
iex> seq |> Sequence.is_instance?({Sequence.OEIS.Core, :a000045})
true
Link to this function

is_instance?(sequence, module, seq_name)

View Source

Check if a sequence struct is an instance of a specific sequence.

See also is_instance?/2.

Example

iex> seq = Sequence.create(Sequence.OEIS.Core, :a000045)
iex> seq |> Sequence.is_instance?(Sequence.OEIS, :a000066)
false
iex> seq |> Sequence.is_instance?(Sequence.OEIS.Core, :a000045)
true

Apply a function to every value in a Sequence. Like Enum.map/2.

For infinite sequences, this will never complete. Use Sequence.take/2 and apply a normal map.

Examples

iex> Sequence.create(Sequence.Test, :list_medium) |> Sequence.map(fn v -> v * 3 end)
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30]

Iterate a sequence, returning the sequence value and updated sequence struct.

The sequence struct needs to be maintained in order to iterate a sequence; with the next/1 function, you can access the iterated value and the updated struct with the same call.

Example

iex> seq = Sequence.create(Sequence.Basic, :whole_numbers)
iex> {val, _updated_seq} = seq |> Sequence.next()
iex> val
1

Iterate a sequence, returning only the updated Sequence structure.

The Sequence structure maintains the last index and value seen in a sequence, so is a useful shorthand when explictly returning the sequence value isn't necessary. This also makes it possible to chain together calls to next!/1 and other sequence methods.

Examples

iex> seq = Sequence.create(Sequence.Basic, :whole_numbers)
iex> updated_seq = seq |> Sequence.next!()
iex> updated_seq.value
1

Get a human readable name for a sequence.

Example

iex> Sequence.create(Sequence.OEIS.Core, :a000045) |> Sequence.readable_name()
"OEIS A000045 - Fibonacci Numbers [0, 1, 1, 2, 3, 5, ...]"

Restart a sequence that has already been iterated.

This returns a new instance of the sequence, as it would be initialized from create/3.

Examples

iex> seq = Sequence.create(Sequence.Basic, :whole_numbers) |> Sequence.start() |> Sequence.drop(30)
iex> seq.value
31
iex> seq = seq |> Sequence.restart!()
iex> seq.value
1
Link to this function

sequence_for_function(seq_function)

View Source

Create a new sequence from a simple function and an information function.

The simple sequence function should take one or more parameters, corresponding to the last N values of the sequence.

Configuration

Configuration values for the sequence function can be provided as @doc string attributes on the function. Supported attributes are:

  • fill_value - Integer. Optional. Default 0. Filler value used as parameters to sequence function when existing values haven't yet been calculated
  • sequence - String. Required. Short sequence summary.
  • data - Map. Optional. Static information to add to sequence data attribute

Sequence Function

The sequence function will be called with the index of the current value to calculate, and the last N values of the function, where N is the function arity minus one. For example, to setup a function that adds the last three values in the sequence, with the first three values being 1, 1, 1, we could write the function:

def add_last_three(_idx, a, b, c) do
    case {a, b, c} do
        {0, 0, 0} -> 1
        {0, 0, _} -> 2
        {0, _, _} -> 3
        {va, vb, vc} -> va+vb+vc
    end
end

In this case we don't use the index of the function at all. Using the sequence_for_function/1 call we could register this sequence inside a create function (so that it can be properly instantiated):

def create_sequence_add_last_three(_opts) do
    sequence_for_function(&add_last_three/4)
end

Note that the create_sequence_add_last_three/1 function should have proper sequence and references attributes. See the Developing Sequences section for more details.

Attributes

For the @doc attributes of the sequence function to readable, the function reference passed to the sequence_for_function/1 function must be scoped to a module or module alias, not a bare referene. This means that this will work:

sequence_for_function(&Sequence.Test.seq_last_three/4)

but the following will not:

sequence_for_function(&seq_last_three/4)

Offset Attribute

Some sequences don't start counting at index 0. When a different offset is required than the default 0, the offset attribute can be used. This attribute needs to be set on the create_sequence_* function as well as the simple sequence function:

@doc offset: 2
def create_sequence_count_from_two(_opts) do

end

@doc offset: 2
def seq_count_from_two(idx) do
    # first _idx_ received will be 2
end

Data attribute

If the sequence function uses a data attribute, the call signature of the sequence function changes. For example, a simple function using the last two values, with no data attribute, will have a function signature like:

def seq_no_data_last_two(idx, a, b) do
    ...
end

While a sequence function that has a data attribute will look like:

@doc data: %{start_values: [1, 2, 3]}
def seq_with_data_last_two(data, idx, a, b) do
    ...
end

Fill Value attribute

Sequence functions that reference past values are normally called with 0 if some of the past values haven't yet been created. The fill_value doc attribute can be used to change the default value.

For instance, on the first call to iterate the sequence:

def seq_defaults_last_two(idx, a, b) do
    # a == 0
    # b == 0
end

but when we change the fill_value:

@doc fill_value: 17
def seq_fill_last_two(idx, a, b) do
    # a == 17
    # b == 17
end
Link to this function

sequence_for_list(list_data)

View Source

Create a sequence that references a static list of data.

In the case of suitable small finite sequences, it is significantly easier to use a static list of data as a sequence. The sequence_for_list/1 function provides a simple way to do this. For example, to create a sequence for a short list of 5 numbers:

def create_sequence_my_short_list(_opts) do
    sequence_for_list([3, 7, 11, 27, 47])
end

Like with the sequence_for_function/1 example, the above example create_sequence_* function should include full sequence and references doc attributes. See the Developing Sequences section for more details.

Sequences generated from a list behave just like any other sequence.

Attributes of List Sequences

Finite Sequence

A sequence created from a list is automatically marked as finite. See is_finite?/1.

Offset Attribute

Some sequences don't start counting at index 0. When a different offset is required than the default 0, the offset attribute can be used. This attribute needs to be set on the create_sequence_* function.

@doc offset: 2
def create_sequence_count_from_two(_opts) do
    sequence_from_list([0, 0, 200, 201, 203, 204])
end

Start iteration of a created sequence. This is an alias of next!/1.

For readability it can be useful to have a separate start function to call on a newly generated sequence.

Examples

iex> seq = Sequence.create(Sequence.Basic, :whole_numbers) |> Sequence.start()
iex> seq.value
1
iex> seq.index
0

Retrieve the first N values from a sequence, like Enum.take/2.

This version of the take function returns a separate list of values and the sequence state. See also Sequence.take!/2.

Examples

iex> seq = Sequence.create(Sequence.Basic, :whole_numbers)
iex> {values, _n_seq} = seq |> Sequence.take(10)
iex> values
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

iex> seq = Sequence.create(Sequence.OEIS.Core, :fibonacci)
iex> {values, _n_seq} = seq |> Sequence.take(10)
iex> values
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Retrieve the next count values from a sequence, without retaining sequence state.

This function behaves similarly to the take/2 function, but the state of the sequence isn't maintained, so any subsequent calls to the same sequence will still contain the values retrieved with take!.

Examples

iex> seq = Sequence.create(Sequence.Basic, :whole_numbers)
iex> seq |> Sequence.take!(5)
[1, 2, 3, 4, 5]
iex> seq |> Sequence.take!(10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]