Custom composable apps
======================

You can make a simple customised app using the ``user_function`` app. This is a wrapper class that takes a reference to your function and the input, output and data types. The resulting app can then become part of a composed function.

Defining a ``user_function`` requires you consider four things.

``func``
    A function you have written. This is required.

``input_types``
    A type, or collection of type that your function can handle. This setting dictates what other apps have an output that is a compatable input for your function.

``output_types``
    A type, or collection of type that your function produces. This setting dictates what other apps can have yours as input.

``data_types``
    The data class names, as strings, that your function can handle. Not required, but useful.

A simple example
----------------

We make a very simple function ``first4``, that returns the first 4 elements of an alignment.

.. jupyter-execute::
    :linenos:

    def first4(val):
        return val[:4]

Now we define a ``user_function`` instance that takes and returns an ``ALIGNED_TYPE``.

.. jupyter-execute::
    :linenos:

    from cogent3.app.composable import user_function, ALIGNED_TYPE

    just4 = user_function(
        first4,
        input_types=ALIGNED_TYPE,
        output_types=ALIGNED_TYPE,
        data_types="Alignment",
    )

The ``repr()`` of your ``user_function`` instance indicates the wrapped function and the module it's in.

.. jupyter-execute::
    :linenos:

    just4

You use it like all composable apps which we demonstrate using a small sample alignment.

.. jupyter-execute::
    :linenos:

    from cogent3 import make_aligned_seqs

    aln = make_aligned_seqs(
        data=dict(a="GCAAGCGTTTAT", b="GCTTTTGTCAAT"), array_align=False
    )
    result = just4(aln)
    result

Renaming sequences
------------------

This time we wrap a method call on a ``SequenceCollection`` (and the alignment sub-classes) for renaming sequences. We also illustrate here that to support both aligned and unaligned data types as input/output, we have to include these in the construction of the custom function.

.. note:: The ``SERIALISABLE_TYPE`` indicates the data has the ability to be converted to ``json``.

.. jupyter-execute::
    :linenos:

    from cogent3.app.composable import (
        user_function,
        ALIGNED_TYPE,
        SEQUENCE_TYPE,
        SERIALISABLE_TYPE,
    )

    def renamer(aln):
        """upper case names"""
        return aln.rename_seqs(lambda x: x.upper())

    rename_seqs = user_function(
        renamer,
        input_types=(ALIGNED_TYPE, SEQUENCE_TYPE),
        output_types=SERIALISABLE_TYPE,
        data_types=("SequenceCollection", "Alignment", "ArrayAlignment"),
    )
    result = rename_seqs(aln)
    result.names

A user function for with a different output type
------------------------------------------------

In this example, we make an function that returns ``DistanceMatrix`` of an alignment.

.. jupyter-execute::
    :linenos:

    from cogent3.app.composable import (
        user_function,
        ALIGNED_TYPE,
        PAIRWISE_DISTANCE_TYPE,
    )

    def _get_dist(aln):
        return aln.distance_matrix(calc="hamming", show_progress=False)

    get_dist = user_function(
        _get_dist,
        input_types=ALIGNED_TYPE,
        output_types=PAIRWISE_DISTANCE_TYPE,
        data_types=("Alignment", "ArrayAlignment"),
    )
    result = get_dist(aln)
    result
