Robopupu.FRP API

Robopupu.FRP API is a simple library for Functional Reactive Programming (FRP). The API is based on creating graphs of nodes for performing a specified computation. A graph consists of an arbitrary number of nodes. Each graph has a single begin node and any number of end nodes. Each node is essentially a function which takes an input value and produces an output value from it. The following image illustrates an example structure of a such graph:

robopupu_graph

Robopupu.FRP API provides a collection of pre-defined node types for various purposes and tasks, for instance:

  • ActionNode: Takes a value of any type as an input, performs a defined action, and emits the input value as an output. The action is typically defines as a lambda expression.
  • FunctionNode: Takes a value of a specified type as an input and computes the output value of a specified type from the input using a given function. The function is typically defined as a lambda expression.
  • FilterNode: Takes a value of a specified type as an input and computes a specified condition to determine if the received input is emitted as an output to succeeding node(s).
  • ListNode: Takes a given list of values and emits them as output values one by one.
  • RequestNode: Executes asynchronously a given REST request when triggered, and emits the response for the request as an output.
  • TimerNode: Starts a timer with specified timeout delay. Emits the timer expiration time as an output. A created timer can be optionally a periodical timer with a specified interval.
  • ZipNode: Takes multiple input values and combines them into a single output value using a specified function. Up to nine inputs can be combined.
  • ConditionNode: Emits a boolean value as an output from an input value using a specified boolean function. The boolean function is typically defined as a lambda expression.
  • TakeNode: Takes a specified number of input values and emits them to succeeding nodes.
  • SkipNode: Skips a specified number of input values before it starts emitting succeeding input values as output values to succeeding nodes.
  • BufferNode: Buffers a specified number of input values, before starts emitting them one by one as output values to succeeding nodes.

Example Graph

Here is a simple Activity implementation that request a random Chuck Norris joke from The Internet Chuck Norris API when the user clicks on a FloatingActionButton (FAB). The received joke is then displayed on the UI. The code line constructing the Graph that gets the described task done, is marked with blue colour. The request is constructed using Robopupu REST.API:


 

public class MainActivity extends AppCompatActivity {

    ...

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        final FloatingActionButton fab = 
            (FloatingActionButton) findViewById(R.id.fab);
        ...

        final RequestSpec<JokeResponse> getJoke =
            new RequestSpec<JokeResponse>(this, "http://api.icndb.com/jokes/random").
                    request(new GsonRequest<>(JokeResponse.class));

        Graph.onClick(fab). // When the FAB is clicked,
            request(getJoke). // perform request GetJoke, and
            action(this::displayJoke); // when a response is received, perform
                                        // the action to display the received joke.
    }

    private void displayJoke(final JokeResponse response) {
        ...
    }
}

 

Key Classes

The following class diagram depicts the key classes of the FRP API. The classes with light blue background are all extended from the AbstractNode base class (the generalisation relationship are left out for clarity):

frp_key_classes

  • Graph: A Graph contains one or more instances of Node. One of the node instances is a begin node. A graph may contain any number of end nodes.
  • OutputNode: Defines an interface for nodes that are capable of emitting output values to its attached InputNode(s).
  • InputNode: Defines an interface for nodes that are capable of receiving input values.
  • Node: An interface for nodes that implement both the InputNode and OutputNode interfaces. Most of the concrete node implementations implement the Node interface.
  • Tag: An object for tagging Node instances added to a Graph so that they can be identified and obtained later on. Tags are need to construct branching graphs in linear code.
  • Action: An interface that defines an action to be performed. Typically an action is defined as a lambda expression (see Retrolamba), but can be also implemented as a class that implements interface Function.
  • Function: An interface that defines a function that produces an output value from the given input value. Typically a function is defined as a lambda expression but can be also implemented as a class that implements interface Function.
  • Callback: An interface for delegating the key Node method invocations: onInput(IN), onError(Throwable), and onCompleted() to a listener object that implements interface Graph.Callback.
  • RequestDelegate: An interface that can be used to execute a specified REST request. (See Robopupu.REST API).

OutputNode

OutputNode is a node that is capable of emitting output values to its attached InputNode(s):


 

package com.robopupu.api.graph;

public interface OutputNode<OUT> {

    /**
     * Sets the {@link Graph} that contains this {@link OutputNode}.
     * @param graph A {@link Graph}.
     */
    void setGraph(Graph<?> graph);

    /**
     * Attaches the given {@link InputNode} to this {@link OutputNode}.
     * @param inputNode An {@link InputNode}.
     */
    void attach(InputNode<OUT> inputNode);

    /**
     * Detaches the given {@link InputNode} from this {@link OutputNode}.
     * @param inputNode An {@link InputNode}.
     */
    void detach(InputNode<OUT> inputNode);

    /**
     * Invoked to emit available output value(s) from this {@link OutputNode}.
     */
    void emitOutput();

    /**
     * Invoked to reset this {@link OutputNode}.
     */
    void onReset();
}

 

InputNode

InputNode is an interface for nodes that are capable of receiving input values. An InputNode can also receive a notification of Graph completion as an invocation to method onCompleted(OutputNode), and error specified as Throwable via method onError(OutputNode, Throwable).


package com.robopupu.api.graph;


public interface InputNode<IN> {

    /**
     * Sets the {@link Graph} that contains this {@link InputNode}.
     * @param graph A {@link Graph}.
     */
    void setGraph(Graph<?> graph);

    /**
     * Invoked by the given source {@link OutputNode} for this {@link InputNode} to receive the input
     * {@link Object} that was emitted by the {@link OutputNode}.
     * @param source The @link OutputNode}.
     * @param input The input {@link Object}.
     * @return  This {@link InputNode}.
     */
    void onInput(OutputNode<IN> source, IN input);

    /**
     * Invoked when the specified source {@link OutputNode} is completed.
     * @param source The completed {@link OutputNode}.
     */
    void onCompleted(OutputNode<?> source);

    /**
     * Invoked when the given source {@link OutputNode} has detected an error.
     * @param source The {@link OutputNode} notifying about error.
     * @param throwable A {@link Throwable} representing the detected error.
     */
    void onError(OutputNode<?> source, Throwable throwable);

    /**
     * Invoked to reset this {@link InputNode}.
     */
    void onReset();
}

 

Node

An interface for nodes that implement both the InputNode and OutputNode interfaces. Most of the pre-defined node classes implement the Node interfaceClass AbstractNode provides a base class for implementing Nodes.


package com.robopupu.api.graph;

public interface Node<IN, OUT> extends InputNode<IN>, OutputNode<OUT> {
}

 

Function

An interface that defines a function that produces an output value from the given input value. Typically a function is defined as a lambda expression but can be also implemented as a class that implements interface Function.


package com.robopupu.api.graph;

/**
 * Defines an function interface for declaring function objects and lambdas.
 */
public interface Function<IN, OUT> {

    /**
     * Evaluates a {@link Function} using the given input.
     * @param input An input value of type {@code IN}.
     * @return The output value of type {@code OUT}.
     */
    OUT eval(IN input);
}

 

Action

Action interface can be used to define some action to be performed. Typically an action is defined as a lambda expression (see Retrolamba), but can be also implemented as a class that implements interface Action.


package com.robopupu.api.graph;

public interface Action<IN> {

    /**
     * Executes an {@link Action} using the given input.
     * @param input An input value of type {@code IN}.
     */
    void execute(IN input);
}

 

Graph

Class Graph (class source code and javadoc) is used as a kind of builder object for constructing a directed graph of Nodes that performs some specified computational task. The Nodes added to a Graph are chained to each other as illustrated in the first image.

The following test cases provide some simple examples how to construct graphs for performing various tasks. The examples make use of lambda expressions. For writing lambda expression in Android  we need to use Retrolambda library.


@Test
public void sumOfIntegers() {
    final int sumOfIntegers = Graph.begin(1, 2, 3).sum().intValue();
    assertEquals(6, sumOfIntegers);
}

@Test
public void mapFromIntToString() {
    final String string = Graph.begin(123).stringValue();
    assertEquals("123", string);
}

@Test
public void filterIntValues() {
    final List<Integer> integers = Graph.begin(1, 2, 3, 4, 5, 6).
        filter(value -> value % 2 == 0).toList();
    assertTrue(equals(integers, 2, 4, 6));
}

@Test
public void mapIntValuesToChars() {
    final List<Character> chars = Graph.begin(1, 2, 3, 4, 5, 6).
        map(value -> (char)('A' + value - 1)).toList();
    assertTrue(equals(chars, 'A', 'B', 'C', 'D', 'E', 'F'));
}

@Test
public void skipIntValues() {
    final List<Integer> integers = 
        Graph.begin(1, 2, 3, 4, 5, 6).skip(3).toList();
    assertTrue(equals(integers, 4, 5, 6));
}

@Test
public void takeIntValues() {
    final List<Integer> integers = 
        Graph.begin(1, 2, 3, 4, 5, 6).take(3).toList();
    assertTrue(equals(integers, 1, 2, 3));
}

@Test
public void repeatIntValues() {
    final List<Integer> integers = 
        Graph.begin(1, 2, 3, 4, 5, 6).repeat(2).toList();
    assertTrue(equals(integers, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6));
}

@Test
public void zipThreeStrings() {
    final Zip3Node<String, String, String, String> zip =
        new Zip3Node<>((in1, in2, in3) -> in1 + in2 + in3);
    final Tag<String> list  = Tag.create();

    final String string = Graph.begin(list, "Robopupu ", "is ", "awesome").
        node(list).nth(1).to(zip.in1).
        node(list).nth(2).to(zip.in2).
        node(list).nth(3).to(zip.in3).stringValue();

    assertEquals("Robopupu is awesome", string);
}

@Test 
public void concatThreeStrings() { 
    final String string = 
        Graph.begin("Robopupu ", "is ", "awesome").concat().stringValue(); 
    assertEquals("Robopupu is awesome", string); 
}

@Test
public void mapIntValuesToChars() {
    final List<Character> chars = Graph.begin(1, 2, 3, 4, 5, 6).
        map(value -> (char)('A' + value - 1)).toList();
    assertTrue(equals(chars, 'A', 'B', 'C', 'D', 'E', 'F'));
}

@Test
public void convertCharValuesToString() {
    final String string = Graph.begin('A', 'B', 'C', 'D', 'E', 'F').
        string().concat().stringValue();
    assertEquals("ABCDEF", string);
}

@Test
public void reverseString() {
    final String string = Graph.begin("upupoboR").
        map(value -> new StringBuilder(value).reverse()).stringValue();
    assertEquals("Robopupu", string);
}

@Test
public void firstIntValue() {
    final List<Integer> integers = 
        Graph.begin(1, 2, 3, 4, 5, 6).first().toList();
    assertTrue(equals(integers, 1));
}

@Test
public void distinctIntValues() {
    final List<Integer> integers = 
        Graph.begin(1, 2, 3, 1, 6, 4, 5, 3).distinct().toList();
    assertTrue(equals(integers, 1, 2, 3, 6, 4, 5));
}

@Test
public void reversedIntValues() {
    final List<Integer> integers = 
        Graph.begin(1, 2, 3, 4, 5, 6).reverse().toList();
    assertTrue(equals(integers, 6, 5, 4, 3, 2, 1));
}

@Test
public void listToList() {
    final List<Integer> integers = Graph.begin(1, 2, 3, 4, 5, 6).toList();
    assertTrue(equals(integers, 1, 2, 3, 4, 5, 6));
}

@Test
public void firstGreaterThanThreeIntValue() {
    final List<Integer> integers = 
        Graph.begin(1, 2, 3, 4, 5, 6).first(value -> value > 3).toList();
    assertTrue(equals(integers, 4));
}

@Test
public void lastIntValue() {
    final List<Integer> integers = 
        Graph.begin(1, 2, 3, 4, 5, 6).last().toList();
    assertTrue(equals(integers, 6));
}

@Test
public void lastLesserThanFourIntValue() {
    final List<Integer> integers = 
        Graph.begin(1, 2, 3, 4, 5, 6).last(value -> value < 4).toList();
    assertTrue(equals(integers, 3));
}

Tag

Instances of class Tag are used to tag Node instances that are added to a Graph. Tagging is needed to access specific Nodes from a Graph. This in turn makes it possible to construct branches in the Graph originating from some Node. The following code constructs a graph structure using tags authToken, http400, and http401. The resulting structure is illustrated in the following image.


final Tag<Response> authToken = Tag.create();
final Tag<Response> http400 = Tag.create();
final Tag<Response> http401 = Tag.create();
final Graph<Response> graph = 
    Graph.begin(authToken, response -> authenticator.onRequestAuthCode());

graph.
    node(authToken).filter(response -> response.statusCode == 200).end(authenticator::onAuthenticationSucceeded).

    node(authToken).tag(http400).filter(response -> response.statusCode == 400).
        node(http400).filter(Error.A::is).end(authenticator::onAuthenticationFailed).
        node(http400).filter(Error.B::is).end(authenticator::onAuthenticationFailed).

    node(authToken).tag(http401).filter(response -> response.statusCode == 401).
        node(http401).filter(Error.C::is).end(authenticator::onAuthenticationFailed).
        node(http401).filter(Error.D::is).end(authenticator::onAuthenticationFailed).
        node(http401).filter(Error.E::is).end(authenticator::onAuthenticationFailed);

tagged_nodes

Retrolambda

The examples given above use Retrolambda, and it is highly recommend to use it with the FRP API to avoid writing class implementations for Function and Action interfaces.