Browser docs

Combinator

Also known as

Composition pattern

Intent

The functional pattern representing a style of organizing libraries centered around the idea of combining functions. Putting it simply, there is some type T, some functions for constructing “primitive” values of type T, and some “combinators” which can combine values of type T in various ways to build up more complex values of type T.

Explanation

Real world example

In computer science, combinatory logic is used as a simplified model of computation, used in computability theory and proof theory. Despite its simplicity, combinatory logic captures many essential features of computation.

In plain words

The combinator allows you to create new “things” from previously defined “things”.

Wikipedia says

A combinator is a higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.

Programmatic Example

Translating the combinator example above. First of all, we have a interface consist of several methods contains, not, or, and .

 1// Functional interface to find lines in text.
 2public interface Finder {
 3
 4	// The function to find lines in text.
 5	List<String> find(String text);
 6
 7	// Simple implementation of function {@link #find(String)}.
 8	static Finder contains(String word) {
 9    		return txt -> Stream.of(txt.split("\n"))
10        		.filter(line -> line.toLowerCase().contains(word.toLowerCase()))
11        		.collect(Collectors.toList());
12  	}
13
14	// combinator not.
15	default Finder not(Finder notFinder) {
16    		return txt -> {
17      			List<String> res = this.find(txt);
18      			res.removeAll(notFinder.find(txt));
19      			return res;
20    			};
21  	}
22
23	// combinator or.
24	default Finder or(Finder orFinder) {
25    		return txt -> {
26      			List<String> res = this.find(txt);
27      			res.addAll(orFinder.find(txt));
28      			return res;
29    			};
30	}
31
32	// combinator and.
33	default Finder and(Finder andFinder) {
34    		return
35        	txt -> this
36            		.find(txt)
37            		.stream()
38            		.flatMap(line -> andFinder.find(line).stream())
39            		.collect(Collectors.toList());
40  	}
41	...
42}

Then we have also another combinator for some complex finders advancedFinder, filteredFinder, specializedFinder and expandedFinder.

 1// Complex finders consisting of simple finder.
 2public class Finders {
 3
 4	private Finders() {
 5  	}
 6
 7	// Finder to find a complex query.
 8	public static Finder advancedFinder(String query, String orQuery, String notQuery) {
 9    		return
10        		Finder.contains(query)
11            			.or(Finder.contains(orQuery))
12            			.not(Finder.contains(notQuery));
13	}
14
15	// Filtered finder looking a query with excluded queries as well.
16	public static Finder filteredFinder(String query, String... excludeQueries) {
17		var finder = Finder.contains(query);
18
19    		for (String q : excludeQueries) {
20      			finder = finder.not(Finder.contains(q));
21    		}
22    		return finder;
23	}
24
25	// Specialized query. Every next query is looked in previous result.
26	public static Finder specializedFinder(String... queries) {
27    		var finder = identMult();
28
29		for (String query : queries) {
30      			finder = finder.and(Finder.contains(query));
31    		}
32    		return finder;
33  	}
34
35	// Expanded query. Looking for alternatives.
36	public static Finder expandedFinder(String... queries) {
37    		var finder = identSum();
38
39    		for (String query : queries) {
40      			finder = finder.or(Finder.contains(query));
41    		}
42   		return finder;
43  	}
44	...
45}

Now we have created the interface and methods for combinators. Now we have an application working on these combinators.

 1var queriesOr = new String[]{"many", "Annabel"};
 2var finder = Finders.expandedFinder(queriesOr);
 3var res = finder.find(text());
 4LOGGER.info("the result of expanded(or) query[{}] is {}", queriesOr, res);
 5
 6var queriesAnd = new String[]{"Annabel", "my"};
 7finder = Finders.specializedFinder(queriesAnd);
 8res = finder.find(text());
 9LOGGER.info("the result of specialized(and) query[{}] is {}", queriesAnd, res);
10
11finder = Finders.advancedFinder("it was", "kingdom", "sea");
12res = finder.find(text());
13LOGGER.info("the result of advanced query is {}", res);
14
15res = Finders.filteredFinder(" was ", "many", "child").find(text());
16LOGGER.info("the result of filtered query is {}", res);
17
18private static String text() {
19    return
20        "It was many and many a year ago,\n"
21            + "In a kingdom by the sea,\n"
22            + "That a maiden there lived whom you may know\n"
23            + "By the name of ANNABEL LEE;\n"
24            + "And this maiden she lived with no other thought\n"
25            + "Than to love and be loved by me.\n"
26            + "I was a child and she was a child,\n"
27            + "In this kingdom by the sea;\n"
28            + "But we loved with a love that was more than love-\n"
29            + "I and my Annabel Lee;\n"
30            + "With a love that the winged seraphs of heaven\n"
31            + "Coveted her and me.";
32  }

Program output:

1the result of expanded(or) query[[many, Annabel]] is [It was many and many a year ago,, By the name of ANNABEL LEE;, I and my Annabel Lee;]
2the result of specialized(and) query[[Annabel, my]] is [I and my Annabel Lee;]
3the result of advanced query is [It was many and many a year ago,]
4the result of filtered query is [But we loved with a love that was more than love-]

Now we can design our app to with the queries finding feature expandedFinder, specializedFinder, advancedFinder, filteredFinder which are all derived from contains, or, not, and.

Class diagram

alt text

Applicability

Use the combinator pattern when:

  • You are able to create a more complex value from more plain values but having the same type(a combination of them)

Benefits

  • From a developers perspective the API is made of terms from the domain.
  • There is a clear distinction between combining and application phase.
  • One first constructs an instance and then executes it.
  • This makes the pattern applicable in a parallel environment.

Real world examples

  • java.util.function.Function#compose
  • java.util.function.Function#andThen

Credits