Skip to content

Latest commit

 

History

History
260 lines (180 loc) · 7.8 KB

README.md

File metadata and controls

260 lines (180 loc) · 7.8 KB

This is a starting point for a tutorial for TotallyLazy which has a all the things that we are missing in the Java 8 class library.

Sequence

On of the basic abstractions of TotallyLazy is Sequence. A sequence is a bit like an Iterable with a lot of useful methods. Wherever possible these methods are lazily evaluated, e.g. map, filter, while some methods like fold force the evaluation of the chain so far.

The typical way to construct a Sequence is to use one of the static sequence factory methods.

Sequence<Integer> fromVarargs = sequence(1, 2, 3);

Sequence<Integer> fromArray = sequence(new Integer[]{1, 2, 3});

Sequence<Integer> fromIterable = sequence(asList(1, 2, 3));

Sequence<Integer> fromRange = range(1, 3);

assertEquals(fromVarargs, fromRange);

Mapping

The map method applies a function to every element of the sequence and returns a sequence of these results:

Sequence<Integer> originalSequence = sequence(1, 2, 3);
Sequence<Integer> squares = originalSequence.map(x -> x * x);

assertEquals(sequence(1, 4, 9), squares);

The map method can apply a function that has a different return type than the original element type:

Sequence<Integer> originalSequence = sequence(1, 2, 3);
Sequence<String> stringSequence = originalSequence.map(x -> String.format("%d", x));

assertEquals(sequence("1", "2", "3"), stringSequence);

Accessing Elements and Subranges

There are a couple of ways to access elements of a sequence. Note that an index lookup can be expensive (rather than iterating over indices consider map or fold operations):

Sequence<String> strings = sequence("1", "2", "3", "4");

assertEquals("1", strings.first());
assertEquals("2", strings.second());
assertEquals("4", strings.last());
assertEquals("3", strings.get(2));

There are a number of ways to get subranges of Sequences. It is important to know that Sequences are immutable, i.e. all these operations return new objects.

Here some examples:

Sequence<String> strings = sequence("1", "2", "3", "A", "B");

assertEquals(sequence("2", "3", "A", "B"), strings.tail());
assertEquals(sequence("1", "2"), strings.take(2));
assertEquals(sequence("1", "2", "3"), strings.takeWhile(s -> Character.isDigit(s.charAt(0))));
assertEquals(sequence("3", "A", "B"), strings.drop(2));
assertEquals(sequence("A", "B"), strings.dropWhile(s -> Character.isDigit(s.charAt(0))));

toString

The toString method can take a separator, that can be used to construct strings from the string representations (as per toString) of the elements of the sequence.

Sequence<String> words = sequence("mouse", "dog", "cat");
assertEquals("mouse, dog, cat", words.toString(", "));

Zipping

Two sequences can be zipped into a single collection of pairs:

Sequence<String> firstNames = sequence("Marge", "Maude");
Sequence<String> lastNames = sequence("Simpson", "Flanders");

Sequence<Pair<String, String>> namePairs = firstNames.zip(lastNames);
assertEquals("Marge", namePairs.get(0).first());
assertEquals("Simpson", namePairs.get(0).second());

assertEquals("Maude", namePairs.get(1).first());
assertEquals("Flanders", namePairs.get(1).second());

// A more intersting way to use this feature:

Sequence<String> fullNames = namePairs.map(pair -> pair.first() + " " + pair.second());
assertEquals("Marge Simpson", fullNames.first());
assertEquals("Maude Flanders", fullNames.second());

A very common usecase is that while we iterate (or map!) over a sequence we need the current element and its index. This is what the zipWithIndex method is for:

Sequence<String> names = sequence("Mark", "Luke", "John", "Matthew");
Sequence<String> namesWithIndex = names
    .zipWithIndex()
    .map(pair -> pair.first() + ". " + pair.second());

assertEquals("0. Mark", namesWithIndex.get(0));
assertEquals("1. Luke", namesWithIndex.get(1));
assertEquals("2. John", namesWithIndex.get(2));
assertEquals("3. Matthew", namesWithIndex.get(3));

Grouping

Sequences can be grouped by a key (function), like so:

Sequence<Person> people = sequence(
    new Person("Homer", "Simpson"),
    new Person("Marge", "Simpson"),
    new Person("Ned", "Flanders"),
    new Person("Maude", "Flanders")
);

Sequence<Group<String, Person>> groups = people.groupBy(person -> person.lastname);

assertEquals("Simpson", groups.get(0).key());

assertEquals("Homer", groups.get(0).get(0).firstname);
assertEquals("Marge", groups.get(0).get(1).firstname);

assertEquals("Flanders", groups.get(1).key());

assertEquals("Ned", groups.get(1).get(0).firstname);
assertEquals("Maude", groups.get(1).get(1).firstname);

Option

An Option represents a value that is optional. The traditional way to express this in Java is to use null for absent values. This is problematic as there is no easy way of telling whether a value could be null. Where traditionally you would say a method that may or may not return a String returns String, with an Option you can say it returns Option<String>

A second issue is the explicit null checking that is needed whenever an operation is performed on a value that is potentially null.

This is an option that has a value (some):

Option<String> optionalString = some("Hello World!");

assertEquals(true, optionalString.isDefined());
assertEquals("Hello World!", optionalString.get());

Here an option without a value (none):

Option<String> optionalString = none();

assertEquals(false, optionalString.isDefined());
try {
    optionalString.get();
    fail("This should fail as the option is not defined");
} catch (NoSuchElementException ignored) {}

You can also try to get the value of an option providing a default value should the option be none. This means you can avoid the none check:

Option<String> optionalStringDefined = some("Hello");

assertEquals("Hello", optionalStringDefined.getOrElse("default value"));

Option<String> optionalStringUndefined = none();

assertEquals("default value", optionalStringUndefined.getOrElse("default value"));

The old-school Java approach to optional values is to use null to indicate that there is no value.

The option factory method provides a way to wrap a nullable value into an option:

Option<String> optionalStringDefined = option("Hello");
assertEquals(some("Hello"), optionalStringDefined);

Option<String> optionalStringUndefined = option(null);
assertEquals(none(), optionalStringUndefined);

So far this is just a more explicit way to declare a type (and you could do this with the Optional type of Java 8), but here is how you can avoid boilerplate null/ defined checks when working with optional values.

Consider logic to parse a number that is optionally supplied as a string:

Option<String> optionalString = some("123");

// We can now perform all sorts of operations without checking explicitly:

Option<Integer> optionalNumber = optionalString.map(Integer::parseInt);
Option<Integer> optionalDouble = optionalNumber.map(n -> 2 * n);


assertEquals(true, optionalDouble.isDefined());
assertEquals(Integer.valueOf(246), optionalDouble.get());

Here the same code passing in none:

Option<String> optionalString = none();

Option<Integer> optionalNumber = optionalString.map(Integer::parseInt);
Option<Integer> optionalDouble = optionalNumber.map(n -> 2 * n);

assertEquals(false, optionalDouble.isDefined());

One way to think about options is as collections that have either zero or one element. In functional programming parlance they are both monads.

Development Info

This README.md is generated from the IntroductionTest.java test case using docufier. To regenerate the content run mvn test.