Skip to content

pivovarit/for-comprehension4j

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

78 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

for-comprehension4j

ci pitest Maven Central Version javadoc

A fluent for-comprehension API for Java, inspired by Scala's for-expressions. It allows the composition of multiple monadic or collection-like types in a type-safe and readable manner.

Getting Started

Maven:

<dependency>
    <groupId>com.pivovarit</groupId>
    <artifactId>for-comprehension4j</artifactId>
    <version>0.0.1</version>
</dependency>

Gradle (Kotlin DSL):

implementation("com.pivovarit:for-comprehension4j:0.0.1")

Gradle (Groovy DSL):

implementation 'com.pivovarit:for-comprehension4j:0.0.1'

Usage

All examples assume:

import static com.pivovarit.forc.ForComprehension.forc;

Optional

Instead of nesting multiple flatMap and map calls, you can use forc to simulate a for-comprehension.

Eager (all inputs are provided upfront):

Optional<Integer> width = Optional.of(3);
Optional<Integer> height = Optional.of(4);

Optional<Integer> area = forc(width, height)
  .yield((w, h) -> w * h);

// area -> Optional[12]

Lazy (later values can depend on earlier ones):

Optional<Integer> width = Optional.of(3);

Optional<Integer> area = forc(
    width,
    w -> Optional.of(w + 1)
  ).yield((w, h) -> w * h);

// area -> Optional[12]

Filter / guard clauses

All comprehension types support an optional .filter() step that acts as a guard, keeping only combinations where the predicate holds. Multiple .filter() calls are AND-composed.

Optional - guard failure produces Optional.empty():

Optional<Integer> width  = Optional.of(4);
Optional<Integer> height = Optional.of(3);

Optional<Integer> area = forc(width, height)
  .filter((w, h) -> w > h)
  .yield((w, h) -> w * h);

// area -> Optional[12]  (4 > 3, so the guard passes)
Optional<Integer> area = forc(Optional.of(2), Optional.of(3))
  .filter((w, h) -> w > h)
  .yield((w, h) -> w * h);

// area -> Optional.empty()  (2 > 3 is false, guard fails)

Stream / Iterable - guard filters the cartesian product:

Stream<String> roles  = Stream.of("admin", "user");
Stream<String> scopes = Stream.of("read", "write");

Stream<String> allowed = forc(roles, scopes)
  .filter((role, scope) -> !(role.equals("user") && scope.equals("write")))
  .yield((role, scope) -> role + ":" + scope);

// allowed -> ["admin:read", "admin:write", "user:read"]

.filter() works the same way on lazy comprehensions:

// Optional - guard is applied after all values are bound
Optional<Integer> area = forc(
    Optional.of(4),
    w -> Optional.of(w - 2)          // height depends on width
  ).filter((w, h) -> w > h)
   .yield((w, h) -> w * h);

// area -> Optional[8]  (4 > 2, guard passes)
// Stream - filter applied to the lazily-generated cartesian product
Stream<String> result = forc(
    Stream.of("admin", "user"),
    role -> Stream.of(role + ":read", role + ":write")
  ).filter((role, permission) -> !permission.equals("user:write"))
   .yield((role, permission) -> permission);

// result -> ["admin:read", "admin:write", "user:read"]
// Iterable
List<String> result = forc(
    List.of("admin", "user"),
    role -> List.of(role + ":read", role + ":write")
  ).filter((role, permission) -> !permission.equals("user:write"))
   .yield((role, permission) -> permission);

// result -> ["admin:read", "admin:write", "user:read"]

.filter() is available for all arities and container types.

Stream

The same approach works with Stream values, producing a cartesian product of all elements.

Eager:

Stream<Integer> s1 = Stream.of(1, 2);
Stream<Integer> s2 = Stream.of(10, 20);

Stream<Integer> result = forc(s1, s2)
  .yield(Integer::sum);

// result -> [11, 21, 12, 22]

Lazy (the second stream can depend on elements from the first):

Stream<Integer> result = forc(
    Stream.of(1, 2),
    v1 -> Stream.of(v1 * 10, v1 * 20)
  ).yield(Integer::sum);

// result -> [11, 21, 22, 42]

Note on Stream: streams are single-use. In the eager Stream variant, later streams may be materialized internally to support generating the cartesian product reliably. If you need fully lazy behavior end-to-end, use the lazy overload.

Iterable

Iterable works similarly, producing a cartesian product and returning a List<R>.

Eager:

Iterable<Integer> i1 = List.of(1, 2);
Iterable<Integer> i2 = List.of(10, 20);

List<Integer> result = forc(i1, i2)
  .yield(Integer::sum);

// result -> [11, 21, 12, 22]

Lazy:

List<Integer> result = forc(
    List.of(1, 2),
    v1 -> List.of(v1 * 10, v1 * 20)
  ).yield(Integer::sum);

// result -> [11, 21, 22, 42]

About

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Contributors

Languages