Skip to content

Commit 8c53086

Browse files
authored
Lazy For() Comprehension Generator (#3085)
Introduces a generator for lazy for-comprehension variants that produce intermediate `ForLazyN<M>` classes for supported monadic types (e.g., _Option_, _Try_, _Either_, etc.). This addition complements the existing eager for-comprehension generator by introducing a lazy variant. - Added generation of `ForLazyN<M`> classes (arity 2 to N) that: - Use one monadic source (ts1) followed by a chain of dependent functions (ts2, ts3, …). - Allow lazy composition of monadic operations through nested lambdas. - Added corresponding `For(...)` factory methods for convenient construction. - Implemented a recursively generated `yield(...)` method with proper indentation and block structure for readability. The generator handles mixed monadic/function parameter patterns such as: ```java Option<T1> ts1; Function1<T1, Option<T2>> ts2; Function2<T1, T2, Option<T3>> ts3; ``` expands for-comprehension with lazily-evaluated variants, so that the existing is possible: ```java Option<Integer> result = API.For( calculate1(), r1 -> calculate2(r1), (r1, r2) -> calculate3(r1, r2)).yield((r1, r2, r3) -> r1 + r2 + r3); ``` The generator enforces that the first argument is always a monadic value, not a function. This is necessary because: - The first argument acts as the entry point of the comprehension; it provides the initial context for all later `flatMap` calls. - If _ts1_ were also a function (e.g., `Function0<M<T1>>`), **Java’s generic type inference fails to resolve chained type variables** properly due to the presence of methods with similar erasure. - Subsequent arguments can safely be functions (`FunctionN<T1, ..., TN, M<T(N+1)>>`), since their input types are already determined by previous monadic values. ---- related: #3038
1 parent 6cfdd63 commit 8c53086

File tree

3 files changed

+4234
-0
lines changed

3 files changed

+4234
-0
lines changed

vavr/generator/Generator.scala

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,151 @@ def generateMainClasses(): Unit = {
743743
"""
744744
}
745745

746+
def genLazyFor(im: ImportManager, packageName: String, className: String): String = {
747+
xs"""
748+
${
749+
monadicTypesFor
750+
.filterNot(_ == "Iterable")
751+
.gen(mtype => (2 to N).gen(i => {
752+
val forClassName = s"ForLazy$i$mtype"
753+
val isComplex = monadicTypesThatNeedParameter.contains(mtype)
754+
val parameterInset = if (isComplex) "L, " else ""
755+
val generics = parameterInset + (1 to i).gen(j => s"T$j")(", ")
756+
757+
val params = (1 to i).gen { j =>
758+
if (j == 1)
759+
s"$mtype<${parameterInset}T1> ts1"
760+
else {
761+
val inputTypes = (1 until j).gen(k => s"? super T$k")(", ")
762+
s"Function${j - 1}<$inputTypes, $mtype<${parameterInset}T$j>> ts$j"
763+
}
764+
}(", ")
765+
766+
val ctorArgs = (1 to i).gen(j => s"ts$j")(", ")
767+
768+
xs"""
769+
/$javadoc
770+
* Creates a lazy {@code For}-comprehension over ${i.numerus(mtype)}.
771+
*
772+
* <p>The first argument ({@code ts1}) is the initial ${mtype}. Each subsequent
773+
* argument ({@code ts2} .. {@code ts$i}) is a function that receives all values
774+
* bound so far and returns the next ${mtype}. This method only constructs the
775+
* lazy comprehension; underlying effects are evaluated when {@code yield(...)}
776+
* is invoked.</p>
777+
*
778+
${(0 to i).gen(j => if (j == 0) "*" else s"* @param ts$j the ${j.ordinal} ${mtype}")("\n")}
779+
${if (isComplex) s"* @param <L> the common left-hand type of all ${mtype}s\n" else ""}
780+
${(1 to i).gen(j => s"* @param <T$j> the component type of the ${j.ordinal} ${mtype}")("\n")}
781+
* @return a new {@code $forClassName} builder of arity $i
782+
* @throws NullPointerException if any argument is {@code null}
783+
*/
784+
public static <$generics> $forClassName<$generics> For($params) {
785+
${(1 to i).gen(j => xs"""$Objects.requireNonNull(ts$j, "ts$j is null");""")("\n")}
786+
return new $forClassName<>($ctorArgs);
787+
}
788+
"""
789+
})("\n\n"))("\n\n")
790+
}
791+
792+
${
793+
monadicTypesFor
794+
.filterNot(_ == "Iterable")
795+
.gen(mtype => (2 to N).gen(i => {
796+
val rtype = mtype
797+
val forClassName = s"ForLazy$i$mtype"
798+
val parameterInset = if (monadicTypesThatNeedParameter.contains(mtype)) "L, " else ""
799+
val generics = parameterInset + (1 to i).gen(j => s"T$j")(", ")
800+
801+
val fields = (1 to i).gen { j =>
802+
if (j == 1)
803+
s"private final $mtype<${parameterInset}T1> ts1;"
804+
else {
805+
val inputTypes = (1 until j).gen(k => s"? super T$k")(", ")
806+
s"private final Function${j - 1}<$inputTypes, $mtype<${parameterInset}T$j>> ts$j;"
807+
}
808+
}("\n")
809+
810+
val ctorParams = (1 to i).gen { j =>
811+
if (j == 1)
812+
s"$mtype<${parameterInset}T1> ts1"
813+
else {
814+
val inputTypes = (1 until j).gen(k => s"? super T$k")(", ")
815+
s"Function${j - 1}<$inputTypes, $mtype<${parameterInset}T$j>> ts$j"
816+
}
817+
}(", ")
818+
819+
val assignments = (1 to i).gen(j => s"this.ts$j = ts$j;")("\n")
820+
821+
val yieldBody = {
822+
def nestedLambda(j: Int): String = {
823+
val base = " " * 23
824+
val indent = " " * (j + 1)
825+
if (j == i) {
826+
val argsList = (1 to i).map(k => s"t$k").mkString(", ")
827+
val inputArgs = (1 until i).map(k => s"t$k").mkString(", ")
828+
s"ts$i.apply($inputArgs).map(t$i -> f.apply($argsList))"
829+
} else if (j == 1) {
830+
s"ts1.flatMap(t1 -> {\n" +
831+
s"${base}${indent} return ${nestedLambda(j + 1)};\n" +
832+
s"${base}${indent}})"
833+
} else {
834+
val inputArgs = (1 until j).map(k => s"t$k").mkString(", ")
835+
s"ts$j.apply($inputArgs).flatMap(t$j -> {\n" +
836+
s"${base}${indent} return ${nestedLambda(j + 1)};\n" +
837+
s"${base}${indent}})"
838+
}
839+
}
840+
841+
nestedLambda(1)
842+
}
843+
844+
xs"""
845+
/$javadoc
846+
* A lazily evaluated {@code For}-comprehension with ${i.numerus(mtype)}.
847+
*
848+
* <p>Constructed via {@code For(...)} and evaluated by calling {@code yield(...)}.
849+
* Construction is side-effect free; underlying ${i.plural(mtype)} are traversed
850+
* only when {@code yield(...)} is invoked.</p>
851+
*
852+
${if (monadicTypesThatNeedParameter.contains(mtype)) s"* @param <L> the common left-hand type of all ${mtype}s\n" else ""}
853+
${(1 to i).gen(j => s"* @param <T$j> the component type of the ${j.ordinal} ${mtype}")("\n")}
854+
*/
855+
public static class $forClassName<$generics> {
856+
857+
$fields
858+
859+
private $forClassName($ctorParams) {
860+
$assignments
861+
}
862+
863+
/$javadoc
864+
* Produces results by mapping the Cartesian product of all bound values.
865+
*
866+
* <p>Evaluation is lazy and delegated to the underlying ${i.plural(mtype)} by
867+
* composing {@code flatMap} and {@code map} chains.</p>
868+
*
869+
* @param f a function mapping a tuple of bound values to a result
870+
* @param <R> the element type of the resulting {@code $rtype}
871+
* @return an {@code $rtype} containing mapped results
872+
* @throws NullPointerException if {@code f} is {@code null}
873+
*/
874+
public <R> $rtype<${parameterInset}R> yield(${
875+
if (i == 2)
876+
s"BiFunction<? super T1, ? super T2, ? extends R>"
877+
else
878+
s"Function$i<${(1 to i).map(j => s"? super T$j").mkString(", ")}, ? extends R>"
879+
} f) {
880+
$Objects.requireNonNull(f, "f is null");
881+
return $yieldBody;
882+
}
883+
}
884+
"""
885+
})("\n\n"))("\n\n")
886+
}
887+
"""
888+
}
889+
890+
746891
def genFor(im: ImportManager, packageName: String, className: String): String = {
747892
xs"""
748893
//
@@ -1440,6 +1585,8 @@ def generateMainClasses(): Unit = {
14401585

14411586
${genFor(im, packageName, className)}
14421587

1588+
${genLazyFor(im, packageName, className)}
1589+
14431590
${genMatch(im, packageName, className)}
14441591
}
14451592
"""
@@ -2975,8 +3122,27 @@ def generateTestClasses(): Unit = {
29753122
).yield(${(i > 1).gen("(")}${(1 to i).gen(j => s"i$j")(", ")}${(i > 1).gen(")")} -> ${(1 to i).gen(j => s"i$j")(" + ")});
29763123
$assertThat(result.get()).isEqualTo(${(1 to i).sum});
29773124
}
3125+
3126+
@$test
3127+
public void shouldIterateLazyFor$mtype$i() {
3128+
final $mtype<${parameterInset}Integer> result = For(
3129+
${(1 to i).gen(j => if (j == 1) {
3130+
s"$mtype.${builderName}($j)"
3131+
} else {
3132+
val args = (1 until j).map(k => s"r$k").mkString(", ")
3133+
val argsUsed = (1 until j).map(k => s"r$k").mkString(" + ")
3134+
s"($args) -> $mtype.${builderName}(${argsUsed} + $j)"
3135+
} )(",\n")}
3136+
).yield(${(i > 1).gen("(")}${(1 to i).gen(j => s"i$j")(", ")}${(i > 1).gen(")")} -> ${(1 to i).gen(j => s"i$j")(" + ")});
3137+
3138+
// Each step builds on the sum of all previous results plus its index
3139+
// This forms a sequence rₙ = 2ⁿ - 1, and the yield sums all rᵢ.
3140+
// Hence total = Σ(2ⁱ - 1) for i = 1..n = (2ⁿ⁺¹ - 2) - n
3141+
assertThat(result.get()).isEqualTo((1 << ($i + 1)) - 2 - $i);
3142+
}
29783143
"""})("\n\n"))("\n\n")}
29793144

3145+
29803146
${monadicFunctionTypesFor.gen(mtype => (1 to N).gen(i => { xs"""
29813147
@$test
29823148
public void shouldIterateFor$mtype$i() {

0 commit comments

Comments
 (0)