Introduction à Atlassian Fugue

1. Introduction

Fugue est une bibliothèque Java d'Atlassian; c'est une collection d'utilitaires prenant en charge la programmation fonctionnelle .

Dans cet article, nous allons nous concentrer et explorer les API les plus importantes de Fugue.

2. Premiers pas avec Fugue

Pour commencer à utiliser Fugue dans nos projets, nous devons ajouter la dépendance suivante:

 io.atlassian.fugue fugue 4.5.1 

Nous pouvons trouver la version la plus récente de Fugue sur Maven Central.

3. Option

Commençons notre voyage en regardant la classe Option qui est la réponse de Fugue à java.util.Optional.

Comme on peut le deviner par son nom, Option est un conteneur représentant une valeur potentiellement absente.

En d'autres termes, une option est soit une valeur d'un certain type, soit aucune :

Option none = Option.none(); assertFalse(none.isDefined()); Option some = Option.some("value"); assertTrue(some.isDefined()); assertEquals("value", some.get()); Option maybe = Option.option(someInputValue);

3.1. L' opération de la carte

L'une des API standard de programmation fonctionnelle est la méthode map () qui permet d'appliquer une fonction fournie aux éléments sous-jacents.

La méthode applique la fonction fournie à la valeur de l' option si elle est présente:

Option some = Option.some("value") .map(String::toUpperCase); assertEquals("VALUE", some.get());

3.2. Option et une valeur nulle

Outre les différences de dénomination, Atlassian a fait quelques choix de conception pour Option qui diffèrent de Optionnel ; regardons-les maintenant.

Nous ne pouvons pas créer directement une option non vide contenant une valeur nulle :

Option.some(null);

Ce qui précède lève une exception.

Cependant, nous pouvons en obtenir un à la suite de l'utilisation de l' opération map () :

Option some = Option.some("value") .map(x -> null); assertNull(some.get());

Cela n'est pas possible en utilisant simplement java.util.Optional.

3.3. Option I de Iterable

Option peut être traitée comme une collection contenant au maximum un élément, il est donc logique qu'elle implémente l' interface Iterable .

Cela augmente considérablement l'interopérabilité lorsque vous travaillez avec des collections / flux.

Et maintenant, par exemple, peut être concaténé avec une autre collection:

Option some = Option.some("value"); Iterable strings = Iterables .concat(some, Arrays.asList("a", "b", "c"));

3.4. Option de conversion en flux

Puisqu'une option est un Iterable, elle peut également être facilement convertie en Stream .

Après la conversion, l' instance Stream aura exactement un élément si l'option est présente, ou zéro sinon:

assertEquals(0, Option.none().toStream().count()); assertEquals(1, Option.some("value").toStream().count());

3.5. java.util.Optional Interopérabilité

Si nous avons besoin d'une implémentation optionnelle standard , nous pouvons l'obtenir facilement en utilisant la méthode toOptional () :

Optional optional = Option.none() .toOptional(); assertTrue(Option.fromOptional(optional) .isEmpty());

3.6. La classe utilitaire Options

Enfin, Fugue fournit des méthodes utilitaires pour travailler avec les Options dans la classe Options bien nommée .

It features methods such as filterNone for removing empty Options from a collection, and flatten for turning a collection of Options into a collection of enclosed objects, filtering out empty Options.

Additionally, it features several variants of the lift method that lifts a Function into a Function > :

Function f = (Integer x) -> x > 0 ? x + 1 : null; Function
    
      lifted = Options.lift(f); assertEquals(2, (long) lifted.apply(Option.some(1)).get()); assertTrue(lifted.apply(Option.none()).isEmpty());
    

This is useful when we want to pass a function which is unaware of Option to some method that uses Option.

Note that, just like the map method, lift doesn't map null to None:

assertEquals(null, lifted.apply(Option.some(0)).get());

4. Soit pour les calculs avec deux résultats possibles

Comme nous l'avons vu, la classe Option nous permet de gérer l'absence de valeur de manière fonctionnelle.

Cependant, nous devons parfois renvoyer plus d'informations que «aucune valeur»; par exemple, nous pourrions vouloir renvoyer une valeur légitime ou un objet d'erreur.

La classe Either couvre ce cas d'utilisation.

Une instance de l'un ou l' autre peut être une droite ou une gauche, mais jamais les deux en même temps .

Par convention, la droite est le résultat d'un calcul réussi, tandis que la gauche est le cas exceptionnel.

4.1. Construire un soit

We can obtain an Either instance by calling one of its two static factory methods.

We call right if we want an Either containing the Right value:

Either right = Either.right("value");

Otherwise, we call left:

Either left = Either.left(-1);

Here, our computation can either return a String or an Integer.

4.2. Using an Either

When we have an Either instance, we can check whether it's left or right and act accordingly:

if (either.isRight()) { ... }

More interestingly, we can chain operations using a functional style:

either .map(String::toUpperCase) .getOrNull();

4.3. Projections

The main thing that differentiates Either from other monadic tools like Option, Try, is the fact that often it's unbiased. Simply put, if we call the map() method, Either doesn't know if to work with Left or Right side.

This is where projections come in handy.

Left and right projections are specular views of an Either that focus on the left or right value, respectively:

either.left() .map(x -> decodeSQLErrorCode(x));

In the above code snippet, if Either is Left, decodeSQLErrorCode() will get applied to the underlying element. If Either is Right, it won't. Same the other way around when using the right projection.

4.4. Utility Methods

As with Options, Fugue provides a class full of utilities for Eithers, as well, and it's called just like that: Eithers.

It contains methods for filtering, casting and iterating over collections of Eithers.

5. Exception Handling with Try

We conclude our tour of either-this-or-that data types in Fugue with another variation called Try.

Try is similar to Either, but it differs in that it's dedicated for working with exceptions.

Like Option and unlike Either, Try is parameterized over a single type, because the “other” type is fixed to Exception (while for Option it's implicitly Void).

So, a Try can be either a Success or a Failure:

assertTrue(Try.failure(new Exception("Fail!")).isFailure()); assertTrue(Try.successful("OK").isSuccess());

5.1. Instantiating a Try

Often, we won't be creating a Try explicitly as a success or a failure; rather, we'll create one from a method call.

Checked.of calls a given function and returns a Try encapsulating its return value or any thrown exception:

assertTrue(Checked.of(() -> "ok").isSuccess()); assertTrue(Checked.of(() -> { throw new Exception("ko"); }).isFailure());

Another method, Checked.lift, takes a potentially throwing function and lifts it to a function returning a Try:

Checked.Function throwException = (String x) -> { throw new Exception(x); }; assertTrue(Checked.lift(throwException).apply("ko").isFailure());

5.2. Working With Try

Once we have a Try, the three most common things we might ultimately want to do with it are:

  1. extracting its value
  2. chaining some operation to the successful value
  3. handling the exception with a function

Besides, obviously, discarding the Try or passing it along to other methods, the above three aren't the only options that we have, but all the other built-in methods are just a convenience over these three.

5.3. Extracting the Successful Value

To extract the value, we use the getOrElse method:

assertEquals(42, failedTry.getOrElse(() -> 42));

It returns the successful value if present, or some computed value otherwise.

There is no getOrThrow or similar, but since getOrElse doesn't catch any exception, we can easily write it:

someTry.getOrElse(() -> { throw new NoSuchElementException("Nothing to get"); });

5.4. Chaining Calls After Success

In a functional style, we can apply a function to the success value (if present) without extracting it explicitly first.

This is the typical map method we find in Option, Either and most other containers and collections:

Try aTry = Try.successful(42).map(x -> x + 1);

It returns a Try so we can chain further operations.

Of course, we also have the flatMap variety:

Try.successful(42).flatMap(x -> Try.successful(x + 1));

5.5. Recovering From Exceptions

We have analogous mapping operations that work with the exception of a Try (if present), rather than its successful value.

However, those methods differ in that their meaning is to recover from the exception, i.e. to produce a successful Try in the default case.

Thus, we can produce a new value with recover:

Try recover = Try .failure(new Exception("boo!")) .recover((Exception e) -> e.getMessage() + " recovered."); assertTrue(recover.isSuccess()); assertEquals("boo! recovered.", recover.getOrElse(() -> null));

As we can see, the recovery function takes the exception as its only argument.

If the recovery function itself throws, the result is another failed Try:

Try failure = Try.failure(new Exception("boo!")).recover(x -> { throw new RuntimeException(x); }); assertTrue(failure.isFailure());

The analogous to flatMap is called recoverWith:

Try recover = Try .failure(new Exception("boo!")) .recoverWith((Exception e) -> Try.successful("recovered again!")); assertTrue(recover.isSuccess()); assertEquals("recovered again!", recover.getOrElse(() -> null));

6. Other Utilities

Let's now have a quick look at some of the other utilities in Fugue, before we wrap it up.

6.1. Pairs

A Pair is a really simple and versatile data structure, made of two equally important components, which Fugue calls left and right:

Pair pair = Pair.pair(1, "a"); assertEquals(1, (int) pair.left()); assertEquals("a", pair.right());

Fugue doesn't provide many built-in methods on Pairs, besides mapping and the applicative functor pattern.

However, Pairs are used throughout the library and they are readily available for user programs.

The next poor person's implementation of Lisp is just a few keystrokes away!

6.2. Unit

Unit is an enum with a single value which is meant to represent “no value”.

It's a replacement for the void return type and Void class, that does away with null:

Unit doSomething() { System.out.println("Hello! Side effect"); return Unit(); }

Quite surprisingly, however, Option doesn't understand Unit, treating it like some value instead of none.

6.3. Static Utilities

We have a few classes packed full of static utility methods that we won't have to write and test.

The Functions class offers methods that use and transform functions in various ways: composition, application, currying, partial functions using Option, weak memoization et cetera.

The Suppliers class provides a similar, but more limited, collection of utilities for Suppliers, that is, functions of no arguments.

Iterables et Iterators , enfin, contiennent une foule de méthodes statiques pour manipuler ces deux interfaces Java standard largement utilisées.

7. Conclusion

Dans cet article, nous avons donné un aperçu de la bibliothèque Fugue d'Atlassian.

Nous n'avons pas touché aux classes lourdes d'algèbre comme Monoid et Semigroups car elles ne rentrent pas dans un article généraliste.

Cependant, vous pouvez en savoir plus sur eux et plus encore dans les javadocs et le code source de Fugue.

Nous n'avons également abordé aucun des modules optionnels, qui offrent par exemple des intégrations avec Guava et Scala.

L'implémentation de tous ces exemples et extraits de code se trouve dans le projet GitHub - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.