lib/behaviours/convertible.ex

defmodule Bio.Convertible do
  @moduledoc """
  Defines behavior for modules to act as a converter between sequences.

  The core of this base module is to provide the default `to/1` function. This
  will return the error tuple for undefined conversions. This alleviates the
  need of the user defined module to provide this implementation, and eliminates
  the possibility of the function `to/1` raising due to no matching clauses.

  To use this as a base for your converter you `use` the module and pass a block
  for defining the user-side `to/1` calls. For example:

  ``` elixir
  defmodule SomeConversion do
    use Bio.Convertible do
      def to(SomeModule), do: {:ok, &your_kmer_converter/2, 6}

      defp your_kmer_converter({:ok, kmers, data}, module) do
        # conversion logic
      end
    end
  end
  ```

  This defines the k-wise converter that will be used by
  `Bio.Polymer.convert/3`.

  The function you define for the actual conversion will be given an `:ok` tuple
  with the kmers and any additional data defined for the struct that you're
  converting. How this data is partitioned is managed by the `Bio.Polymeric`
  definition.

  As an example, the `Bio.Sequence.DnaStrand` struct simply drops the `sequence`
  key, retaining all other keys as the `data` given to the `fn/2` you define.
  This allows you to retain any relevant information for a newly created struct.

  If you wanted to define your own sequence, this then requires that you also
  implement the `Bio.Polymeric` interface. So if you were to implement
  `SomeSequence`, you would do the following:

  ``` elixir
  defmodule SomeSequence do
    @behaviour Bio.Sequential

    @impl Bio.Sequential
    def converter, do: SomeConversion

    # implementation of other callbacks
  end
  ```

  The `Bio.Sequential` behavior ensures that we implement the `converter/0`
  function which is called from the `Bio.Polymer` module. This in turn
  constructs the basic converter mechanic, and now you would implement
  `Bio.Polymeric`:

  ``` elixir
  defimpl Bio.Polymeric, for: SomeSequence do
    def kmers(seq, k) do
      # your logic for splitting the polymer into k sized kmers
    end

    def valid?(seq, alpha) do
      # your logic checking if the polymer is valid
    end

    def validate(seq, alpha) do
      # your logic for validating the polymer
    end
  end
  ```

  Now you can simply call:

  ``` elixir
  SomeSequence.new("some data")
  |> Bio.Polymer.convert(SomeModule)
  ```

  The `Bio.Polymer.convert/3` function now handles calling your conversion
  method `your_kmer_converter`. This will be called with the kmers generated by
  `Bio.Polymeric`'s implementation of `kmers/2`, which will be passed a value of
  `k=6`, as defined in the conversion callback.

  This is perhaps overly complex, but here are the design goals:

  1. Allow a user to define a conversion from one type to another, regardless of
  if that type is internal.
  2. Allow that definition to leverage existing protocols for e.g. kmers.
  3. Allow a dead simple interface for working with internal components when
  extensions aren't needed.

  A lot of complexity was pulled in to hit these targets, and I think that the
  goal is achieved. A user who doesn't need their own types can simply work with
  the `Bio.Sequence` types using the `Bio.Polymer` interface.

  However, if they want to define `e.g.` a converter between
  `Bio.Sequence.RnaStrand` and `Bio.Sequence.AminoAcid`, they can simply define
  a module for that conversion and pass it to the `Bio.Polymer.convert/3`
  function.

  The complexity only arises to the user when they need their own sequences.
  Hopefully they'd have appreciated the necessity of the complexity before then.
  """

  @doc """
  Defines the converter's k-wise conversion function

  This is called within the `Bio.Polymer.convert/3` function to acquire the
  k-wise conversion function for sequence to another.
  """
  @callback to(thing :: module()) ::
              {:ok, (term() -> term()), integer()} | {:error, :undef_conversion}

  defmacro __using__(opts) do
    block = Keyword.get(opts, :do, nil)

    quote do
      @behaviour Bio.Convertible
      unquote(block)

      def to(module), do: {:error, :undef_conversion}
    end
  end
end