Javactic is a Java port of Scalactic's Or and Every mechanism. Javactic is based on the Javaslang functional library for Java 8+. This documentation is mostly ported from here.

Or and Every

The Or and Every types of Javactic allow you to represent errors as an "alternate return value" (like Either) and to optionally accumulate errors. Or represents a value that is one of two possible types, with one type being "good" (a value wrapped in an instance of Good ) and the other "bad" (a value wrapped in an instance of Bad ).

The motivation for Or

Or is very similar to Javaslang's Either type (which is right biased), but allows for additional accumulation of bad values. Scala's Either is somewhat different in that it treats both its Left and Right alternatives in an identical manner and requires the use of explicit projections when manipulated. To illustrate all this, imagine you want to create instances of this Person class from user input strings:


class Person {
    private final String name;
    private final int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person(" + name + "," + age + ")";
    }
}

		

You might write a method that parses the name from user input string and returns an Option<String> : None if the string is empty or blank, else the trimmed string wrapped in a Some :


Option<String> parseName(String name) {
    String trimmed = name.trim();
    return (trimmed.isEmpty()) ? Option.none() : Option.of(trimmed);
}

		

You might also write a method that parses the age from user input string and returns an Option<Int> : None if either the string is not a valid integer or it is a negative integer, else the string converted to an integer wrapped in a Some :


Option<Integer> parseAge(String input) {
    try {
        int age = Integer.parseInt(input.trim());
        return (age >= 0) ? Option.of(age) : Option.none();
    } catch (NumberFormatException e) {
        return Option.none();
    }
}

		

With these building blocks you could write a method that parses name and age input strings and returns either a Person , wrapped in a Some , or None if either the name or age, or both, was invalid:


Option<Person> parsePerson(String inputName, String inputAge) {
    return parseName(inputName).flatMap(name ->
        parseAge(inputAge).map(age -> new Person(name, age))
    );
}

		

Here are some examples of invoking parsePerson :


parsePerson("Tyler Durden", "29")
// Some(Person(Tyler Durden,29))

parsePerson("Tyler Durden", "")
// None

parsePerson("Tyler Durden", "-29")
// None

parsePerson("", "")
// None

		

Now imagine you want to give an error message back if the user's input is invalid. You might rewrite the parsing methods to return an Either instead. In this case, the desired result is a valid name or age, which by convention should be placed on the right of the Either . The left will be a string error message. Here's the new parseName function, which returns an Either<String, String> :


Either<String,String> parseName(String input) {
    String trimmed = input.trim();
    return (!trimmed.isEmpty())
    	? Either.right(trimmed)
    	: Either.left("'" + input + "' is not a valid name");
}

		

And here's the new parseAge function, which returns an Either<String, Int> :


Either<String, Integer> parseAge(String input) {
    try {
        int age = Integer.parseInt(input.trim());
        return (age >= 0) ? Either.right(age) : Either.left("'" + age + "' is not a valid age");
    } catch (NumberFormatException e) {
        return Either.left("'" + input + "' is not a valid integer");
    }
}

		

The new parsePerson method will return an Either<String, Person> :


Either<String, Person> parsePerson(String inputName, String inputAge) {
    return parseName(inputName)
        .flatMap(name -> parseAge(inputAge)
            .map(age -> new Person(name, age)));
}

		

Given this implementation, the parsePerson method will now short-circuit at the first sign of trouble (as it did when we used an Option ), but you now get the first error message returned in a Left . Here are some examples:


parsePerson("Tyler Durden", "29")
// Right(Person(Tyler Durden,29))

parsePerson("Tyler Durden", "")
// Left('' is not a valid integer)

parsePerson("Tyler Durden", "-29")
// Left('-29' is not a valid age)

parsePerson("", "")
// Left('' is not a valid name)

		

An alternative to Either

As explained above, at first glance Or is very similar to Javaslang's Either. One difference to note with Or is that the Good alternative is on the left, Bad on the right.

Here's how the parseName method might be written using an Or , where the error type is also a String:


Or<String, String> parseName(String input) {
    String trimmed = input.trim();
    return (!trimmed.isEmpty())
        ? Good.of(trimmed)
        : Bad.ofString("'{}' is not a valid name", input);
}

		

Here's how the parseAge method might be written:


Or<Integer, String> parseAge(String input) {
    try {
        int age = Integer.parseInt(input.trim());
        return (age >= 0) ? Good.of(age) : Bad.ofString("'{}' is not a valid age", age);
    } catch (NumberFormatException e) {
        return Bad.ofString("'{}' is not a valid integer", input);
    }
}

		

Given these implementations, here's how you'd write the parsePerson method:


Or<Person, String> parsePerson(String inputName, String inputAge) {
    return parseName(inputName)
    	.flatMap(name -> parseAge(inputAge)
    		.map(age -> new Person(name, age))
    	);
}

		

This is pretty much the same code as for Either, including the short circuiting at the first sign of a Bad. The invocations of parsePerson produce the exact same results.

Accumulating errors with Or

The main difference between Or and Either is that Or enables you to accumulate errors very naturally if the Bad type is an Every. An Every is similar to a Seq in that it contains ordered elements, but different in that it cannot be empty. An Every is either a One (containing one and only one element), or a Many (containing two or more elements).

Note: an Or whose Bad type is an Every, or one of its subtypes, is called an "accumulating Or."

To rewrite the previous example so that errors can be accumulated, you need first to return an Every as the Bad type. Here's how you'd change the parseName method:


Or<String, One<String>> parseName(String input) {
    String trimmed = input.trim();
    return (!trimmed.isEmpty())
    	? Good.of(trimmed)
    	: Bad.ofOneString("'{}' is not a valid name", input);
}

		

Because parseName will either return a valid name String wrapped in a Good , or one error message, wrapped in a Bad , you would write the Bad type as One<String> . The same is true for parseAge :


Or<Integer, One<String>> parseAge(String input) {
    try {
        int age = Integer.parseInt(input.trim());
        return (age >= 0) ? Good.of(age) : Bad.ofOneString("'{}' is not a valid age", age);
    } catch (NumberFormatException e) {
        return Bad.ofOneString("'{}' is not a valid integer", input);
    }
}

		

Because a for expression short-circuits on the first Bad encountered, you'll need to use a different approach to write the parsePerson method. This can be done with the withGood method from class Accumulation:


Or<Person, Every<String>> parsePerson(String inputName, String inputAge) {
    Or<String, One<String>> name = parseName(inputName);
    Or<Integer, One<String>> age = parseAge(inputAge);
    return Accumulation.withGood(name, age, (n, a) -> new Person(n, a));
}

		

Class Accumulation offers overloaded withGood methods that take 1 to 8 accumulating Or s, plus a function taking the same number of corresponding Good values. In this example, if both name and age are Good s, the withGood method will pass the good name String and age int to the Person constructor, and return the resulting Person object wrapped in a Good . If either name and age, or both, are Bad , withGood will return the accumulated errors in a Bad .

The result of parsePerson , if Bad , will therefore contain either one or two error messages, i.e., the result will either be a One or a Many . As a result, the result type of parsePerson must be Or<Person, Every<String>> . Regardless of whether a Bad result contains one or two error messages, it will contain every error message. Here's some invocations of this accumulating version of parsePerson:


parsePerson("Tyler Durden", "29")
// Good(Person(Tyler Durden,29))

parsePerson("Tyler Durden", "")
// Bad(One('' is not a valid integer))

parsePerson("Tyler Durden", "-29")
// Bad(One('-29' is not a valid age))

parsePerson("", "")
// Bad(Many('' is not a valid name, '' is not a valid integer))

		

Note that in the last example, the Bad contains an error message for both name and age.

Working with Ors

Ors can be created using static constructors on either the Or interface or the implementing Good and Bad classes. Constructors on the Or interface will return the value as an Or , whereas constructors on specific types will return specific types:


Or.good("good"); // Or<String, Object>
Or.bad("bad");   // Or<Object, String>
Good.of("good"); // Good<String, Object>
Bad.of("bad");   // Bad<Object, String>

Note that since Or has two types, but each of its two subtypes only takes a value of one or the other type, the Java compiler will infer Object for the unspecified type. This can be changed by either assigning the value to a variable with a more specific type or by giving explicit type arguments:


Or<String, Integer> good = Or.good("good"); // Or<String, Integer>
Or<Integer, String> bad = Or.bad("bad");    // Or<Integer, String>

Or.<String, Integer>good("good"); // Or<String, Integer>
Or.<Integer, String>bad("bad");   // Or<Integer, String>

		

A specific type like Bad can be widened back to an Or with the asOr method


Bad.of("bad").asOr(); // Or<Object, String>

		

You can transform an existing Or into an accumulating one with the accumulating() method. There are also factory methods for creating accumulating Or s directly:


Or<String, One<String>> acc = Bad.<String,String>of("bad").accumulating();
Bad<String, One<String>> ofOne = Bad.ofOne("bad");
Bad<String, One<String>> ofOneString = Bad.ofOneString("error with value {}", 12);

		

Working with Everys

The previous examples demonstrate constructing a one-element Every with a factory method in the One companion object. You can similarly create an Every that contains more than one using a Many factory method. Here are some examples:


One.of(1);
Many.of(1, 3);
Many.of(1, 2, 3);

		

You can also construct an Every by passing one or more elements to the Every.of factory method:


Every.of(1);
Every.of(1, 2);
Every.of(1, 2, 3);

		

Every does not extend Seq or Traversable interfaces because these require that implementations may be empty. For example, if you invoke tail() on a Seq that contains just one element, you'll get an empty Seq

On the other hand, many useful methods exist on Seq that when invoked on a non-empty Seq are guaranteed to not result in an empty Seq . For convenience, Every defines a method corresponding to every such Seq method. Here are some examples:


Many.of(1, 2, 3).map(i -> i + 1);                   // Many(2, 3, 4)
One.of(1).map(i -> i + 1);                          // One(2)
Every.of(1, 2, 3).containsSlice(Every.of(2, 3));    // true
Every.of(1, 2, 3).containsSlice(Every.of(3, 4));    // false
Every.of(-1, -2, 3, 4, 5).minBy(i -> Math.abs(i));  // -1

		

Every does not currently define any methods corresponding to Seq methods that could result in an empty Seq . However, you can convert an Every to a Seq and get hold of these methods:


Every.of(1, 2, 3).toSeq().filter(i -> i < 10); // Vector(1, 2, 3)
Every.of(1, 2, 3).toSeq().filter(i -> i > 10); // Vector()

		

Other ways to accumulate errors

The Accumulation class also enables other ways of accumulating errors.

Using combined

If you have a collection of accumulating Or s, for example, you can combine them into one Or using combined:


List<Or<Integer, One<String>>> list = List.ofAll(parseAge("29"), parseAge("30"), parseAge("31"));
Accumulation.combined(list, List.collector());  // Good(List(29, 30, 31))

List<Or<Integer, One<String>>> list2 = List.ofAll(parseAge("29"), parseAge("-30"), parseAge("31"));
Accumulation.combined(list2, List.collector()); // Bad(One("-30" is not a valid age))

List<Or<Integer, One<String>>> list3 = List.ofAll(parseAge("29"), parseAge("-30"), parseAge("-31"));
Accumulation.combined(list3, List.collector());
// Bad(Many("-30" is not a valid age, "-31" is not a valid age))

		

Using validatedBy

If you have a collection of values and a function that transforms that type of value into an accumulating Or, you can validate the values using the function using validatedBy:


List<String> list = List.ofAll("29", "30", "31");
Accumulation.validatedBy(list, this::parseAge, List.collector()); // Good(List(29, 30, 31))

List<String> list2 = List.ofAll("29", "-30", "31");
Accumulation.validatedBy(list2, this::parseAge, List.collector()); // Bad(One("-30" is not a valid age))

List<String> list3 = List.ofAll("29", "-30", "-31");
Accumulation.validatedBy(list3, this::parseAge, List.collector());
// Bad(Many("-30" is not a valid age, "-31" is not a valid age))

		

Using zip

You can also zip two accumulating Or s together. If both are Good , you'll get a Good tuple containing both original Good values. Otherwise, you'll get a Bad containing every error message. Here are some examples:


Or<Tuple2<String, Integer>, Every<String>> zip = Accumulation.zip(parseName("Dude"), parseAge("21"));
// Good((Dude,21))

Accumulation.zip(parseName("Dude"), parseAge("-21"));
// Bad(One("-21" is not a valid age))

Accumulation.zip(parseName(""), parseAge("-21"));
// Bad(Many("" is not a valid name, "-21" is not a valid age))

		

Using when

In addition, given an accumulating Or , you can pass one or more validation functions to when on the Or to submit that Or to further scrutiny. A validation function accepts a Good type and returns a Validation<E> , where E is the type in the Every in the Bad type. For an Or<Integer, One<String> , for example the validation function type would be Integer -> Validation<String> . Here are a few examples:


Validation<String> isRound(int i) {
    return (i % 10 == 0) ? Pass.instance() : Fail.of(i + " was not a round number");
}

Validation<String> isDivBy3(int i) {
    return (i % 3 == 0) ? Pass.instance() : Fail.of(i + " was not divisible by 3");
}

  

If the Or on which you call when is already Bad , you get the same (Bad) Or back, because no Good value exists to pass to the validation functions:


Or<Integer, Every<String>> when = Accumulation.when(parseAge("-30"), this::isRound, this::isDivBy3);
// Bad(One("-30" is not a valid age))

		

If the Or on which you call when is Good , and also passes all the validation functions (i.e., they all return Pass ), you again get the same Or back, but this time, a Good one:


Accumulation.when(parseAge("30"), this::isRound, this::isDivBy3);
// Good(30)

		

If one or more of the validation functions fails, however, you'll get a Bad back containing every error:


Accumulation.when(parseAge("33"), this::isRound, this::isDivBy3);
// Bad(One(33 was not a round number))

Accumulation.when(parseAge("20"), this::isRound, this::isDivBy3);
// Bad(One(20 was not divisible by 3))

Accumulation.when(parseAge("31"), this::isRound, this::isDivBy3);
// Bad(Many(31 was not a round number, 31 was not divisible by 3))

		

OrFuture

The OrFuture and OrPromise are not part of the original Scalactic library. So why one more future implementation? The problem with futures as they are traditionally implemented is that failures are always represented by an exception, when it's perfectly ok to think that an asynchronous operation can fail in a way that is not exceptional. The OrFuture represents an asynchronous version of the Or type, and it doesn't complete with a success or a failure but with an instance of Or. Let's see a few examples:

Working with OrFutures

An OrFuture can be created with one of the static .of() methods:


OrFuture<String, String> f = OrFuture.of(() -> Or.good("good value"));
OrFuture<String, String> f =
  OrFuture.of(Executors.newSingleThreadExecutor(), () -> Or.good("good value"));

          

OrFutures can also be created through a FutureFactory that automatically handles uncaught exceptions and transforms them to Bad values through the provided exception translator:


FutureFactory<String> factory = FutureFactory.of(Throwable::getMessage);
OrFuture<String, String> future =
  factory.newFuture(() -> throw new RuntimeException("runtime exception"));
future.onComplete(System.out::println);
// Bad(runtime exception)

          

It is advised to create all OrFutures through a factory, exceptions that leak out of the asynchronous computation will be handled by the executor in its ThreadGroup's UncaughtExceptionHandler and might not be visible anywhere. You can think of the exception translator as a much less obtrusive way of handling an ExecutionException.

Here are the same parsing examples we saw for Or's implemented asynchronously using OrFutures:


Function<Throwable, String> converter = Throwable::getMessage;
FutureFactory<String> ff = FutureFactory.of(converter);

OrFuture<String, String> parseNameAsync(String input) {
    return ff.newFuture(() -> parseName(input));
}

OrFuture<Integer, String> parseAgeAsync(String input) {
    return ff.newFuture(() -> parseAge(input));
}

OrFuture<Person, String> parsePersonAsync(String inputName, String inputAge) {
    return parseNameAsync(inputName)
            .flatMap(name -> parseAgeAsync(inputAge)
                    .map(age -> new Person(name, age)));
}

        

Running these methods will provide the same results:


OrFuture<Person, String> asyncOr = parsePersonAsync("Tyler Durden", "29");
asyncOr.onComplete(System.out::println);
// Good(Person(Tyler Durden,29))

asyncOr = parsePersonAsync("Tyler Durden", "");
asyncOr.onComplete(System.out::println);
// Bad('' is not a valid integer)

asyncOr = parsePersonAsync("Tyler Durden", "-29");
asyncOr.onComplete(System.out::println);
// Bad('-29' is not a valid age)

asyncOr = parsePersonAsync("", "");
asyncOr.onComplete(System.out::println);
// Bad('' is not a valid name)

        

Accumulating errors with OrFutures

Like we saw with vanilla Ors, the parsePersonAsync only returned the error for the first failing argument. This can be corrected by implementing accumulating versions of the parse methods and using the helper methods on the OrFuture interface to accumulate potential errors:


Function<Throwable, One<String>> converter = throwable -> One.of(throwable.getMessage());
FutureFactory<One<String>> ff = FutureFactory.of(converter);

OrFuture<String, One<String>> parseNameAsync(String input) {
  return ff.newFuture(() -> parseName(input));
}

OrFuture<Integer, One<String>> parseAgeAsync(String input) {
  return ff.newFuture(() -> parseAge(input));
}

OrFuture<Person, Every<String>> parsePersonAsync(String inputName, String inputAge) {
  OrFuture<String, One<String>> name = parseNameAsync(inputName);
  OrFuture<Integer, One<String>> age = parseAgeAsync(inputAge);
  return OrFuture.withGood(name, age, Person::new);
}

        

Methods combined, validatedBy & zip also exist as with the Accumulation class for Ors.