docs: explanation for combinator (#2877)

This commit is contained in:
Ilkka Seppälä authored and GitHub committed 2024-03-30 14:44:43 +02:00
1 parent 30ac97fe59
commit 2228212c23
3 files changed
+179 -147

No files matched your search

+154 -123
View File
@@ -1,132 +1,136 @@
--- ---
title: Combinator title: Combinator
category: Idiom category: Functional
language: en language: en
tag: tag:
- Reactive - Idiom
- Reactive
--- ---
## Also known as ## Also known as
Composition pattern * Function Composition
* Functional Combinator
## Intent ## Intent
The functional pattern representing a style of organizing libraries centered around the idea of combining functions. The Combinator pattern is intended to enable complex functionalities by combining simple functions into more complex
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. ones. It aims to achieve modularization and reusability by breaking down a task into simpler, interchangeable components
that can be composed in various ways.
## Explanation ## Explanation
Real world example 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 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 In plain words
> The combinator allows you to create new "things" from previously defined "things". > The combinator allows you to create new "things" from previously defined "things"
>
Wikipedia says 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. > 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** **Programmatic Example**
Translating the combinator example above. First of all, we have a interface consist of several methods `contains`, `not`, `or`, `and` . Translating the combinator example above. First of all, we have an interface consist of several
methods `contains`, `not`, `or`, `and` .
```java ```java
// Functional interface to find lines in text. // Functional interface to find lines in text.
public interface Finder { public interface Finder {
// The function to find lines in text. // The function to find lines in text.
List<String> find(String text); List<String> find(String text);
// Simple implementation of function {@link #find(String)}. // Simple implementation of function {@link #find(String)}.
static Finder contains(String word) { static Finder contains(String word) {
return txt -> Stream.of(txt.split("\n")) return txt -> Stream.of(txt.split("\n"))
.filter(line -> line.toLowerCase().contains(word.toLowerCase())) .filter(line -> line.toLowerCase().contains(word.toLowerCase()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
// combinator not. // combinator not.
default Finder not(Finder notFinder) { default Finder not(Finder notFinder) {
return txt -> { return txt -> {
List<String> res = this.find(txt); List<String> res = this.find(txt);
res.removeAll(notFinder.find(txt)); res.removeAll(notFinder.find(txt));
return res; return res;
}; };
} }
// combinator or. // combinator or.
default Finder or(Finder orFinder) { default Finder or(Finder orFinder) {
return txt -> { return txt -> {
List<String> res = this.find(txt); List<String> res = this.find(txt);
res.addAll(orFinder.find(txt)); res.addAll(orFinder.find(txt));
return res; return res;
}; };
} }
// combinator and. // combinator and.
default Finder and(Finder andFinder) { default Finder and(Finder andFinder) {
return return
txt -> this txt -> this
.find(txt) .find(txt)
.stream() .stream()
.flatMap(line -> andFinder.find(line).stream()) .flatMap(line -> andFinder.find(line).stream())
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
... ...
} }
``` ```
Then we have also another combinator for some complex finders `advancedFinder`, `filteredFinder`, `specializedFinder` and `expandedFinder`. Then we have also another combinator for some complex finders `advancedFinder`, `filteredFinder`, `specializedFinder`
and `expandedFinder`.
```java ```java
// Complex finders consisting of simple finder. // Complex finders consisting of simple finder.
public class Finders { public class Finders {
private Finders() { private Finders() {
} }
// Finder to find a complex query. // Finder to find a complex query.
public static Finder advancedFinder(String query, String orQuery, String notQuery) { public static Finder advancedFinder(String query, String orQuery, String notQuery) {
return return
Finder.contains(query) Finder.contains(query)
.or(Finder.contains(orQuery)) .or(Finder.contains(orQuery))
.not(Finder.contains(notQuery)); .not(Finder.contains(notQuery));
} }
// Filtered finder looking a query with excluded queries as well. // Filtered finder looking a query with excluded queries as well.
public static Finder filteredFinder(String query, String... excludeQueries) { public static Finder filteredFinder(String query, String... excludeQueries) {
var finder = Finder.contains(query); var finder = Finder.contains(query);
for (String q : excludeQueries) { for (String q : excludeQueries) {
finder = finder.not(Finder.contains(q)); finder = finder.not(Finder.contains(q));
} }
return finder; return finder;
} }
// Specialized query. Every next query is looked in previous result. // Specialized query. Every next query is looked in previous result.
public static Finder specializedFinder(String... queries) { public static Finder specializedFinder(String... queries) {
var finder = identMult(); var finder = identMult();
for (String query : queries) { for (String query : queries) {
finder = finder.and(Finder.contains(query)); finder = finder.and(Finder.contains(query));
} }
return finder; return finder;
} }
// Expanded query. Looking for alternatives. // Expanded query. Looking for alternatives.
public static Finder expandedFinder(String... queries) { public static Finder expandedFinder(String... queries) {
var finder = identSum(); var finder = identSum();
for (String query : queries) { for (String query : queries) {
finder = finder.or(Finder.contains(query)); finder = finder.or(Finder.contains(query));
} }
return finder; return finder;
} }
... ...
} }
``` ```
@@ -134,75 +138,102 @@ public class Finders {
Now we have created the interface and methods for combinators. Now we have an application working on these combinators. Now we have created the interface and methods for combinators. Now we have an application working on these combinators.
```java ```java
var queriesOr = new String[]{"many", "Annabel"}; var queriesOr=new String[]{"many","Annabel"};
var finder = Finders.expandedFinder(queriesOr); var finder=Finders.expandedFinder(queriesOr);
var res = finder.find(text()); var res=finder.find(text());
LOGGER.info("the result of expanded(or) query[{}] is {}", queriesOr, res); LOGGER.info("the result of expanded(or) query[{}] is {}",queriesOr,res);
var queriesAnd = new String[]{"Annabel", "my"}; var queriesAnd=new String[]{"Annabel","my"};
finder = Finders.specializedFinder(queriesAnd); finder=Finders.specializedFinder(queriesAnd);
res = finder.find(text()); res=finder.find(text());
LOGGER.info("the result of specialized(and) query[{}] is {}", queriesAnd, res); LOGGER.info("the result of specialized(and) query[{}] is {}",queriesAnd,res);
finder = Finders.advancedFinder("it was", "kingdom", "sea"); finder=Finders.advancedFinder("it was","kingdom","sea");
res = finder.find(text()); res=finder.find(text());
LOGGER.info("the result of advanced query is {}", res); LOGGER.info("the result of advanced query is {}",res);
res = Finders.filteredFinder(" was ", "many", "child").find(text()); res=Finders.filteredFinder(" was ","many","child").find(text());
LOGGER.info("the result of filtered query is {}", res); LOGGER.info("the result of filtered query is {}",res);
private static String text() { private static String text(){
return return
"It was many and many a year ago,\n" "It was many and many a year ago,\n"
+ "In a kingdom by the sea,\n" +"In a kingdom by the sea,\n"
+ "That a maiden there lived whom you may know\n" +"That a maiden there lived whom you may know\n"
+ "By the name of ANNABEL LEE;\n" +"By the name of ANNABEL LEE;\n"
+ "And this maiden she lived with no other thought\n" +"And this maiden she lived with no other thought\n"
+ "Than to love and be loved by me.\n" +"Than to love and be loved by me.\n"
+ "I was a child and she was a child,\n" +"I was a child and she was a child,\n"
+ "In this kingdom by the sea;\n" +"In this kingdom by the sea;\n"
+ "But we loved with a love that was more than love-\n" +"But we loved with a love that was more than love-\n"
+ "I and my Annabel Lee;\n" +"I and my Annabel Lee;\n"
+ "With a love that the winged seraphs of heaven\n" +"With a love that the winged seraphs of heaven\n"
+ "Coveted her and me."; +"Coveted her and me.";
} }
``` ```
**Program output:** **Program output:**
```java ```java
the 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;] the 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;]
the result of specialized(and) query[[Annabel, my]] is [I and my Annabel Lee;] the result of specialized(and)query[[Annabel,my]]is[I and my Annabel Lee;]
the result of advanced query is [It was many and many a year ago,] the result of advanced query is[It was many and many a year ago,]
the result of filtered query is [But we loved with a love that was more than love-] the 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`. 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 ## Class diagram
![alt text](./etc/combinator.urm.png "Combinator class diagram") ![alt text](./etc/combinator.urm.png "Combinator class diagram")
## Applicability ## 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) This pattern is applicable in scenarios where:
## Benefits * The solution to a problem can be constructed from simple, reusable components.
* There is a need for high modularity and reusability of functions.
* The programming environment supports first-class functions and higher-order functions.
- From a developers perspective the API is made of terms from the domain. ## Known Uses
- 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.
* Functional programming languages like Haskell and Scala extensively use combinators for tasks ranging from parsing to
UI construction.
* In domain-specific languages, particularly those involved in parsing, such as parsing expression grammars.
* In libraries for functional programming in languages like JavaScript, Python, and Ruby.
* java.util.function.Function#compose
* java.util.function.Function#andThen
## Real world examples ## Consequences
- java.util.function.Function#compose Benefits:
- java.util.function.Function#andThen
* Enhances modularity and reusability by breaking down complex tasks into simpler, composable functions.
* Promotes readability and maintainability by using a declarative style of programming.
* Facilitates lazy evaluation and potentially more efficient execution through function composition.
Trade-offs:
* Can lead to a steep learning curve for those unfamiliar with functional programming principles.
* May result in performance overhead due to the creation of intermediate functions.
* Debugging can be challenging due to the abstract nature of function compositions.
## Related Patterns
[Strategy](https://java-design-patterns.com/patterns/strategy/): Both involve selecting an algorithm at runtime, but
Combinator uses composition of functions.
[Decorator](https://java-design-patterns.com/patterns/decorator/): Similar to Combinator in enhancing functionality, but
Decorator focuses on object augmentation.
[Chain of Responsibility](https://java-design-patterns.com/patterns/chain-of-responsibility/): Relies on chaining
objects, whereas Combinator chains functions.
## Credits ## Credits
- [Example for java](https://gtrefs.github.io/code/combinator-pattern/) - [Example for Java](https://gtrefs.github.io/code/combinator-pattern/)
- [Combinator pattern](https://wiki.haskell.org/Combinator_pattern) - [Combinator pattern](https://wiki.haskell.org/Combinator_pattern)
- [Combinatory logic](https://wiki.haskell.org/Combinatory_logic) - [Combinatory logic](https://wiki.haskell.org/Combinatory_logic)
- [Structure and Interpretation of Computer Programs](https://amzn.to/3PJwVsf)
- [Functional Programming in Scala](https://amzn.to/4cEo6K2)
- [Haskell: The Craft of Functional Programming](https://amzn.to/4axxtcF)
@@ -22,6 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package com.iluwatar.combinator; package com.iluwatar.combinator;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -33,12 +34,12 @@ class FinderTest {
@Test @Test
void contains() { void contains() {
var example = """ var example = """
the first one the first one
the second one\s the second one\s
"""; """;
var result = Finder.contains("second").find(example); var result = Finder.contains("second").find(example);
assertEquals(1, result.size()); assertEquals(1, result.size());
assertEquals( "the second one ", result.get(0)); assertEquals("the second one ", result.get(0));
} }
} }
@@ -22,6 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package com.iluwatar.combinator; package com.iluwatar.combinator;
import static com.iluwatar.combinator.Finders.advancedFinder; import static com.iluwatar.combinator.Finders.advancedFinder;
@@ -38,48 +39,47 @@ class FindersTest {
void advancedFinderTest() { void advancedFinderTest() {
var res = advancedFinder("it was", "kingdom", "sea").find(text()); var res = advancedFinder("it was", "kingdom", "sea").find(text());
assertEquals(1, res.size()); assertEquals(1, res.size());
assertEquals( "It was many and many a year ago,", res.get(0)); assertEquals("It was many and many a year ago,", res.get(0));
} }
@Test @Test
void filteredFinderTest() { void filteredFinderTest() {
var res = filteredFinder(" was ", "many", "child").find(text()); var res = filteredFinder(" was ", "many", "child").find(text());
assertEquals(1, res.size()); assertEquals(1, res.size());
assertEquals( "But we loved with a love that was more than love-", res.get(0)); assertEquals("But we loved with a love that was more than love-", res.get(0));
} }
@Test @Test
void specializedFinderTest() { void specializedFinderTest() {
var res = specializedFinder("love", "heaven").find(text()); var res = specializedFinder("love", "heaven").find(text());
assertEquals(1, res.size()); assertEquals(1, res.size());
assertEquals( "With a love that the winged seraphs of heaven", res.get(0)); assertEquals("With a love that the winged seraphs of heaven", res.get(0));
} }
@Test @Test
void expandedFinderTest() { void expandedFinderTest() {
var res = expandedFinder("It was", "kingdom").find(text()); var res = expandedFinder("It was", "kingdom").find(text());
assertEquals(3, res.size()); assertEquals(3, res.size());
assertEquals( "It was many and many a year ago,", res.get(0)); assertEquals("It was many and many a year ago,", res.get(0));
assertEquals( "In a kingdom by the sea,", res.get(1)); assertEquals("In a kingdom by the sea,", res.get(1));
assertEquals( "In this kingdom by the sea;", res.get(2)); assertEquals("In this kingdom by the sea;", res.get(2));
} }
private String text() { private String text() {
return return """
""" It was many and many a year ago,
It was many and many a year ago, In a kingdom by the sea,
In a kingdom by the sea, That a maiden there lived whom you may know
That a maiden there lived whom you may know By the name of ANNABEL LEE;
By the name of ANNABEL LEE; And this maiden she lived with no other thought
And this maiden she lived with no other thought Than to love and be loved by me.
Than to love and be loved by me. I was a child and she was a child,
I was a child and she was a child, In this kingdom by the sea;
In this kingdom by the sea; But we loved with a love that was more than love-
But we loved with a love that was more than love- I and my Annabel Lee;
I and my Annabel Lee; With a love that the winged seraphs of heaven
With a love that the winged seraphs of heaven Coveted her and me.""";
Coveted her and me.""";
} }
} }