diff --git a/CHANGELOG.md b/CHANGELOG.md index be61d3de..2a5853cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ ## CHANGELOG ## [Unreleased] +- Future notes. +### Features +- Overhaul list zipping: make Cons(head, tail) less lazy, preserving laziness only for infinite sublists. ### Docs - Documentation: Complete 16-week v2.0 production readiness plan (docs/PLAN.md) with detailed weekly breakdowns, parallel work streams, risk mitigation, and success metrics - Documentation: Update README.md to properly link and describe the v2.0 production readiness plan @@ -24,9 +27,9 @@ - Documentation: Add cross-references between BREAKING_CHANGES.md and API_AUDIT_v2.0.md for improved navigation - Documentation: Update README.md with v2.0 release planning section and migration guides - Documentation: Remove registry-related items from the v2.0 plan to align with annotation-processor-driven code generation. +- Documentation: Add initial documentation for core concepts in docs/CORE_CONCEPTS.md, covering functional and algebraic structures, annotations, type hierarchy, and minimal implementations. ### Infrastructure - Pipeline compiles in java 17, 21 and 24 -- Future notes. ### Code smells - Some sonar code smells removed by refactoring and code cleanup, but many remain due to the nature of the generated code and reflection utilities. Focus is on critical issues and maintainable code rather than eliminating all smells. diff --git a/README.md b/README.md index 5f894b8b..7e803bd5 100644 --- a/README.md +++ b/README.md @@ -60,22 +60,9 @@ Welcome to the complete documentation for **Functional Java by Annotations**. Th **Understanding the library** -- **[Core Concepts](docs/CORE_CONCEPTS.md)** - - Type hierarchy and structure - - Functional vs Algebraic structures - - Minimal implementations - - Type constructors and variance - -- **[Functional Structures](docs/FUNCTIONAL_STRUCTURES.md)** - - Functor, Applicative, Monad - - Foldable, Traversal - - Alternative - - Laws and requirements - -- **[Algebraic Structures](docs/ALGEBRAIC_STRUCTURES.md)** - - Semigroup, Monoid - - Ring - - Laws and examples +- **[Core Concepts](docs/CORE_CONCEPTS.md)**: Overview of functional and algebraic structures, annotations, type hierarchy, and minimal implementations. +- **[Functional Structures](docs/FUNCTIONAL_STRUCTURES.md)**: Details on Functor, Applicative, Monad, Foldable, Traversal, Alternative, and their laws. +- **[Algebraic Structures](docs/ALGEBRAIC_STRUCTURES.md)**: Details on Semigroup, Monoid, Ring, and their laws. ### 📖 Practical Guides diff --git a/docs/CORE_CONCEPTS.md b/docs/CORE_CONCEPTS.md index e69de29b..6d515e1a 100644 --- a/docs/CORE_CONCEPTS.md +++ b/docs/CORE_CONCEPTS.md @@ -0,0 +1,37 @@ +# Core Concepts + +## Introduction +This project leverages Java annotations to define and implement functional and algebraic structures. It enables a declarative approach to programming, making code more modular, composable, and easier to reason about. + +## Functional Structures +Functional structures are abstractions that allow operations on data in a context. Common examples include: +- **Functor**: Enables mapping functions over values in a context. +- **Applicative**: Allows applying functions wrapped in a context to values in a context. +- **Monad**: Supports chaining computations in a context. +- **Foldable**: Reduces structures to a single value. +- **Traversal**: Maps with effects across structures. +- **Alternative**: Provides choice operations. + +## Algebraic Structures +Algebraic structures define mathematical operations and laws: +- **Semigroup**: Associative operation (`op`). +- **Monoid**: Semigroup with an identity element (`unit`). +- **Ring**: Supports addition and multiplication operations. + +## Annotations +Annotations such as `@Functor`, `@Monad`, `@Semigroup`, etc., are used to mark types and specify minimal required methods. The annotation processor generates boilerplate code and validates that the required methods are present. + +## Type Hierarchy and Structure +Types are organized hierarchically, with interfaces like `Algebraic` and `Structure` serving as base types. Each structure requires minimal implementations (e.g., `map` for Functor, `op` for Semigroup). + +## Minimal Implementations +Each annotation specifies the minimal set of methods required. For example: +- `@Functor`: Requires `map`. +- `@Monoid`: Requires `op` and `unit`. +- `@Ring`: Requires both addition and multiplication operations. + +## Type Constructors and Variance +The library supports generic types and type constructors, enabling flexible and reusable abstractions. + +--- +For more details, see [Functional Structures](FUNCTIONAL_STRUCTURES.md) and [Algebraic Structures](ALGEBRAIC_STRUCTURES.md). diff --git a/example-functional/src/main/java/com/dan323/functional/data/either/Left.java b/example-functional/src/main/java/com/dan323/functional/data/either/Left.java index c629056a..da8cf70f 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/either/Left.java +++ b/example-functional/src/main/java/com/dan323/functional/data/either/Left.java @@ -22,8 +22,12 @@ public C either(Function aToC, Function bToC) { @Override public boolean equals(Object obj) { if (obj == this) return true; - if (obj == null || obj.getClass() != getClass()) return false; - return Objects.equals(a, ((Left) obj).a); + if (obj == null) return false; + if (obj instanceof Left left) { + return Objects.equals(a, left.a); + } else { + return false; + } } @Override diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/Cons.java b/example-functional/src/main/java/com/dan323/functional/data/list/Cons.java index bbf8122e..2535a3de 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/Cons.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/Cons.java @@ -1,7 +1,6 @@ package com.dan323.functional.data.list; -import com.dan323.functional.data.optional.Maybe; - +import java.util.function.BiFunction; import java.util.function.Function; /** @@ -23,8 +22,8 @@ final class Cons extends InfiniteList { } @Override - public Maybe head() { - return Maybe.of(head); + public A getHead() { + return head; } @Override @@ -37,6 +36,11 @@ public InfiniteList map(Function mapping) { return new Cons<>(mapping.apply(head), tail().map(mapping)); } + public InfiniteList zipBy(BiFunction mapper, InfiniteList list) { + var newHead = mapper.apply(head,list.getHead()); + return new Cons<>(newHead, (InfiniteList) ZipApplicative.liftA2(mapper, tail, list.tail())); + } + @Override public String toString() { return "[" + head + "," + tail + "]"; diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/Cycle.java b/example-functional/src/main/java/com/dan323/functional/data/list/Cycle.java new file mode 100644 index 00000000..2dedeead --- /dev/null +++ b/example-functional/src/main/java/com/dan323/functional/data/list/Cycle.java @@ -0,0 +1,31 @@ +package com.dan323.functional.data.list; + +import java.util.function.Function; + +public final class Cycle extends InfiniteList { + + private final FiniteList cycled; + + Cycle(FiniteList cycle) { + if (cycle == null || cycle.length() == 0) { + throw new IllegalArgumentException("The list to be cycled must not be null or empty."); + } + this.cycled = cycle; + } + + @Override + public A getHead() { + return cycled.head().maybe(Function.identity(), null); + } + + @Override + public InfiniteList tail() { + var init = cycled.tail(); + return ListUtils.concat(init, this); + } + + @Override + public Cycle map(Function mapping) { + return new Cycle<>(cycled.map(mapping)); + } +} diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/FiniteList.java b/example-functional/src/main/java/com/dan323/functional/data/list/FiniteList.java index 9ace6606..7bdfac9e 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/FiniteList.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/FiniteList.java @@ -43,6 +43,11 @@ static FiniteList of(A... a) { } } + @Override + default FiniteList cons(A head) { + return cons(head, this); + } + @SafeVarargs private static FiniteList of(int n, A... a) { if (n >= a.length) { diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/FiniteListFunctional.java b/example-functional/src/main/java/com/dan323/functional/data/list/FiniteListFunctional.java index 652126a1..281d582f 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/FiniteListFunctional.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/FiniteListFunctional.java @@ -43,7 +43,7 @@ public static AlternativeMonoid, FiniteList> getAlternative } public static FiniteList map(FiniteList finiteList, Function mapping) { - return finiteList.head().maybe(h -> FiniteList.cons(mapping.apply(h), map(finiteList.tail(), mapping)), FiniteList.nil()); + return finiteList.map(mapping); } public static FiniteList disjunction(FiniteList op1, FiniteList op2) { diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/Generating.java b/example-functional/src/main/java/com/dan323/functional/data/list/Generating.java index edc5a95b..7f109660 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/Generating.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/Generating.java @@ -1,8 +1,5 @@ package com.dan323.functional.data.list; -import com.dan323.functional.data.optional.Maybe; - -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.UnaryOperator; @@ -20,8 +17,8 @@ final class Generating extends InfiniteList { } @Override - public Maybe head() { - return Maybe.of(head); + public A getHead() { + return head; } @Override @@ -36,17 +33,17 @@ public InfiniteList map(Function mapping) { protected static final class GeneratingMapped extends InfiniteList { - private final List originalList; + private final InfiniteList originalList; private final Function mapping; - GeneratingMapped(List originalList, Function mapping){ + GeneratingMapped(InfiniteList originalList, Function mapping){ this.mapping = mapping; this.originalList = originalList; } @Override - public Maybe head() { - return originalList.head().maybe(h -> Maybe.of(mapping.apply(h)), Maybe.of()); + public B getHead() { + return mapping.apply(originalList.getHead()); } @Override diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/InfiniteList.java b/example-functional/src/main/java/com/dan323/functional/data/list/InfiniteList.java index c8838844..a6e723ab 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/InfiniteList.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/InfiniteList.java @@ -1,5 +1,7 @@ package com.dan323.functional.data.list; +import com.dan323.functional.data.optional.Maybe; + import java.util.function.Function; /** @@ -7,13 +9,25 @@ * * @param type of elements in the list */ -public sealed abstract class InfiniteList implements List permits Cons, Generating, Generating.GeneratingMapped, Repeat, Zipped { +public abstract sealed class InfiniteList implements List permits Cons, Cycle, Generating, Generating.GeneratingMapped, Repeat, Zipped { + + @Override + public abstract InfiniteList tail(); + + @Override + public abstract InfiniteList map(Function mapping); + + public abstract A getHead(); @Override - abstract public InfiniteList tail(); + public Maybe head() { + return Maybe.of(getHead()); + } @Override - abstract public InfiniteList map(Function mapping); + public InfiniteList cons(A head) { + return new Cons<>(head, this); + } /** * Infinite lists are incomparable in a finite amount of time diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/List.java b/example-functional/src/main/java/com/dan323/functional/data/list/List.java index 10c1eecc..de720f7c 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/List.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/List.java @@ -11,23 +11,14 @@ public sealed interface List permits FiniteList, InfiniteList { List tail(); + List cons(A head); + List map(Function mapping); - static List generate(A first, UnaryOperator generator){ + static InfiniteList generate(A first, UnaryOperator generator){ return new Generating<>(first, generator); } - static List cons(A first, List tail) { - if (first == null || tail == null) { - throw new IllegalArgumentException("No input can be null"); - } - if (tail instanceof FiniteList finiteTail){ - return new FinCons<>(first, finiteTail); - } else { - return new Cons<>(first, (InfiniteList) tail); - } - } - default FiniteList limit(int k){ return head().maybe(h -> limitWithHead(h, k), FiniteList.nil()); } @@ -40,11 +31,21 @@ private FiniteList limitWithHead(A h, int k){ } } - static List nil() { + static List cycle(FiniteList lst){ + if (lst.length() == 0) { + return nil(); + } else if (lst.length() == 1) { + return lst.head().maybe(List::repeat,nil()); + } else { + return new Cycle<>(lst); + } + } + + static FiniteList nil() { return (FiniteList) Nil.NIL; } - static List repeat(A a){ + static InfiniteList repeat(A a){ return new Repeat<>(a); } } \ No newline at end of file diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/ListUtils.java b/example-functional/src/main/java/com/dan323/functional/data/list/ListUtils.java index 74ec41e5..3709f14b 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/ListUtils.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/ListUtils.java @@ -7,6 +7,10 @@ public static FiniteList reverse(FiniteList lst) { } public static FiniteList concat(FiniteList a, FiniteList b){ - return a.head().maybe(h -> FiniteList.cons(h, concat(a.tail(), b)), b); + return a.head().maybe(h -> concat(a.tail(), b).cons(h), b); + } + + public static InfiniteList concat(FiniteList a, InfiniteList b){ + return a.head().maybe(h -> concat(a.tail(), b).cons(h), b); } } diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/Repeat.java b/example-functional/src/main/java/com/dan323/functional/data/list/Repeat.java index 79176c22..a46bbbdd 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/Repeat.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/Repeat.java @@ -1,9 +1,5 @@ package com.dan323.functional.data.list; -import com.dan323.functional.data.optional.Maybe; - -import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.Function; /** @@ -21,8 +17,8 @@ final class Repeat extends InfiniteList { } @Override - public Maybe head() { - return Maybe.of(element); + public A getHead() { + return element; } @Override diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/ZipApplicative.java b/example-functional/src/main/java/com/dan323/functional/data/list/ZipApplicative.java index a22f2164..688c6317 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/ZipApplicative.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/ZipApplicative.java @@ -2,8 +2,6 @@ import com.dan323.functional.annotation.Applicative; import com.dan323.functional.annotation.funcs.IApplicative; -import com.dan323.functional.data.optional.Maybe; -import com.dan323.functional.data.optional.MaybeMonad; import java.util.function.BiFunction; import java.util.function.Function; @@ -15,19 +13,39 @@ public static List pure(A a) { return List.repeat(a); } + /** + * Depending on the type of the lists, we can have different implementations of liftA2. If one of the lists is finite, we can stop when we reach the end of it. If one of the lists is infinite, we can stop when we reach the end of the other list. If both lists are infinite, we can zip them together. + *
+ * If one of the lists is finite, we apply the bifunction sequentially until the shortest list is empty. + *
+ * If one of the lists is constantly repeating an element, we can create a mapped list by fixing + * in the bifunction the element of the repeating list and applying it to the other list. + * + * @param fun bifunction to apply to the elements of the lists + * @param lstA first list + * @param lstB second list + * @return a list containing the results of applying the bifunction to the elements of the lists + * @param
type of elements in the first list + * @param type of elements in the second list + * @param type of elements in the resulting list + */ public static List liftA2(BiFunction fun, List lstA, List lstB) { - if (lstA instanceof FiniteList finiteList) { - return finiteList.head().maybe(a -> lstB.head().maybe(b -> FiniteList.cons(fun.apply(a, b), (FiniteList) liftA2(fun, lstA.tail(), lstB.tail())), FiniteList.nil()), FiniteList.nil()); - } else if (lstB instanceof FiniteList finiteList) { - return finiteList.head().maybe(a -> lstA.head().maybe(b -> FiniteList.cons(fun.apply(b, a), (FiniteList) liftA2(fun, lstA.tail(), lstB.tail())), FiniteList.nil()), FiniteList.nil()); - } else if (lstA instanceof Repeat repeat) { - Maybe> aux = MaybeMonad.map(repeat.head(), a -> ((B b) -> fun.apply(a, b))); - return aux.maybe(lstB::map, List.nil()); - } else if (lstB instanceof Repeat repeat) { - Maybe> aux = MaybeMonad.map(repeat.head(), a -> ((A b) -> fun.apply(b, a))); - return aux.maybe(lstA::map, List.nil()); + if (lstA instanceof FiniteList) { + return lstA.head().maybe(a -> lstB.head().maybe(b -> FiniteList.cons(fun.apply(a, b), (FiniteList) liftA2(fun, lstA.tail(), lstB.tail())), FiniteList.nil()), FiniteList.nil()); + } else if (lstB instanceof FiniteList) { + return lstB.head().maybe(a -> lstA.head().maybe(b -> FiniteList.cons(fun.apply(b, a), (FiniteList) liftA2(fun, lstA.tail(), lstB.tail())), FiniteList.nil()), FiniteList.nil()); + } else if (lstA instanceof Repeat ra) { + Function auxFun = b -> fun.apply(ra.getHead(), b); + return lstB.map(auxFun); + } else if (lstB instanceof Repeat rb) { + Function auxFun = b -> fun.apply(b, rb.getHead()); + return lstA.map(auxFun); + } else if (lstA instanceof Cons ca) { + return ca.zipBy(fun, (InfiniteList) lstB); + } else if (lstB instanceof Cons cb) { + return cb.zipBy((a,b) -> fun.apply(b,a), (InfiniteList) lstA); } else { - return new Zipped<>(lstA, fun, lstB); + return new Zipped<>((InfiniteList) lstA, fun, (InfiniteList) lstB); } } diff --git a/example-functional/src/main/java/com/dan323/functional/data/list/Zipped.java b/example-functional/src/main/java/com/dan323/functional/data/list/Zipped.java index 8fb9ee66..ba0291c9 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/list/Zipped.java +++ b/example-functional/src/main/java/com/dan323/functional/data/list/Zipped.java @@ -1,29 +1,20 @@ package com.dan323.functional.data.list; -import com.dan323.functional.data.optional.Maybe; -import com.dan323.functional.data.optional.MaybeMonad; - import java.util.function.BiFunction; import java.util.function.Function; final class Zipped extends InfiniteList { - private final List first; - private final List second; + private final InfiniteList first; + private final InfiniteList second; private final BiFunction zipper; - Zipped(List first, BiFunction zipper, List second) { + Zipped(InfiniteList first, BiFunction zipper, InfiniteList second) { this.first = first; this.zipper = zipper; this.second = second; } - @Override - public Maybe head() { - var aux = MaybeMonad.map(first.head(), x -> (Function) ((B b) -> zipper.apply(x, b))); - return MaybeMonad.fapply(aux, second.head()); - } - @Override public InfiniteList tail() { return new Zipped<>(first.tail(), zipper, second.tail()); @@ -34,4 +25,9 @@ public InfiniteList map(Function mapping) { return new Zipped<>(first, zipper.andThen(mapping), second); } + @Override + public C getHead() { + return zipper.apply(first.getHead(), second.getHead()); + } + } diff --git a/example-functional/src/main/java/com/dan323/functional/data/optional/Just.java b/example-functional/src/main/java/com/dan323/functional/data/optional/Just.java index 374aa8a5..70a14ae4 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/optional/Just.java +++ b/example-functional/src/main/java/com/dan323/functional/data/optional/Just.java @@ -29,7 +29,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(element); + return Objects.hash(element) * 5 + 7; } @Override diff --git a/example-functional/src/main/java/com/dan323/functional/data/optional/Nothing.java b/example-functional/src/main/java/com/dan323/functional/data/optional/Nothing.java index d2057883..77087ce0 100644 --- a/example-functional/src/main/java/com/dan323/functional/data/optional/Nothing.java +++ b/example-functional/src/main/java/com/dan323/functional/data/optional/Nothing.java @@ -1,6 +1,5 @@ package com.dan323.functional.data.optional; -import java.util.Objects; import java.util.function.Function; public final class Nothing implements Maybe { @@ -22,7 +21,7 @@ public boolean equals(Object obj) { @Override public int hashCode(){ - return 90; + return 7; } @Override diff --git a/example-functional/src/test/java/com/dan323/functional/ListFunctorTest.java b/example-functional/src/test/java/com/dan323/functional/ListFunctorTest.java index 13ca5c7f..129eb839 100644 --- a/example-functional/src/test/java/com/dan323/functional/ListFunctorTest.java +++ b/example-functional/src/test/java/com/dan323/functional/ListFunctorTest.java @@ -11,7 +11,7 @@ public class ListFunctorTest { @Test public void constInfiniteMap() { - var sol = ZipApplicative.map(List.cons(5, List.repeat(6)), x -> x * 2); + var sol = ZipApplicative.map(List.repeat(6).cons(5), x -> x * 2); assertEquals(FiniteList.of(10, 12, 12), sol.limit(3)); } @@ -33,6 +33,12 @@ public void repeatListFunctor() { assertEquals(List.repeat(15).limit(10), sol.limit(10)); } + @Test + public void cycleListFunctor() { + var sol = ZipApplicative.map(List.cycle(FiniteList.of(1,2,3)), x -> x * 3); + assertEquals(List.cycle(FiniteList.of(3,6,9)).limit(10), sol.limit(10)); + } + @Test public void generatingListFunctor() { // Since this type of lists cannot be checked in all their entries, we will test the first 10 elements diff --git a/example-functional/src/test/java/com/dan323/functional/ListTest.java b/example-functional/src/test/java/com/dan323/functional/ListTest.java index 622cf26f..7a25aca9 100644 --- a/example-functional/src/test/java/com/dan323/functional/ListTest.java +++ b/example-functional/src/test/java/com/dan323/functional/ListTest.java @@ -17,7 +17,7 @@ public void nilTail() { @Test public void consToFiniteList() { - var sol = List.cons(5, FiniteList.of(1, 2)); + var sol = FiniteList.of(1, 2).cons(5); assertEquals(FiniteList.of(5, 1, 2), sol); } } diff --git a/example-functional/src/test/java/com/dan323/functional/ZipApplicativeTest.java b/example-functional/src/test/java/com/dan323/functional/ZipApplicativeTest.java index f0b6567c..5d4248de 100644 --- a/example-functional/src/test/java/com/dan323/functional/ZipApplicativeTest.java +++ b/example-functional/src/test/java/com/dan323/functional/ZipApplicativeTest.java @@ -27,6 +27,15 @@ public void zipSum() { assertEquals(FiniteList.of(6, 7, 8), lst); } + @Test + public void zipCons() { + ZipApplicative zipApplicative = new ZipApplicative(); + List lst = (List) ApplicativeUtil.liftA2(zipApplicative, Integer::sum, List.repeat(5).cons(7), List.generate(1, x -> x + 1)); + assertEquals(FiniteList.of(8, 7, 8, 9), lst.limit(4)); + lst = (List) ApplicativeUtil.liftA2(zipApplicative, Integer::sum, List.generate(1, x -> x + 1).cons(3).cons(5), List.repeat(5)); + assertEquals(FiniteList.of(10, 8, 6, 7, 8), lst.limit(5)); + } + @Test public void zipGenerating() { ZipApplicative zipApplicative = new ZipApplicative();