Pattern matching w Javie z Javaslang IV
Poprzednie części:
- Pattern matching w Javie z Javaslang I
- Pattern matching w Javie z Javaslang II
- Pattern matching w Javie z Javaslang III
Początkowo chciałem włączyć temat tworzenia własnych wzorców do poprzedniego wpisu. Jednak ze względu na konieczność zrobienia kilku rzeczy, bez których będzie nam ciężko, postanowiłem wyłączyć ten temat do osobnego wpisu.
Dodatkowe zależności
Samodzielne pisanie wzorców jest uciążliwe i w sumie mało przyjemne. Javaslang jest jednak oparty o generatory, a zatem pozwólmy robić im ich robotę. W tym celu musimy dodać jedną zależność w naszym pliku build.gradle:
Listing 1. Konfiguracja projektu
apply plugin: 'java'
repositories {
jcenter()
mavenCentral()
}
sourceSets {
generated {
java {
srcDirs = ['src/main/java']
}
}
}
configurations {
patternMatch
}
dependencies {
compile 'org.slf4j:slf4j-api:1.7.21'
compile('io.javaslang:javaslang:2.0.2')
patternMatch 'io.javaslang:javaslang-match:2.0.2'
testCompile 'junit:junit:4.12'
}
task generatePatterns(type: JavaCompile, group: 'build', description: 'Generates patterns classes') {
source = sourceSets.main.java
classpath = configurations.compile + configurations.patternMatch
options.compilerArgs = [
"-proc:only",
"-processor", "javaslang.match.PatternsProcessor"
]
destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
}
compileJava {
dependsOn generatePatterns
options.compilerArgs =[
"-proc:none"
]
}
compileGeneratedJava {
dependsOn generatePatterns
options.warnings = false
classpath += sourceSets.main.runtimeClasspath
}
Biblioteka javaslang-match zawiera procesor adnotacji, który wygeneruje nam odpowiednie klasy. Jednak by prawidłowo działał, należy:
- Stworzyć dodatkowy task, który zajmie się generowaniem klas.
- Wyłączyć procesory adnotacji w głównym cyklu kompilacji.
Pierwsze robimy poprzez dodanie zadania generatePatterns oraz konfiguracji patternMatch. Drugie za pomocą dodania argumentu -proc:none do wywołania kompilatora. Powyższa konfiguracja to kombinacją braku możliwości konfiguracji generatora i mojej niewiedzy w zakresie konfigurowania Gradle.
Model
Tu sprawa jest prosta. Będziemy używać klasy User, która zawiera trzy pola:
Listing 2. Klasa User
public class User {
private String name;
private String email;
private String role;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
Ot zwykłe POJO. Równie dobrze możemy użyć data class z Kotlina. Jednak jest jeden warunek. Jeżeli nie chcemy samodzielnie kompilować Javaslang, to musimy ograniczyć się do maksymalnie ośmiu pól w dekompozycji obiektu. Tyle właśnie wynosi największa pojemność krotki w Javaslang.
Własny wzorzec
Tworzenie własnego wzorca rządzi się prostymi zasadami. Po pierwsze musimy stworzyć klasę, która będzie zawierać wzorce. Oznaczymy ją adnotacją @Patterns.
Listing 3. Klasa Users
@Patterns
public class Users {
}
T’n’T z nazwami
Generator utworzy klasę UsersPatterns. Dlaczego nie możemy utworzyć klasy User? Jest bug w Javaslang, który powoduje, że generator nie używa pełnych nazw klas wraz z pakietami. W efekcie w pewnym momencie mamy sytuację jak poniżej:
Listing 4. Błędnie wygenerowana klasa
package pl.koziolekweb.javaslang.patternmatching.patterns;
import javaslang.API.Match.Pattern;
import javaslang.API.Match.Pattern3;
import pl.koziolekweb.javaslang.patternmatching.User;
// GENERATED BY JAVASLANG <<>> derived from pl.koziolekweb.javaslang.patternmatching.patterns.User
public final class UserPatterns {
private UserPatterns() {
}
public static <_1 extends String, _2 extends String, _3 extends String> Pattern3<User, _1, _2, _3> User(Pattern<_1, ?> p1, Pattern<_2, ?> p2, Pattern<_3, ?> p3) {
return Pattern3.of(User.class, p1, p2, p3, User::User);
}
}
Problemem jest zapis User::User, który odwołuje się do naszej klasy ze wzorcami. Jednocześnie mamy zaimportowaną klasę pl.koziolekweb.javaslang.patternmatching.User, co prowadzi do konfliktu nazw. W efekcie mamy błąd kompilacji…
Zgłosiłem buga. Zobaczymy co z nim zrobią. Rozwiązali go.
Dekonstrukcja
Kolejnym krokiem jest dodanie metody, która będzie dekonstruować obiekt User do Tuple3. Z dekonstrukcją na poziomie semantyki języka mieliśmy już do czynienia w Kotlinie. Choć brzmi to groźnie, to sama implementacja jest banalnie prosta:
Listing 5. Metoda User służy do dekonstrukcji
@Patterns
public class Users {
@Unapply
static Tuple3<String, String, String> User(pl.User user) {
return Tuple.of(user.getName(), user.getEmail(), user.getRole());
}
}
Metoda musi mieć adnotację @Unapply i musi być statyczna. Możemy używać naszego wzorca:
Listing 6. Użycie User w dopasowaniu
public void custom() {
User user = new User("Jan Kowalski", "jan@kowalski", "Jan");
System.out.println(
Match(user).of(
Case(User($(), $(), $("Jan")), "OK - Jan"),
Case(User($(), $(), $("Emil")), "OK - Emil")
)
);
}
W efekcie mamy możliwość dopasowania obiektu do wzorca według samodzielnie zdefiniowanych reguł.
Podsumowanie
Tworzenie własnych wzorców i reguł dopasowania nie jest trudne. Najwięcej czasu zajęło mi odpowiednie skonfigurowanie gradle tak by generowane klasy były widoczne w projekcie. To jest jakaś porażka niestety.