chunky v0.12.0 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:
Chunky.Sequence.Basic
- Simple integer sequences, and defined sequence progressionsChunky.Sequence.OEIS
- Integer Sequences from the Online Encyclopedia of Integer SequencesChunky.Sequence.Test
- Sequences for test purposes
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 modulesavailable/1
- List available sequences from a specific modulehas_next?/1
- Check that a sequence has at least one more available valueis_available?/2
- Check if a specific sequence is availableis_finite?/1
- Check of a sequence is finite or infinite in the Sequence library implementationis_instance?/2
- Check if a sequence is an instance of a specific sequence identifieris_instance?/3
- Check if a sequence is an instance of a specific sequence identifierget_references/1
- Retrieve reference sources and links for a sequencehas_reference?/2
- Check if a sequence has a specific reference sourcereadable_name/1
- Find the human readable name of a sequence
Enumerating / Manipulating Sequences
at/2
- Access then
-th value of a sequence from the current locationdrop/2
- Drop N values from the front of the sequencemap/2
- Apply a function to values in a sequence, and collect the resultnext/1
- Retrieve the next sequence value and updated sequence struct as a tuplenext!/1
- Retrieve the next sequence as just an updated sequence structrestart!/1
- Restart a sequence at it's initial value/indexstart/1
- Start iteration of a sequencetake/2
- LikeEnum.take/2
- retrieve a list of values from a sequencetake!/2
- Liketake/2
, but only return the updated sequence struct
Developing Sequences
sequence_for_function/1
- Wrap a function as a sequence - see Developing New Sequencessequence_for_list/1
- Wrap a list as a sequence - see Developing New 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
andSequence.OEIS.Core.create_sequence_a000045/1
) - Static Lists (See
sequence_for_list/1
andSequence.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 thecreate_sequence_*/1
function
Sequence Attributes
Top level create_sequence_*
functions should have the following attributes:
sequence
- Readable name of the sequencereferences
- List of tuples of the form{:source, :short_name, "URI"}
along with the required attributes, the following optional attributes can be used:
offset
- Integer. Default0
. Start a sequence at an index other than0
.
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:
- The
@doc
attribute defines the sequence readable name with thesequence:
attribute - The
@doc
attribute defines references for the sequence with thereferences:
attribute (Seeget_references/1
) - The function signature
create_sequence_whole_numbers/1
defines the sequence identifierwhole_numbers
- this is automatically translated by the Sequence module functions - For this sequence, we check for a
start
option - what value our sequence should start at - We return a Map with two keys, the
next_fn
which is a function with arity 3, and adata
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
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. Default1_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."
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]
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
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
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
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
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
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. Default0
. Filler value used as parameters to sequence function when existing values haven't yet been calculatedsequence
- String. Required. Short sequence summary.data
- Map. Optional. Static information to add to sequencedata
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
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]