Manipulating fields

The core aspect that makes this library awesome are transformations. It is very common to change in some way the data before displaying it or passing it to another function. To achieve this but keep the reactive nature of fields around, we must apply a transformation.

Note: all transformations are lazy by default. This means that they won't get computed until they are needed in some way (either someone tries to obtain the value or a listener is registered).

One-way transformations

link

In most cases, we'll only need a one-way transformation: this means that we get back a simple non-mutable Field, and that we only need to supply a single function that takes as input the value of our field and transforms it to another value.

This function can be called on any Field instance and accepts as single argument a function that converts a value of the original field to a new value, either of the same or different type. (Given a Field<T>, the function is defined as T -> O where O can be any type). The new field will then recalculate its value calling the provided function whenever it detects a change in the original field. For this reason transformation functions should always be pure (have no side effects).

Here's our first example:

import com.femastudios.dataflow.util.* fun main() { val number = mutableFieldOf(10) val isPositive /* : Field<Boolean> */ = number.transform { it > 0 } println(isPositive.value) //Prints true number.value = -5 println(isPositive.value) //Prints false }
MutableField<Integer> number = MutableField.of(10); Field<Boolean> isPositive = number.transform(n -> n > 0); System.out.println(isPositive.getValue()) //Prints true number.setValue(-5); System.out.println(isPositive.getValue()) //Prints false

A more advanced way of running a field transformation is through the then() function: it acts very similarly to transform(), but allows to return a Field on the transformation function. The returned field will recalculate its value both when the original field changes and when the returned field changes.

In the following example, suppose we have a class User that contains a property username : MutableField<String>:

import com.femastudios.dataflow.* import com.femastudios.dataflow.util.* class User(val id : Int, val username : MutableField<String>) fun main() { val currentUser = mutableFieldOf(User(6, mutableFieldOf("Tom"))) val currentUsername /* : Field<String> */ = currentUser.then { it.username } println(currentUsername.value) //Prints "Tom" //Changing current user val seven = User(7, mutableFieldOf("Dick")) currentUser.value = seven println(currentUsername.value) //Prints "Dick" //Changing username of current user seven.username.value = "Harry" println(currentUsername.value) //Prints "Harry" }
class User { public final int id; public final MutableField<String> username; public User(int id, MutableField<String> username) { this.id = id; this.username = username; } } MutableField<User> currentUser = MutableField.of(new User(6, MutableField.of("Tom"))); Field<String> currentUsername = currentUser.then(u -> u.username); System.out.println(currentUsername.getValue()); //Prints "Tom" //Changing current user User seven = new User(7, MutableField.of("Dick")); currentUser.setValue(seven); System.out.println(currentUsername.getValue()); //Prints "Dick" //Changing username of current user seven.username.setValue("Harry"); System.out.println(currentUsername.getValue()); //Prints "Harry"

In this example we have transformed a field that contains a user instance into a field that represents the current user's username.

Transforming more than one field at a time

link

Sometimes we want to create a new field that is calculated by using more than one field as its input. In this case we can call a transform or then function that accept a number of parameters and pass the respective values to the lambda.

import com.femastudios.dataflow.listen.* import com.femastudios.dataflow.util.* fun main() { val n1 = mutableFieldOf(1) val n2 = mutableFieldOf(2) val sum = transform(n1, n2) { a, b -> a + b } sum.listeners.addStrongly { newVal -> println("n1 + n2 = $newVal") } println("n1 = 3") n1.value = 3 println("\nn2 = 4") n2.value = 4 }
MutableField<Integer> n1 = MutableField.of(1); MutableField<Integer> n2 = MutableField.of(2); Field<Integer> sum = FieldUtils.transform(n1, n2, a, b -> a + b);

Two-way transformations

link

While most of the time we'll need to transform data only in one way, sometimes it could be useful to set a "transformed" value, that will work its way back to change the original data. This is obviously doable only in a MutableField<T> instance, providing two functions: a T -> O to transform the data, and an additional one O -> T to convert it backwards. The function name is twoWayTransform().

Here's an example:

import com.femastudios.dataflow.listen.* import com.femastudios.dataflow.util.* fun main() { val number = mutableFieldOf(5) val numberTwoTimes /* : MutableField<Int> */ = number.twoWayTransform({ it * 2 }, { it / 2 }) println(numberTwoTimes.value) //Prints 10 number.value = 10 println(numberTwoTimes.value) //Prints 20 numberTwoTimes.value = 50 println(number.value) //Prints 25 }
MutableField<Integer> number = MutableField.of(5); MutableField<Integer> numberTwoTimes = number.twoWayTransform(n -> n * 2, n -> n / 2); System.out.println(numberTwoTimes.getValue()); //Prints 10 number.setValue(10); System.out.println(numberTwoTimes.getValue()); //Prints 20 numberTwoTimes.setValue(50); System.out.println(number.getValue()); //Prints 25

Problems with two-way transformations

link

Two-way transformations have a few gotchas since not all transformations have a perfect opposite one.

For instance, in the above example, the given functions are not symmetric (think of odd numbers). What happens if we set 49 to numberTwoTimes?

When we change the value of numberTwoTimes to 49 the following steps occur:

  1. The second transformation function is evaluated with the parameter 49, which gives 24;
  2. 24 is set to the field number;
  3. The first transformation is computed with the new value of 24, which gives 48;
  4. 48 is set to the field numberTwoTimes;
  5. The second transformation function is once again evaluated with the parameter 48, which gives, again, 24;
  6. 24 is set to the field number, but since it already contained that value, the process stops.

So we effectively have two main problems with uneven transformation functions:

  • Value stability: after setting a value, a series of events is generated that effectively changes the value we just set.
  • Infinite ping-ponging: this more serious problem occurs if the transformation functions never reach an agreement on what the global state should be. For instance, imagine if both the transformation functions incremented the value by one: an endless loop between the two fields would ensue, consuming lots of CPU cycles and potentially freezing the program.

In order to avoid these problems we must either accurately choose our transformation functions to be perfectly symmetric, or accept the explained value stability problem. In any case, we should always provide functions that ultimately reach an agreement on the field values.

Exceptions in transformation functions

link

If a transformation function throws an exception that you want to handle, you must do it inside the transformation function itself. For example:

import com.femastudios.dataflow.listen.* import com.femastudios.dataflow.util.* fun main() { val str = mutableFieldOf("10"); val num = str.transform { try { it.toInt() } catch(nfe: NumberFormatException) { null //Default value } } num.listeners.addStrongly { newVal -> println("num = $newVal") } println("str = \"15\"") str.value = "15" println("\nstr = \"abc\"") str.value = "abc" println("\nstr = \"123\"") str.value = "123" }
MutableField<Stirng> str = MutableField.of("10"); Field<Integer> str = FieldUtils.transform(str, n -> { try { return Integer.parseInt(n); } catch(NumberFormatException nfe) { return null; //Default value } });

In the example we've captured the exception in the transformation function and provided a default value, so when the string in str is not parsable num will become null. If however you don't want to handle the exception, you can avoid the try...catch block, but it won't be caught anywhere.

What happens if an exception is thrown?

link

If an exception manages to "escape" the transformation function it will naturally be propagated up the stack to the caller that triggered the field reevaluation. Notice that since fields are lazy by nature the exception could bubble up in different places, and for this reason it shouldn't be caught. The only place to handle exceptions is inside the transformation functions themselves, as shown in the example above.