Managing collections

Up until this point we've seen examples where fields where mainly containing very basic values such as Int or String. We'll now discuss how to manage collections inside fields.

First of all, recall that we can set only immutable values inside fields. For this reason Kotlin™ collections already provide some help since they already have the distinction between mutable and read-only collections.

Be careful: read-only doesn't necessarily mean immutable! We could have a read-only collection that changes because its backed by some other mutable collection. Also, we could easily cast a mutable collection to a read-only one. For this reasons you should try to user the idiomatic functions to create fields containing collections whenever possible.

Creating fields

link

The library provides multiple ways of idiomatically create fields of immutable collections:

listFieldOf(1, 2, 3); //Yields Field<List<Int>> mutableListFieldOf(1, 2, 3); //Yields MutableField<List<Int>>
FieldUtils.listFieldOf(1, 2, 3); //Yields Field<List<Integer>> FieldUtils.mutableListFieldOf(1, 2, 3); //Yields MutableField<List<Integer>>

Notice that in both cases the value is an immutable List, even if the field is itself mutable. To change the value we must create a new list.

Similar methods exist for Sets and Maps. Examples can be found here.

Transformations

link

Here we'll explain how to run transformations when our data starts getting a little bit more complicated. In all the following examples we'll obtain the same thing in different ways. Suppose we have this data structure:

class Movie(val name : MutableField<String>, val year : MutableField<Int>) val movies : Field<List<Movie>>
class Movie { public final MutableField<String> name; public final MutableField<Integer> year; //Constructor omitted } Field<List<Movie>> movies;

And we want to obtain a Field<List<String>> whose value will looks like this: ["Avatar 2009", "Titanic 1999", ...], and want this Field to automatically change as soon as the list of movie changes, or one of the properties of a movie changes.

In each section we'll use a more advanced and high-level function improving each time readability, ease-of-use and performance.

Using plain-old then()

link
import com.femastudios.dataflow.* import com.femastudios.dataflow.extensions.* import com.femastudios.dataflow.util.* class Movie(val name : MutableField<String>, val year : MutableField<Int>) val movies = listFieldOf( Movie(mutableFieldOf("Avatar"), mutableFieldOf(2009)), Movie(mutableFieldOf("Titanic"), mutableFieldOf(1999)) ) fun main() { val f = movies.then { list -> val allFields /* : List<Field<String>> */ = list.map { movie -> //The operator + is overridden for Field<String> movie.name + " (" + movie.year + ")" } transform(allFields) { it } //Yields Field<List<String>> } println(f.value) }
Field<List<String>> f = movies.then(list -> { List<Field<String>> allFields = list.stream().map(movie -> transform(movie.name, movie.year, (name, year) -> name + " (" + year + ")" ) ).collect(Collectors.toList()); return transform(allFields, l -> l); //Yields Field<List<String>> }); System.out.println(f.getValue());
  • Pros: uses already learnt paradigms.
  • Cons: difficult to read and write; recalculates all values for each change.

Snapshots

link

We'll now introduce a new concept: snapshots! When the value of a field is a collection we can call the snapshot() function: we'll pass a lambda that will be called once for each item in the collection. In this lambda you must return a field whose content will be captured by the snapshot.

The example will clarify:

import com.femastudios.dataflow.* import com.femastudios.dataflow.extensions.* import com.femastudios.dataflow.util.* class Movie(val name : MutableField<String>, val year : MutableField<Int>) val movies = listFieldOf( Movie(mutableFieldOf("Avatar"), mutableFieldOf(2009)), Movie(mutableFieldOf("Titanic"), mutableFieldOf(1999)) ) fun main() { val f = movies.snapshot { movie -> //Returning a Field<Pair<String, Int>> movie.name pair movie.year }.transform { snapshot -> //Transforming the Field<IterableSnapshot> snapshot.originalIterable.map { movie -> //Obtaining from the snapshot the value of the fields val (name, year) = snapshot[movie] "$name ($year)" } } println(f.value) }
Field<List<String>> f = IterableFieldUtils.snapshot(movies, movie -> { //Returning a Field<Pair<String, Int>> return movie.name.pair(movie.year); }).transform( snapshot -> { //Transforming the Field<IterableSnapshot> return snapshot.originalIterable().stream().map(movie -> { //Obtaining from the snapshot the value of the fields Pair<String, Int> pair = snapshot.get(movie); return pair.getFirst() + " (" + pair.getSecond() + ")"; }).collect(Collectors.toList()); }); System.out.println(f.getValue());
  • Pros: most flexible; recalculates only needed values.
  • Cons: difficult to read and write; lot of code.

For more info about snapshot see the IterableSnapshot class doc.

Transform for snapshots

link

We'll now learn to use a version of the transform function that builds on top of snapshot() but is easier to use.

The transform function will accept a number of lambdas where each one accepts an element of the collection and returns a field. Then, as last parameter, a lambda must be provided that accepts the collection contained in the field and one function for each lambda with the following signature T.() -> I where T is an element of the collection and I is the type of the fields returned by the corresponding lambda.

In other words, all lambdas but the last one are responsible to creating different views of the data contained in the iterable, while the last lambda is responsible to combining all the views in a single object, that will be contained in the returning field.

Example:

import com.femastudios.dataflow.* import com.femastudios.dataflow.extensions.* import com.femastudios.dataflow.util.* class Movie(val name : MutableField<String>, val year : MutableField<Int>) val movies = listFieldOf( Movie(mutableFieldOf("Avatar"), mutableFieldOf(2009)), Movie(mutableFieldOf("Titanic"), mutableFieldOf(1999)) ) fun main() { val f = movies.transform( { it.name }, { it.year }, { collection /* : List<Movie> */, name, year -> //name and year are functions that accept a movie and return a string (the name) and an int (the year). //Kotlin allows also the syntax movie.function() instead of function(movie) in this case collection.map { m -> m.name() + " (" + m.year() + ")" //Same as calling name(m) + " (" + year(m) + ")" } } ) println(f.value) }
Field<List<String>> f = IterableFieldUtils.transform(movies, m -> m.name, m -> m.year, (/* List<Movie> */ collection, name, year) -> collection.stream().map(m -> name.invoke(m) + " (" + year.invoke(m) + ")" ).collect(Collectors.toList()) ); System.out.println(f.getValue());
  • Pros: cleaner and easy to write, so-so to read; recalculates only needed values.
  • Cons: slightly less flexible.

Utility functions

link

We finally arrived at the last iteration of our example, using already provided utility functions that use internally the previous described functions.

For this example we'll need the mapF() function:

import com.femastudios.dataflow.* import com.femastudios.dataflow.extensions.* import com.femastudios.dataflow.util.* class Movie(val name : MutableField<String>, val year : MutableField<Int>) val movies = listFieldOf( Movie(mutableFieldOf("Avatar"), mutableFieldOf(2009)), Movie(mutableFieldOf("Titanic"), mutableFieldOf(1999)) ) fun main() { val f = movies.mapF { m -> m.name + " (" + m.year + ")" } println(f.value) }
Field<List<String>> f = IterableFieldUtils.mapF(movies, m -> FieldUtils.transform(m.name, m.year, (name, year) -> name + " (" + year + ")") ); System.out.println(f.getValue());
  • Pros: super-easy to write and read; recalculates only needed values; immediately know what's going on; less code.
  • Cons: specialized function.

There are a ton of utility extension functions that can run this kind of transformations. A complete list can be found here.

Changing value

link

Changing the value of collection fields is a delicate process since a new one must be created for each modification. There are several helper methods will automatically do this for you, for example:

import com.femastudios.dataflow.extensions.* import com.femastudios.dataflow.util.* fun main() { val f = mutableListFieldOf("hello") val listBefore = f.value println(listBefore) //["hello"] f.add("world") //Will create a new list ["hello", "world"] and set it to the field, atomically val listAfter = f.value println(listAfter) //["hello", "world"] println(listBefore === listAfter) //false, different object }
MutableField<List<String>> f = FieldUtils.listFieldOf(1, 2, 3); List<String> listBefore = f.getValue(); System.out.println(listBefore); //["hello"] FieldListUtils.add(f, "world"); //Will create a new list ["hello", "world"] and set it to the field, atomically List<String> listAfter = f.getValue(); System.out.println(listAfter); //["hello", "world"] System.out.println(listBefore === listAfter) //false, different object

Virtually all methods of List, Set and Map are available with similar behavior. It goes without saying that doing this repeatedly is not very efficient, for this reason subsequent changes should be batched together.