Introduzione alla Fuga Atlassiana

1. Introduzione

Fugue è una libreria Java di Atlassian; è una raccolta di utilità che supportano la programmazione funzionale .

In questo articolo, ci concentreremo ed esploreremo le API più importanti di Fugue.

2. Iniziare con Fugue

Per iniziare a utilizzare Fugue nei nostri progetti, dobbiamo aggiungere la seguente dipendenza:

 io.atlassian.fugue fugue 4.5.1 

Possiamo trovare la versione più recente di Fugue su Maven Central.

3. Opzione

Iniziamo il nostro viaggio guardando la classe Option che è la risposta di Fugue a java.util.Optional.

Come possiamo intuire dal nome, Option è un contenitore che rappresenta un valore potenzialmente assente.

In altre parole, un'opzione può essere Un valore di un certo tipo o Nessuno :

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. Il funzionamento della mappa

Una delle API di programmazione funzionale standard è il metodo map () che consente di applicare una funzione fornita agli elementi sottostanti.

Il metodo applica la funzione fornita al valore dell'opzione se è presente:

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

3.2. Opzione e un valore nullo

Oltre a denominare le differenze, Atlassian ha fatto alcune scelte di design per Option che differiscono da Optional ; guardiamoli ora.

Non possiamo creare direttamente un'opzione non vuota che contiene un valore nullo :

Option.some(null);

Quanto sopra genera un'eccezione.

Tuttavia, possiamo ottenerne uno come risultato dell'utilizzo dell'operazione map () :

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

Questo non è possibile quando si utilizza semplicemente java.util.Optional.

3.3. Opzione I è modificabile

L'opzione può essere trattata come una raccolta che contiene al massimo un elemento, quindi ha senso per implementare l' interfaccia Iterable .

Ciò aumenta notevolmente l'interoperabilità quando si lavora con raccolte / flussi.

E ora, ad esempio, può essere concatenato con un'altra raccolta:

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

3.4. Opzione di conversione in streaming

Poiché un'opzione è un iterabile, può anche essere convertita facilmente in un flusso .

Dopo la conversione, l' istanza Stream avrà esattamente un elemento se l'opzione è presente, o zero in caso contrario:

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

3.5. java.util.Optional Interoperability

Se abbiamo bisogno di un'implementazione opzionale standard , possiamo ottenerla facilmente utilizzando il metodo toOptional () :

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

3.6. La classe di utilità delle opzioni

Infine, Fugue fornisce alcuni metodi di utilità per lavorare con le opzioni nella classe Opzioni dal nome appropriato .

È dotato di metodi come filterNone per rimuovere vuoti Opzioni da una collezione, e appiattire per turno ing una raccolta di opzioni in un insieme di oggetti racchiusi, filtrando vuoti Opzioni.

Inoltre, presenta diverse varianti del metodo lift che eleva una funzione in una funzione > :

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());
    

Ciò è utile quando vogliamo passare una funzione che non è a conoscenza di Option a un metodo che utilizza Option .

Nota che, proprio come il metodo map , lift non mappa null su None :

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

4. O per calcoli con due possibili risultati

Come abbiamo visto, la classe Option ci permette di affrontare l'assenza di un valore in modo funzionale.

Tuttavia, a volte è necessario restituire più informazioni di "nessun valore"; ad esempio, potremmo voler restituire un valore legittimo o un oggetto di errore.

La classe Either copre questo caso d'uso.

Un'istanza di O può essere una destra o una sinistra ma mai entrambe allo stesso tempo .

Per convenzione, la destra è il risultato di un calcolo riuscito, mentre la sinistra è il caso eccezionale.

4.1. Costruire un Either

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.

Gli iterabili e gli iteratori , infine, contengono una serie di metodi statici per manipolare queste due interfacce Java standard ampiamente utilizzate.

7. Conclusione

In questo articolo, abbiamo fornito una panoramica della libreria Fugue di Atlassian.

Non abbiamo toccato le classi algebriche come Monoid e Semigruppi perché non rientrano in un articolo generalista.

Tuttavia, puoi leggere su di loro e altro nei javadoc di Fugue e nel codice sorgente.

Inoltre non abbiamo toccato nessuno dei moduli opzionali, che offrono ad esempio integrazioni con Guava e Scala.

L'implementazione di tutti questi esempi e frammenti di codice può essere trovata nel progetto GitHub: questo è un progetto Maven, quindi dovrebbe essere facile da importare ed eseguire così com'è.