Java 8 dała nam wiele wspaniałych rzeczy. Jednak nadal nie dostarczyła nam API krotek (ang. tuples). Z pomocą przychodzą oczywiście różne biblioteki. Chociażby JavaTuples, którą należy już nazywać Java Truples, bo nie była aktualizowana od dawien dawna. Choć ostatnio dorzucili… konfigurację OSGi. Inną biblioteką jest osławiony Javaslang. Zobaczmy co można z jej pomocą uzyskać.

Tworzenie i dostęp do elementów

By utworzyć nową krotkę, musimy skorzystać z jednej ze statycznych metod fabrykujących o wspólnej nazwie of z interfejsu Tuple. Różnią się one ilością parametrów:

Listing 1. Tworzenie pięcioelementowej krotki.

public void creatingAndAccess() {
    Tuple5<Integer, Integer, Integer, String, String> wyliczanka = Tuple.of(1, 2, 3, "Baba Jaga", "patrzy");
}

Teraz by odwołać się, do któregoś z elementów możemy użyć zarówno pola, jak i metody:

Listing 2. Dostęp do elementów krotki.

public void creatingAndAccess() {
    Tuple5<Integer, Integer, Integer, String, String> wyliczanka = Tuple.of(1, 2, 3, "Baba Jaga", "patrzy");
    System.out.println(wyliczanka._1);
    System.out.println(wyliczanka._2());
    System.out.println(wyliczanka._1);
    System.out.println(wyliczanka._4());
    System.out.println(wyliczanka._5);
}

Jedynym ograniczeniem jest maksymalna wielkość krotki. W bibliotece Javaslang to 8. Z drugiej strony implementacja większych krotek jest prosta, a i trzeba bardziej uważać, co się projektuje, by nie musieć samodzielnie rzeźbić krotki o dwudziestu elementach.

Transformowanie

Jak już mamy naszą krotkę, to możemy ją zamienić na jakiś sensowny obiekt. W tym celu wykorzystamy metodę transform.

Listing 3. Przykład transformacji krotki

public void transforming() {
    Tuple2<String, String> alaIKot = Tuple.of("Ala", "Kot");
    String message = alaIKot.transform((who, whom) -> who + " ma " + whom);
    System.out.println(message);
}

W tym miejscu warto zauważyć, że Javaslang używa funkcji z API Javy 8 tam gdzie można. Zatem argumentem metody transform jest BiFunction, a nie Function2.

Wielkość i sekwencjonowanie

Gdy pracujemy z krotkami, to czasami potrzebujemy wiedzieć, jaka jest ich wielkość. W tym celu mamy metodę arity.

Listing 4. Wielkość krotki z arity

public void arityAndSeq() {
    Tuple2<String, String> alaIKot = Tuple.of("Ala", "Kot");
    System.out.println(alaIKot.arity());
}

A gdy chcemy zamienić krotkę na sekwencję, reprezentowaną przez Seq, to możemy użyć toSeq:

Listing 5. Sekwencjonowanie z toSeq

public void arityAndSeq() {
    Tuple2<String, String> alaIKot = Tuple.of("Ala", "Kot");
    Seq<?> elems = alaIKot.toSeq();
    System.out.println(elems);
}

Sama sekwencja to dość ciekawa sprawa, ale o tym innym razem.

Mapowanie

Krotka to uporządkowany zbiór elementów różnych typów. W językach funkcyjnych takich jak erlang czy Elixir krotki stanowią podstawę przy tworzeniu bardziej skomplikowanych struktur danych. By w pełni wykorzystać ich możliwości, musimy mieć możliwość mapowania, czyli inaczej mówiąc transformacji krotki na inną o tej samej wielkości. W tym celu Tuple posiada zestaw metod map oraz mapX. Gdy chcemy mapować krotkę jako całość możemy użyć metody map, która jako parametr przyjmuje funkcję o liczbie parametrów równej wielkości krotki:

Listing 6. Mapowanie krotki jako całości

public void mapping() {
    Tuple2<String, String> alaIKot = Tuple.of("Ala", "Kot");
    System.out.println(alaIKot.map((who, whom) -> Tuple.of(whom, who)));
}

Jest to przydatne, ale nie zawsze mamy do dyspozycji odpowiednią funkcję. Dlatego też istnieje map, które jako parametry przyjmuje funkcje o pojedynczym parametrze zgodnym z typem elementu na danej pozycji:

Listing 7. Mapowanie krotki po elemencie

public void mapping() {
    Tuple2<String, String> alaIKot = Tuple.of("Ala", "Kot");
    System.out.println(alaIKot.map(String::length, String::length));
}

To rozwiązanie jest całkiem niezłe, bo zapewnia odpowiednią elastyczność. Znacznie łatwiej jest pisać proste funkcje o jednym argumencie niż kobyłki o na przykład 5 argumentach. Jednak nadal pozostają przypadki, gdy naszą funkcję chcemy zaaplikować tylko do jednego elementu, a pozostałe argumenty traktujemy funkcją tożsamościowąW. W takim przypadku bezsensowne jest wywoływanie metody map z poprzedniego przykładu. Javaslang udostępnia nam jednak grupę metod mapX, gdzie X to numer kolejnych elementów (nieludzko zrobiona, bo od jedynki). Gdy wywołamy taką metodę to otrzymujemy nową krotkę, w której tylko jeden element został zmieniony:

Listing 8. Mapowanie pojedynczego elementu krotki

public void mapping() {
    Tuple2<String, String> alaIKot = Tuple.of("Ala", "Kot");
    System.out.println(alaIKot.map1(String::length));
    System.out.println(alaIKot.map2(String::length));
}

Oczywiście wywołania możemy łączyć:

Listing 9. Mapowanie pojedynczych elementów krotki

public void mapping() {
    Tuple3<String, String, String> alaIKot = Tuple.of("Ala", "ma",  "Kot");
    System.out.println(alaIKot.map1(String::length).map3(String::length));
}

Przy czym, należy pamiętać, że każde wywołanie oznacza utworzenie nowego obiektu. W pewnych przypadkach może być to niepożądane zachowanie.

Podsumowanie

Krotki udostępnione w bibliotece Javaslang są bardzo fajnym dodatkiem do naszej skrzynki z narzędziami. Mają wszystko to, czego oczekujemy od tego typu struktury. Ciekawostką jest to, że kod źródłowy jest automatycznie generowany za pomocą generatora napisanego w Scali.