Skip to content

Pattern matching : Pattern Matching for Java 8

johnmcclean-aol edited this page Feb 24, 2016 · 9 revisions

Cyclops has merged with simple-react. Please update your bookmarks (stars :) ) to https://github.com/aol/cyclops-react

All new develpoment on cyclops occurs in cyclops-react. Older modules are still available in maven central.

screen shot 2016-02-22 at 8 44 42 pm

Cyclops Module

cyclops-pattern-matching

As of cyclops 7.4.0 Matchable will be the only non-deprecated public interface into Pattern Matching. Matching inside a stream and other types will be available directly on SequenceM, CollectionX & any class that implements Value (Maybe, Xor, Ior, Eval etc).

Cyclops pattern matching

Entry points (Builder so you can Pattern Match your way)

  • Matchable : provides succinct pattern matching for some of the most common case types.
  • Matching : provides a more powerful & flexible interface at the expense of increased verboseness
  • Recursive Matcher : for more advanced recursive decomposition based matching
  • CollectionMatcher : for matching against collections and Streams.
  • PatternMatcher : provides a more flexible interface at the expense of looser typing
  • Cases / Case : low level functional classes for building pattern matching cases

Matchable is an interface that provides 3 match methods for common use cases. Any (well most) Objects can be coerced to Matchable (via As.asMatchable) even if they don't implement it. It is the recommended entry point for most use cases.

Matching is a class with a number of static methods, it offers a more powerful set of Matching options - at a cost of greater verbosity / complexity.

PatternMatcher is a builder object for Case and Cases Objects. It allows much looser typing to be employed than Matchable or Matching.

Cases / Case or the primitives of Cyclops Pattern Matching and offer functional composition via map / flatMap / filter etc over the Predicate / Function combos that form the core pattern matching logic.

Cyclops Pattern Matching is structured into two packages

  1. A core which holds the cases to be executed
  2. A set of builders which aims to make building pattern matching expressions simpler

Builders can build ontop of builders. The matchable interface provides the highest level of abstraction and is the recommended starting point.

Conversely Case and Cases provide the lowest level inputs into the pattern matcher.

The Matchable interface / trait

Objects that implement Matchable get a number of Pattern Matching helper methods by default.

  • match : matches by value
  • _match : matches by type and value
  • matchType : matches by type only

Matchable wiki entry Matchable javadoc AsMatchable javadoc

Clean match statements

The cleanest way to use the Matchable instance is to encapsulate your matching logic inside a method with a name that indicates the intention of the matching. E.g.

	double benefits = employee.match(this::calcEmployeeBenefits);
	
	private CheckValues<I,T> calcEmployeeBenefits(CheckValues<I,T> c){
		return c.with(__,Bonus.PAYABLE,__).then(e->e.salary()*e.bonus())
		        .with(__,__,__).then(e->e.salary());
	}

Any Object can be coerced to a Matchable (even if they don't implement it directly via AsMatchable - e.g.

	double benefits = AsMatchable(employee).match(this::calcEmployeeBenefits);

match example

       new MyCase(4,2,3).match(this::message,"how are you?");
	
       private <I,T> CheckValues<Object, T> message(CheckValues<I, T> c) {
           return c.with(1,2,3).then(i->"hello")
                   .with(4,5,6).then(i->"goodbye");
	  }

Returns the default message "how are you?" as values 4,2,3 don't match 1,2,3 or 4,5,6

_match example

       new MyCase(4,5,6)._match(c ->c.isType( (MyCase ce)-> "hello").with(1,2,3),"goodbye")

Returns "goodbye" as altough the type matches, 1,2,3 doesn't match 4,5,6

matchType example

       new MyCase(4,5,6).matchType(c ->c.isType((MyCase ce) -> "hello")

Returns "hello" as MyCase is an instance of MyCase

Wildcards

com.aol.cyclops.matcher.Predicates

contains a number of Wildcard Predicates

Predicates.__ (double underscore) indicates a wild card

       new MyCase(4,5,6)._match(c ->c.isType( (MyCase ce)-> "hello").with(___,5,6),"goodbye")

The first value can be a Wildcard, the second and third should be 5 & 6.

Predicates.ANY() can also be used as a Wildcard. ANY() is capitalised to differentiate from Hamcrest Matchers any()

Recursive matching

It is possible to recursivley match on values. For example if the entity being matched on consists of other entities we can match recurisvely on those.

com.aol.cyclops.matcher.Predicates.with - facilitates recursive matching

e.g.

    new MyCase(1,new MyEntity(10,11),6)._match(c ->c.isType( (MyCase ce)-> "hello").with(___,with(10,__),6),"goodbye")

or in fully expanded form

	new MyCase(1,new MyEntity(10,11),6)._match(c ->c.isType( (MyCase ce)-> "hello").with(Predicates.___,Predicates.with(10,Predicates.__),6),"goodbye")

Interfaces that extend Matchable

  • ValueObject
  • StreamableValue
  • CachedValues, PTuple1-8

Coercing any Object to a Matchable

    As.asMatchable(myObject).match(this::makeFinancialDecision)

com.aol.cyclops.dynamic.As provides a range of methods to dynamically convert types/

The Decomposable Interface / Trait

The Decomposable Interface defines an unapply method that is used to convert the implementing Object into an iterable. This can be used to control how Cyclops performs recursive decomposition.

	public <I extends Iterable<?>> I unapply();

Interfaces that extend Decomposable

  • ValueObject
  • StreamableValue
  • CachedValues, PTuple1-8

Coercing any Object to a Decomposable

As.asDecomposable(myObject).unapply().forEach(System.out::println);

com.aol.cyclops.dynamic.As provides a range of methods to dynamically convert types

Creating Case classes

In Java it is possible to create sealed type hierarchies by reducing the visibilty of constructors. E.g. If the type hierarchies are defined in one file super class constructors can be made private and sub classes made final. This will prevent users from creating new classes externally. Lombok provides a number of annotations that make creating case classes simpler.

@Value : see https://projectlombok.org/features/Value.html

A sealed type hierarchy

An example sealed hierarchy (ValueObject implies both Matchable and Decomposable)

	@AllArgsConstructor(access=AccessLevel.PRIVATE) 
	public static class CaseClass implements ValueObject { } 
	@Value public static class MyCase1 extends CaseClass { int var1; String var2; }
	@Value public static class MyCase2 extends CaseClass { int var1; String var2; }

    CaseClass result;
    return result.match(this::handleBusinessCases);
    

The Matching class

Matching provides a number of builders for performing advanced pattern matching.

Features available via the Matching class include

  • Match by type, value, predicate or Hamcrest Matcher
  • Sequential, Parallel and Async execution
  • Recursively decompose and match against Case classes
  • Fluent step builders for common cases
  • Support for chain of responsibility pattern within a Stream
  • Support hamcrest matchers
  • Java 8 predicates for matching.
  • Match on first (return Optional)
  • Match many (return Stream)
  • Pre & post value extraction per case
  • Match using multiple in case expressions via tuples or iterables of predicates / matchers
  • Match against streams of data
  • Usable within a Stream (strategy pattern)
  • Fluent step builders
  • Define cases in situ via method chaining or plugin in variables (implement Consumer)
  • Match against collections with each element processed independently
  • Three case types (standard, atomised, stream) can be mixed within a single Matching test

Operators

At the top level the operators are

  • when : to define a new case
  • whenValues : to define a new case, potentially recursively matching against the internal values of an Object
  • whenFromStream : to a define a new case from a Stream of cases
  • whenIterable : to specifically handle the case where the Object to match is an iterable

Second level operators are

  • isType : provide a lambda / Function that will be used to both verify the type and provide a return value if triggered
  • isValue : compare object to match against specified value
  • isTrue : Use a Predicate to determine if Object matches
  • isMatch : Use a Hamcrest Matcher to determine if Object matches

Further Operators

  • thenApply : final action to determine result on match
  • thenConsume : final action with no return result on match
  • thenExtract : extract a new value to pass to next stage (or as result)

Special cases

Iteables

  • allTrue : all the predicates must match
  • allMatch : all the hamcrest matchers must match
  • allHold : allows mix of predicates, hamcrest matchers and prototype values all of which must hold

Streams

  • streamOfResponsibility : extract the matching cases from a Stream. Useful for introducing selection logic within your own Java 8 Streams

Examples :

With Hamcrest

    Matching.when().isMatch(hasItem("hello2")).thenConsume(System.out::println)
							.match(Arrays.asList("hello","world"))

methods xxMatch accept Hamcrest Matchers

Matching multiple

     Stream<Integer> resultStream = Matching.when().isValue(100).thenApply(v-> v+100)
											.when().isType((Integer i) -> i)
											.matchMany(100);

Use the matchMany method to instruct cylops to return all results that match

Inside a Stream

#### flatMap

Use asStreamFunction to Stream multiple results back out of a set of Cases.

     Integer num = Stream.of(1)
							.flatMap(Matching.when().isValue(1).thenApply(i->i+10).asStreamFunction())
							.findFirst()
							.get();							

asStreamFunction converts the MatchingInstance into a function that returns a Stream. Perfect for use within flatMap.

map

	Integer num = Stream.of(1)
							.map(Matching.when().isValue(1).thenApply(i->i+10))
							.findFirst()
							.get().get();	

Or drop the second get() (which unwraps from an Optional) with

	Integer num = Stream.of(1)
							.map(Matching.when().isValue(1).thenApply(i->i+10).asUnwrappedFunction())
							.findFirst()
							.get();	

Async execution

Use the Async suffix - available on the Cases object, when calling match to run the pattern matching asynchronously, potentially on another thread.

		CompletableFuture<Integer> result =	Matching.when().isValue(100).thenApply(this::expensiveOperation1)
													.when().isType((Integer i) -> this.exepensiveOperation2(i))
													.cases()
													.matchAsync(100)		

The PatternMatcher class

The PatternMatcher builder is the core builder for Cyclops Cases, that other builder instances leverage to build pattern matching cases. It's API is unsuitable for general use in most applications, but can leveraged to build application specific Matching builders.

The patternMatcher class provides a lot of utility methods that are organisied as follows

  • inCaseOf : match with a Predicate and return a result
  • caseOf : match with a Predicate but no result will be returned
  • inMatchOf : match with a Hamcrest Matcher and return a result
  • matchOf : match with a Hamcrest Matcher but no result will be returned

cyclops-pattern-matching :

Advanced Scala-like pattern matching for Java 8

  • Sequential, Parallel and Async execution
  • Match by type, value, predicate or Hamcrest Matcher
  • Recursively decompose and match against Case classes
  • Fluent step builders for common cases
  • Fluent, functionally compositional monad-like core Case and Cases classes
  • Support for chain of responsibility pattern within a Stream
  • Support hamcrest matchers
  • Java 8 predicates for matching.
  • Match on first (return Optional)
  • Match many (return Stream)
  • Strict and lose typing
  • Pre & post value extraction per case
  • Match using multiple in case expressions via tuples or iterables of predicates / matchers
  • Match against streams of data
  • Usable within a Stream (strategy pattern)
  • Fluent step builders
  • Define cases in situ via method chaining or plugin in variables (implement Consumer)
  • Match against collections with each element processed independently
  • Three case types (standard, atomised, stream) can be mixed within a single Matching test

Matching with hamcrest

       Matching.when().isMatch(hasItem("hello2"))
                         .thenConsume(System.out::println)
	       .match(Arrays.asList("hello","world"));

    //no match

With pre-extraction<

       Matching.when().extract(Person::getAge)
                         .isMatch(greaterThan(21)
                         .thenConsume(System.out::println)
                     .match(new Person("bob",24));
                         

    //prints 24 
    //bobs age

Note on Extraction

If Person implements iterable (returning [name,age] - so age is at(1) )

then this code will also work

       Matching.when().extract(Extractors.at(1))
                         .isMatch(greaterThan(21)
                         .thenConsume(System.out::println)
                   .match(new Person("bob",24));

With Post Extraction

         Matching.when().isMatch(is(not(empty())))
                                        .thenExtract(get(1)) //get is faster (than at) lookup for indexed lists
					.thenConsume(System.out::println)
				.match(Arrays.asList(20303,"20303 is passing",true));

    //prints 20303 is passing

Matching with predicates

    Matching.when().isTrue((Integer a)-> a>100)
                      .thenApply(x->x*10)
            .match(101);

    //return Optional[1010]

With PostExtraction

    Matching.when().isTrue((Person person)->person.getAge()>18)
                      .thenExtract(Person::getName)
                      .thenApply(name-> name + " is an adult")
            .match(new Person("rosie",39))

Reusable cases

Example

         Matching.when(c->c.extract(Person::getName)
                                  .isTrue((String name)->name.length()>5)
                                  .thenApply(name->name+" is too long"))
	        .when(c->c.extract(Person::getName)
                                  .isTrue((String name)->name.length()<3)
                                  .thenApply(name->name+" is too short"))				
             .match(new Person("long name",9))

can be refactored to

    Consumer<AggregatedCase> nameTooLong => c->c.extract(Person::getName)
                                            .isTrue((String name)->name.length()>5) 
                                           .thenApply(name->name+" is too long");

    Consumer<AggregatedCase> nameTooShort => c->c.extract(Person::getName)
                                           .isTrue((String name)->name.length()<3)
                                           .thenApply(name->name+" is too short");

                Matching.when(nameTooLong)
                        .when(nameTooShort)
                        .match(new Person("long name",9))

Matching against Tuples or Collections

It is possible to match against user data as a collection with each element verified separately

E.g. For the user input [1,"hello",2] each element can be verified and processed independently

        Matching.whenIterable().allValues(10,ANY,2).thenApply(l->"case1")

			.whenIterable().allValues(1,3,8).thenApply(l->"case2")

			.whenIterable().bothTrue((Integer i)->i==1,(String s)->s.length()>0)
					.thenExtract(Extractors.<Integer,String>toTuple2())
					.thenApply(t->"Integer at pos 0 is " + t.v1+ " + String at pos 1 is " +t.v2)

			.match(1,"hello",2)

Strategy pattern within a Stream

     Stream.of(1,2,3,4).map(Matching.when().isType((GenericRule rule)> selectRuleBuilder(rule)))
					    .map(o->o.orElse(defaultRuleBuilder())
                        .map(RuleBuilder::applyRule)
                        .collect(Collectors.toList());

Chain of responsibility pattern within a Stream

A chain of responsibility can be implemented by creating a Stream of objects that implement ChainOfResponsibility.

ChainOfResponsibility is an interface that extends both Predicate and Function.

The Matcher will test each member of the chain via it's Predicate.test method with the current value to match against, if it passes, the Matcher will forward the current value to the apply method on the selected member.

Example with default behaviour

         return	Seq.seq(urlListMatchRules)
					.map(Matching.whenFromStream().streamOfResponsibility(domainPathExpresssionBuilders.stream()))
					.map(o->o.orElse(new Expression()))  //type is Optional<Expression>
					.toList();

    //if there is no match we create a new Expression, otherwise we generate an expression
    // from the first member of the Chain of Responsibility to match

Example where no match is unnacceptable

       return	Seq.seq(urlListMatchRules)
					.map(Matching.whenFromStream().streamOfResponsibility(domainPathExpresssionBuilders.stream()).asUnwrappedFunction())
					.toList(); 

    //throws NoSuchElementException is no match found

Example where no match is acceptable

	private final List<ChainOfResponsibility<UrlListMatchRule,Expression>> domainPathExpresssionBuilders;
	
	return	Seq.seq(urlListMatchRules)
					.flatMap(Matching.whenFromStream().streamOfResponsibility(domainPathExpresssionBuilders.stream()).asStreamFunction())
					.toList();

    //empty list if no match

Example where multiple matches are acceptable

	return	Seq.seq(urlListMatchRules)
					.flatMap(Matching.whenFromStream().selectFromChain(domainPathExpresssionBuilders.stream())::matchMany)
					.toList();

    //in this case each rule can result in multiple Expressions being produced

Scala parser example

parser.eval(expr, 3) == 19


	public Integer eval(Expression expression, int xValue){

		
		return Matching.when().isType( (X x)-> xValue)
				.when().isType((Const c) -> c.getValue())
				.when().isType((Add a) ->  eval(a.getLeft(),xValue) + eval(a.getRight(),xValue))
				.when().isType( (Mult m) -> eval(m.getLeft(),xValue) * eval(m.getRight(),xValue))
				.when().isType( (Neg n) ->  -eval(n.getExpr(),xValue))
				.match(expression).orElse(1);
		
		
	}
	
	
	
	static class Expression{ }
	
	static class X extends Expression{ }
	
	@AllArgsConstructor
	@FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE)
	@Getter
	static class Const extends Expression{
		int value;
		
	}
	@AllArgsConstructor
	@FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE)
	@Getter
	static class Add extends Expression{
		Expression left;
		Expression right;
		
	}
	
	@AllArgsConstructor
	@FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE)
	@Getter
	static class Mult extends Expression{
		Expression left;
		Expression right;
		
	}
	@AllArgsConstructor (access=AccessLevel.PROTECTED) 
	@FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE)
	@Getter
	static class Neg extends Expression{
		Expression expr;
		
		
	}

Using the PatternMatcher Builder directly

Looser typing

Can match one value

    String result = new PatternMatcher()
	        		.inCaseOfValue(5,at(0),r-> "found "+r)
	        		.inCaseOfValue(10,at(0),r-> "found 10")
	        		.inCaseOfType(at(1),(FileNotFoundException e) -> "file not found")
	        		.inCaseOf(at(2),(Integer value)->value>1000,value -> "larger than 1000")
	        		.caseOf(at(2),(Integer value)->value>1000,System.out::println)
	                .<String>match(ImmutableList.of(10,Optional.empty(),999))
	                .orElse("ok");

or many

    List data = new PatternMatcher().inCaseOfType((String s) -> s.trim())
				.inCaseOfType((Integer i) -> i+100)
				.inCaseOfValue(100, i->"jackpot")
				.matchMany(100)
				.collect(Collectors.toList());

Match against many from a Stream

    List data = new PatternMatcher().inCaseOfType((String s) -> s.trim())
				.inCaseOfType((Integer i) -> i+100)
				.inCaseOfValue(100, i->"jackpot")
				.matchManyFromStream(Stream.of(100,200,300,100," hello "))
				.collect(Collectors.toList());
Clone this wiki locally