From 1a26189ca3dbc65d098c18a717785e3cb0874504 Mon Sep 17 00:00:00 2001 From: Magnus Smith Date: Tue, 24 Dec 2024 17:32:55 +0000 Subject: [PATCH 1/4] first draft --- ...25-01-01-algebraic-data-types-with-java.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 _posts/2025-01-01-algebraic-data-types-with-java.md diff --git a/_posts/2025-01-01-algebraic-data-types-with-java.md b/_posts/2025-01-01-algebraic-data-types-with-java.md new file mode 100644 index 000000000..8df680902 --- /dev/null +++ b/_posts/2025-01-01-algebraic-data-types-with-java.md @@ -0,0 +1,59 @@ +title: Algebraic Data Types with Java +date: 2025-01-01 00:00:00 Z +categories: +- Tech + tags: +- Java + author: magnussmith + summary: In this post I explore the power of Algebraic Data Types in Java. + image: magnussmith/assets/java.jpg +--- + +## Introduction + +## What are Algebraic Data Types (ADT)? + +ADTs provide a way to define composite data types by combining other types in a structured and type-safe manner. They allow developers to model complex data structures using simpler building blocks, much like building with LEGOs. Think of them as custom, compound data types you design for your specific needs. + +## Why do we need them and what kind of problems to they help solve? + +ADTs make code more readable by explicitly defining the structure and possible values of complex data. This makes it easier to understand and reason about the code, leading to improved maintainability. + +ADTs use the type system to enforce constraints on the data. The compiler can detect errors at compile time, preventing runtime issues that might arise from invalid data structures or operations. + +Compared to using classes or structs alone, ADTs can often reduce the amount of boilerplate code needed to define and manipulate complex data. For example, pattern matching with ADTs often eliminates the need for lengthy if-else chains or switch statements. + +ADTs can accurately model data that has a limited set of possible states or variations. This is particularly useful for representing things like: + +- State machines: Each state can be a variant of an ADT. +- Abstract Syntax Trees (ASTs): Used in compilers and interpreters to represent the structure of code. +- Error Handling: An ADT can represent either a successful result or a specific error. +In essence, ADTs help by allowing us to model the application domain by defining custom data types that are tailor-made for a specific application domain and enforced by the type system. +They provide a powerful tool for tackling complexity in software engineering. + + +## Why Algebra? + +SUM + +PRODUCT + + + +## A Bit of History + +Before we get into recent changes in Java that help us exploit ADTs lets take a little detour into the history od ADTs + +ADTs are not a new idea. Java has been able to simulate them to some extent since enums were introduced in 1.1 (date?), but the ideas go back much further. + +In the 1960's a kind of ADT known as a tagged union which later became part of the C language. Here the tag is a value that indicates the variant of the enum stored in the union. +This allows a structure that is the Sum of different types. + +By the mid 1970s, In Standard ML, ADTs are defined using the datatype keyword. They allow you to create new types as a combination of constructors, each potentially holding values of other types. + +Essentially, datatype lets you build custom, compound types with named constructors and pattern matching lets you effectively use them. + + + + + From 8df51507a19f7ad2615f2cf008d95f3a333efbbc Mon Sep 17 00:00:00 2001 From: Magnus Smith Date: Fri, 27 Dec 2024 18:18:37 +0000 Subject: [PATCH 2/4] fleshing out --- ...25-01-01-algebraic-data-types-with-java.md | 161 +++++++++++++++++- 1 file changed, 155 insertions(+), 6 deletions(-) diff --git a/_posts/2025-01-01-algebraic-data-types-with-java.md b/_posts/2025-01-01-algebraic-data-types-with-java.md index 8df680902..403fabd88 100644 --- a/_posts/2025-01-01-algebraic-data-types-with-java.md +++ b/_posts/2025-01-01-algebraic-data-types-with-java.md @@ -28,25 +28,105 @@ ADTs can accurately model data that has a limited set of possible states or vari - State machines: Each state can be a variant of an ADT. - Abstract Syntax Trees (ASTs): Used in compilers and interpreters to represent the structure of code. - Error Handling: An ADT can represent either a successful result or a specific error. + In essence, ADTs help by allowing us to model the application domain by defining custom data types that are tailor-made for a specific application domain and enforced by the type system. They provide a powerful tool for tackling complexity in software engineering. ## Why Algebra? -SUM +In algebraic data types (ADTs), algebra refers to the operations used to combine types to create ADTs, and the relationships between those operations and the types: +- `Objects`: The types that make up the algebra +- `Operations`: The ways to combine types to create new types +- `Laws`: The relationships between the types and the operations + + +In ADTs the algebra consists of just two operators '+' and 'x' + +## Product + +- Represents combination. Types that are built with the 'x' operator and combine types with AND. A Product type bundles two or more arbitrary types together such that T=A and B and C. +- Defines values +- Logical AND operator +- The product is the cartesian product of all their components + +In code, we may see this as Tuples, POJOs or Records. +In Set theory this is the cartesian product + +``` (Bool * Bool) ``` + +``` 2 * 2 = 4 ``` + + +## Sum + +- Represents alternation. Types are built with the '+' operator and combine types with OR as in T = A or B or C. +- Defines variants +- Logical OR operator +- The sum is the (union) of the value sets of the alternatives + +Traditionally more common in functional languages like Haskel as a data type or in Scala as a sealed trait of case classes and Java as a sealed interface of records. +A very simple version in Java is an Enum type. Enums cannot have additional data associated with them once instantiated. + +An important property of ADTs is that they can be sealed or closed. That means that their definition contains all possible cases and no further cases can exist. This allows the compiler is able to exhaustively verify all the alternatives. + +Below Status is a disjunction, the relation of three distinct alternatives + +``` Under Review | Accepted | Rejected ``` + +``` Status + Boolean ``` + +``` 3 + 2 = 5 ``` + + + + +We can further combine Product and Sum as they follow the same distributive law of numerical algebra +``` +(a * b + a * c) <=> a * (b +c) +``` + + + +``` + +DnsRecord( + AValue(ttl, name, ipv4) + | AaaaValue(ttl, name, ipv6) + | CnameValue(ttl, name, alias) + | TxtValue(ttl, name, name) +) -PRODUCT +DnsRecord(ttl, name, + AValue(ipv4) + | AaaaValue(ipv6) + | CnameValue(alias) + | TxtValue(value) +) +``` -## A Bit of History +At the type level we can change ordering in using the same commutative law we would in algebra +``` +(a * b) <=> (b * a) +(a + b) <=> (b + a) +``` +similar with associativity +``` +(a + b) + c <=> a + (b + c) +(a * b) * c <=> a * (b * c) +``` -Before we get into recent changes in Java that help us exploit ADTs lets take a little detour into the history od ADTs -ADTs are not a new idea. Java has been able to simulate them to some extent since enums were introduced in 1.1 (date?), but the ideas go back much further. -In the 1960's a kind of ADT known as a tagged union which later became part of the C language. Here the tag is a value that indicates the variant of the enum stored in the union. +## A brief detour into the history of algebraic data types + +Before we get into how we can exploit ADTs in Java lets take a little detour into the history od ADTs + +ADTs are not a new idea. As we have seen Java has been able to simulate them to some extent since enums were introduced in 1.1 (date?), but the ideas actually go back much further. + +In the 1960's a kind of ADT known as a tagged union which later became part of the C language. The tagged union (also called a disjoint union) is a data structure used to hold a value that could take on several different, but fixed types. Only one of the types can be in use at any one time, and a tag field explicitly indicates which one is in use. Here the tag is a value that indicates the variant of the enum stored in the union. This allows a structure that is the Sum of different types. By the mid 1970s, In Standard ML, ADTs are defined using the datatype keyword. They allow you to create new types as a combination of constructors, each potentially holding values of other types. @@ -54,6 +134,75 @@ By the mid 1970s, In Standard ML, ADTs are defined using the datatype keyword. Essentially, datatype lets you build custom, compound types with named constructors and pattern matching lets you effectively use them. +## ADTs in Java + +Java's records and sealed interfaces provide an elegant mechanism for implementing ADTs. Records, introduced in Java 14, offer a concise syntax for defining immutable data classes. Providing `nominal` types and components with `human readable` names. + +Sealed interfaces, a feature introduced in Java 17, allows classes and interfaces to have more control over their permitted subtypes. We achieve precise data modelling as `sealed` hierarchies of immutable records. Restricting the possible implementations of a type, enables exhaustive pattern matching and makes invalid states unrepresentable. + +This is particularly useful for general domain modeling with type safety. + + + +### Why not just use enums? +~~~ java +enum Task = { + NotStarted, + Started, + Completed, + Cancelled; +} + +sealed interface TaskStatus{ + record NotStarted(...) implements TaskStatus + record Started(...) implements TaskStatus + record Completed(...) implements TaskStatus + record Cancelled(...) implements TaskStatus +} +~~~ +It is possible to associate data with an enum constant, such as the mass and radius of the planet + +~~~ java +enum Planet { + MERCURY (3.303e+23, 2.4397e6), + VENUS (4.869e+24, 6.0518e6), + EARTH (5.976e+24, 6.37814e6), +... +} +~~~ +sealed records work at a higher level. Where enums enumerate a fixed list of `instances` sealed records enumerate a fixed list of `kinds of instances` + +~~~ java + +sealed interface Celestial { + record Planet(String name, double mass, double radius) implements Celestial {} + record Star(String name, double mass, double temperature) implements Celestial {} + record Comet(String name, double period) implements Celestial {} +} + +~~~ + +Unlike enums records allow us to attach arbitrary attributes to each of the enumerated states. We are no longer restricted to fixed constants + +In the Celestial example we see a Sum of Products. This is a useful technique for modelling complex domains in a flexible but type-safe manner. +For thw Sums of Products to work we have to commit to the subtypes, which is a form of tight coupling. This works if we are sure the subtypes are unlikely to change. +We trade some future flexibility for an exhaustive list of subtypes that allows better reasoning about shapes especially when it comes to pattern matching + + +### Pattern Matching + +Pattern matching is a powerful feature that enhances Java's instanceof operator and switch expressions/statements. It allows developers to concisely and safely extract data from objects based on their structure. This capability streamlines type checking and casting, leading to more readable and less error-prone code. +The evolution of pattern matching in Java is noteworthy. Initially introduced in Java 16 to enhance the instanceof operator (JEP 394), it was later extended to switch expressions and statements in Java 17 (JEP 406)9. This expansion broadened the applicability of pattern matching, enabling more expressive and safer code constructs. + + + +A key advantage of pattern matching in switch statements is the increased safety it provides. By requiring that pattern switch statements cover all possible input values, it helps prevent common errors arising from incomplete case handling10. +The combination of ADTs and pattern matching is particularly powerful, as it allows for exhaustive and type-safe handling of different data variants within a sealed hierarchy. This synergy simplifies code and reduces the risk of runtime errors. + + + +References: +- [Where does the name "algebraic data type" come from?](https://blog.poisson.chat/posts/2024-07-26-adt-history.html) \ No newline at end of file From 969ef604155b8952200f8b92079028f3b1ac994d Mon Sep 17 00:00:00 2001 From: Magnus Smith Date: Mon, 30 Dec 2024 18:54:31 +0000 Subject: [PATCH 3/4] further update --- ...25-01-01-algebraic-data-types-with-java.md | 132 +++++++++++++++--- 1 file changed, 110 insertions(+), 22 deletions(-) diff --git a/_posts/2025-01-01-algebraic-data-types-with-java.md b/_posts/2025-01-01-algebraic-data-types-with-java.md index 403fabd88..4aedb62df 100644 --- a/_posts/2025-01-01-algebraic-data-types-with-java.md +++ b/_posts/2025-01-01-algebraic-data-types-with-java.md @@ -11,16 +11,21 @@ categories: ## Introduction + Building modern applications means managing complexity. For years, Java developers have grappled with modelling representations of complex data and relationships in a clean, maintainable way. More traditional Object-Oriented programming techniques provide many tools, but sometimes, it can feel that we are forcing data into structures that don't quite fit. + Algebraic Data Types (ADTs), a powerful concept from functional programming that's making waves in the Java world, offering an elegant solution in the programmers arsenal. + ## What are Algebraic Data Types (ADT)? -ADTs provide a way to define composite data types by combining other types in a structured and type-safe manner. They allow developers to model complex data structures using simpler building blocks, much like building with LEGOs. Think of them as custom, compound data types you design for your specific needs. +Algebraic data types (ADTs) are a way to structure data in functional programming languages. They define a mechanism to create composite data types by combining other simpler types. They allow developers to model complex data structures using simpler building blocks, much like building with LEGOs. Think of them as custom, compound data types you design for your specific needs. +ADTs are prevalent in functional programming due to their ability to enhance code safety, readability, and maintainability in a structured and type-safe manner. ## Why do we need them and what kind of problems to they help solve? +### Readable ADTs make code more readable by explicitly defining the structure and possible values of complex data. This makes it easier to understand and reason about the code, leading to improved maintainability. - +### Enforce Constraints ADTs use the type system to enforce constraints on the data. The compiler can detect errors at compile time, preventing runtime issues that might arise from invalid data structures or operations. - +### Remove boilerplate Compared to using classes or structs alone, ADTs can often reduce the amount of boilerplate code needed to define and manipulate complex data. For example, pattern matching with ADTs often eliminates the need for lengthy if-else chains or switch statements. ADTs can accurately model data that has a limited set of possible states or variations. This is particularly useful for representing things like: @@ -45,9 +50,12 @@ In ADTs the algebra consists of just two operators '+' and 'x' ## Product -- Represents combination. Types that are built with the 'x' operator and combine types with AND. A Product type bundles two or more arbitrary types together such that T=A and B and C. + +- These represent a combination of data, where a type holds values of several other types simultaneously. Think of it as an "AND" relationship. A Point might be a product type consisting of an x coordinate and a y coordinate. + - Defines values - Logical AND operator +- Product types bundle two or more arbitrary types together such that T=A and B and C. - The product is the cartesian product of all their components In code, we may see this as Tuples, POJOs or Records. @@ -59,8 +67,9 @@ In Set theory this is the cartesian product ## Sum +These represent a choice between different types, where a value can be one of several possible types, but only one at a time. It's an "or" relationship. A Shape might be a sum type, as it could be a Circle or a Square or a Triangle. -- Represents alternation. Types are built with the '+' operator and combine types with OR as in T = A or B or C. +- Sum types are built with the '+' operator and combine types with OR as in T = A or B or C. - Defines variants - Logical OR operator - The sum is the (union) of the value sets of the alternatives @@ -80,14 +89,12 @@ Below Status is a disjunction, the relation of three distinct alternatives - We can further combine Product and Sum as they follow the same distributive law of numerical algebra ``` (a * b + a * c) <=> a * (b +c) ``` - ``` DnsRecord( @@ -119,27 +126,58 @@ similar with associativity ``` +## A Historical Perspective +The concept of ADTs traces back to the early days of functional programming languages like ML and Hope in the 1970s. They were then popularized by languages like Haskell, which uses them as a fundamental building block. -## A brief detour into the history of algebraic data types -Before we get into how we can exploit ADTs in Java lets take a little detour into the history od ADTs +Let's take a quick tour of how ADTs (or their approximations) have been handled in different languages: -ADTs are not a new idea. As we have seen Java has been able to simulate them to some extent since enums were introduced in 1.1 (date?), but the ideas actually go back much further. + - `C`: C lacks built-in support for ADTs but can simulate by using structs for product types and unions (combined with an enum for type tracking) for a rudimentary form of sum types. + The tagged union (also called a disjoint union) is a data structure used to hold a value that can take on several different, but fixed types. Only one of the types can be in use at any one time, and a tag field explicitly indicates which one is in use. Here the tag is a value that indicates the variant of the enum stored in the union. However, unions are notoriously unsafe, as they don't enforce type checking at compile time. + ~~~ c + union vals { + char ch; + int nt; + }; + + struct tagUnion { + char tag; + vals val; + }; + ~~~ + - `Haskell`: Haskell a functional language elegantly expresses ADTs with its data keyword. Haskell's type system is specifically designed to support the creation and manipulation of ADTs. -In the 1960's a kind of ADT known as a tagged union which later became part of the C language. The tagged union (also called a disjoint union) is a data structure used to hold a value that could take on several different, but fixed types. Only one of the types can be in use at any one time, and a tag field explicitly indicates which one is in use. Here the tag is a value that indicates the variant of the enum stored in the union. -This allows a structure that is the Sum of different types. + ~~~ haskell + data Shape = Circle Float | Rectangle Float Float + ~~~ + This defines Shape as a sum type that can be either a Circle with a radius (Float) or a Rectangle with width and height (Float). + + - `Scala`: Scala uses case classes for product types and sealed traits with case classes/objects for sum types. This provides a robust and type-safe way to define ADTs. + ~~~ scala + sealed trait Shape + case class Circle(radius: Double) extends Shape + case class Rectangle(width: Double, height: Double) extends Shape + ~~~ + + - `Java` (Pre-Java 17): Historically, Java relied on class hierarchies and the Visitor pattern to mimic sum types. This approach was verbose, requiring a lot of boilerplate code and was prone to errors if not carefully implemented. Product types were typically represented by classes with member variables. -By the mid 1970s, In Standard ML, ADTs are defined using the datatype keyword. They allow you to create new types as a combination of constructors, each potentially holding values of other types. -Essentially, datatype lets you build custom, compound types with named constructors and pattern matching lets you effectively use them. ## ADTs in Java -Java's records and sealed interfaces provide an elegant mechanism for implementing ADTs. Records, introduced in Java 14, offer a concise syntax for defining immutable data classes. Providing `nominal` types and components with `human readable` names. +Java's records and sealed interfaces provide an elegant mechanism for implementing ADTs. + + +- Records, introduced in Java 14, offer a concise syntax for defining immutable data carriers. Providing `nominal` types and components with `human readable` names. + +- Sealed interfaces, a feature introduced in Java 17, allows classes and interfaces to have more control over their permitted subtypes. We achieve precise data modelling as `sealed` hierarchies of immutable records. This enables the compiler to know all possible subtypes at compile time, a crucial requirement for safe sum types. + -Sealed interfaces, a feature introduced in Java 17, allows classes and interfaces to have more control over their permitted subtypes. We achieve precise data modelling as `sealed` hierarchies of immutable records. Restricting the possible implementations of a type, enables exhaustive pattern matching and makes invalid states unrepresentable. +- Pattern matching is a powerful feature that enhances Java's instanceof operator and switch expressions/statements. It allows developers to concisely and safely extract data from objects based on their structure. This capability streamlines type checking and casting, leading to more readable and less error-prone code. + The evolution of pattern matching in Java is noteworthy. Initially introduced in Java 16 to enhance the instanceof operator (JEP 394), it was later extended to switch expressions and statements in Java 17 (JEP 406)9. This expansion broadened the applicability of pattern matching, enabling more expressive and safer code constructs. +Restricting the possible implementations of a type, enables exhaustive pattern matching and making invalid states unrepresentable. This is particularly useful for general domain modeling with type safety. @@ -185,19 +223,69 @@ sealed interface Celestial { Unlike enums records allow us to attach arbitrary attributes to each of the enumerated states. We are no longer restricted to fixed constants In the Celestial example we see a Sum of Products. This is a useful technique for modelling complex domains in a flexible but type-safe manner. -For thw Sums of Products to work we have to commit to the subtypes, which is a form of tight coupling. This works if we are sure the subtypes are unlikely to change. +For the Sums of Products to work we have to commit to the subtypes, which is a form of tight coupling. This works if we are sure the subtypes are unlikely to change. We trade some future flexibility for an exhaustive list of subtypes that allows better reasoning about shapes especially when it comes to pattern matching -### Pattern Matching -Pattern matching is a powerful feature that enhances Java's instanceof operator and switch expressions/statements. It allows developers to concisely and safely extract data from objects based on their structure. This capability streamlines type checking and casting, leading to more readable and less error-prone code. -The evolution of pattern matching in Java is noteworthy. Initially introduced in Java 16 to enhance the instanceof operator (JEP 394), it was later extended to switch expressions and statements in Java 17 (JEP 406)9. This expansion broadened the applicability of pattern matching, enabling more expressive and safer code constructs. +### Advantages over the Visitor Pattern +Traditionally, Java developers used the Visitor pattern to handle different types within a hierarchy. However, this approach has several drawbacks: +- Verbosity: The Visitor pattern requires a lot of boilerplate code, with separate visitor interfaces and classes for each operation. +- Openness to Extension: Adding a new type to the hierarchy requires modifying the visitor interface and all its implementations, violating the Open/Closed Principle. +- Lack of Exhaustiveness Checking: The compiler cannot guarantee that all possible types are handled, leading to potential runtime errors. -A key advantage of pattern matching in switch statements is the increased safety it provides. By requiring that pattern switch statements cover all possible input values, it helps prevent common errors arising from incomplete case handling10. -The combination of ADTs and pattern matching is particularly powerful, as it allows for exhaustive and type-safe handling of different data variants within a sealed hierarchy. This synergy simplifies code and reduces the risk of runtime errors. + +~~~ java + +public sealed interface Shape permits Circle, Rectangle { + double area(); + double perimeter(); +} + +public record Circle(double radius) implements Shape { + @Override + public double area() { + return Math.PI * radius * radius; + } + @Override + public double perimeter() { + return 2 * Math.PI * radius; + } +} + +public record Rectangle(double width, double height) implements Shape { + @Override + public double area() { + return width * height; + } + @Override + public double perimeter() { + return 2 * (width + height); + } +} + +public class Shapes { + public static void printShapeInfo(Shape shape) { + switch (shape) { + case Circle c -> System.out.println("Circle with radius: " + c.radius() + ", area: " + c.area() + ", perimeter: " + c.perimeter()); + case Rectangle r -> System.out.println("Rectangle with width: " + r.width() + ", height: " + r.height() + ", area: " + r.area() + ", perimeter: " + r.perimeter()); + } + } + public static Shape scaleShape(Shape shape, double scaleFactor) { + return switch (shape) { + case Circle c -> new Circle(c.radius() * scaleFactor); + case Rectangle r -> new Rectangle(r.width() * scaleFactor, r.height() * scaleFactor); + }; + } +} +~~~ +Explanation: +1. Shape is a sealed interface, allowing only Circle and Rectangle to implement it. +2. Circle and Rectangle are records, concisely defining the data they hold. +3. Shapes demonstrates how to use pattern matching with switch to handle different Shape types and perform operations like calculating area, perimeter or scaling. The compiler ensures that all possible Shape types are covered in the switch. + From 1add85d0c9da13022545e5240dcac8731ae42e18 Mon Sep 17 00:00:00 2001 From: Magnus Smith Date: Sun, 5 Jan 2025 17:31:30 +0000 Subject: [PATCH 4/4] Added comparison to visitor pattern Signed-off-by: Magnus Smith --- ...25-01-01-algebraic-data-types-with-java.md | 145 ++++++++++-------- 1 file changed, 79 insertions(+), 66 deletions(-) diff --git a/_posts/2025-01-01-algebraic-data-types-with-java.md b/_posts/2025-01-01-algebraic-data-types-with-java.md index 4aedb62df..0b1befc54 100644 --- a/_posts/2025-01-01-algebraic-data-types-with-java.md +++ b/_posts/2025-01-01-algebraic-data-types-with-java.md @@ -51,12 +51,13 @@ In ADTs the algebra consists of just two operators '+' and 'x' ## Product -- These represent a combination of data, where a type holds values of several other types simultaneously. Think of it as an "AND" relationship. A Point might be a product type consisting of an x coordinate and a y coordinate. - -- Defines values -- Logical AND operator -- Product types bundle two or more arbitrary types together such that T=A and B and C. -- The product is the cartesian product of all their components +These represent a combination of data, where a type holds values of several other types simultaneously. + + Think of it as an "AND" relationship. + + A Point might be a product type consisting of an x coordinate and a y coordinate. + + It defines values + + Logical AND operator + + Product types bundle two or more arbitrary types together such that T=A and B and C. + + The product is the cartesian product of all their components In code, we may see this as Tuples, POJOs or Records. In Set theory this is the cartesian product @@ -67,19 +68,20 @@ In Set theory this is the cartesian product ## Sum -These represent a choice between different types, where a value can be one of several possible types, but only one at a time. It's an "or" relationship. A Shape might be a sum type, as it could be a Circle or a Square or a Triangle. - -- Sum types are built with the '+' operator and combine types with OR as in T = A or B or C. -- Defines variants -- Logical OR operator -- The sum is the (union) of the value sets of the alternatives +These represent a choice between different types, where a value can be one of several possible types, but only one at a time. + + Think of it as an "or" relationship. + + A Shape might be a sum type, as it could be a Circle or a Square or a Triangle. + + Defines variants + + Logical OR operator + + Sum types are built with the '+' operator and combine types with OR as in T = A or B or C. + + The Sum is the (union) of the value sets of the alternatives Traditionally more common in functional languages like Haskel as a data type or in Scala as a sealed trait of case classes and Java as a sealed interface of records. A very simple version in Java is an Enum type. Enums cannot have additional data associated with them once instantiated. An important property of ADTs is that they can be sealed or closed. That means that their definition contains all possible cases and no further cases can exist. This allows the compiler is able to exhaustively verify all the alternatives. -Below Status is a disjunction, the relation of three distinct alternatives +We can define a Status as a disjunction, the relation of three distinct alternatives ``` Under Review | Accepted | Rejected ``` @@ -94,17 +96,17 @@ We can further combine Product and Sum as they follow the same distributive law (a * b + a * c) <=> a * (b +c) ``` - -``` - +We could define a DNS Record as a Sum Type +``` DnsRecord( AValue(ttl, name, ipv4) | AaaaValue(ttl, name, ipv6) | CnameValue(ttl, name, alias) | TxtValue(ttl, name, name) ) - - +``` +But we could also refactor to a Product of Produce and Sums +``` DnsRecord(ttl, name, AValue(ipv4) | AaaaValue(ipv6) @@ -126,6 +128,7 @@ similar with associativity ``` + ## A Historical Perspective The concept of ADTs traces back to the early days of functional programming languages like ML and Hope in the 1970s. They were then popularized by languages like Haskell, which uses them as a fundamental building block. @@ -230,66 +233,76 @@ We trade some future flexibility for an exhaustive list of subtypes that allows ### Advantages over the Visitor Pattern -Traditionally, Java developers used the Visitor pattern to handle different types within a hierarchy. However, this approach has several drawbacks: -- Verbosity: The Visitor pattern requires a lot of boilerplate code, with separate visitor interfaces and classes for each operation. -- Openness to Extension: Adding a new type to the hierarchy requires modifying the visitor interface and all its implementations, violating the Open/Closed Principle. -- Lack of Exhaustiveness Checking: The compiler cannot guarantee that all possible types are handled, leading to potential runtime errors. +Traditionally, Java developers used the Visitor pattern to handle different types within a hierarchy. However, this approach has several drawbacks that we will see when we compare using a Sum type with pattern matching to the same effect using the Visitor pattern: +### Using Visitor Pattern + -~~~ java - -public sealed interface Shape permits Circle, Rectangle { - double area(); - double perimeter(); -} -public record Circle(double radius) implements Shape { - @Override - public double area() { - return Math.PI * radius * radius; - } - @Override - public double perimeter() { - return 2 * Math.PI * radius; - } -} +```jshelllanguage +Shapes: +Circle with radius: 5.00, area: 78.54, perimeter: 31.42 +Triangle with sides: 3.00, 3.00, 3.00, area: 3.90, perimeter: 9.00 +Rectangle with width: 3.00 , height: 5.00, area: 15.00, perimeter: 16.00 +Pentagon with side: 5.60, area: 53.95, perimeter: 28.00 -public record Rectangle(double width, double height) implements Shape { - @Override - public double area() { - return width * height; - } - @Override - public double perimeter() { - return 2 * (width + height); - } -} +Shapes scaled by 2: +Circle with radius: 10.00, area: 314.16, perimeter: 62.83 +Triangle with sides: 6.00, 6.00, 6.00, area: 15.59, perimeter: 18.00 +Rectangle with width: 6.00 , height: 10.00, area: 60.00, perimeter: 32.00 +Pentagon with side: 11.20, area: 215.82, perimeter: 56.00 +``` -public class Shapes { - public static void printShapeInfo(Shape shape) { - switch (shape) { - case Circle c -> System.out.println("Circle with radius: " + c.radius() + ", area: " + c.area() + ", perimeter: " + c.perimeter()); - case Rectangle r -> System.out.println("Rectangle with width: " + r.width() + ", height: " + r.height() + ", area: " + r.area() + ", perimeter: " + r.perimeter()); - } - } - public static Shape scaleShape(Shape shape, double scaleFactor) { - return switch (shape) { - case Circle c -> new Circle(c.radius() * scaleFactor); - case Rectangle r -> new Rectangle(r.width() * scaleFactor, r.height() * scaleFactor); - }; - } -} -~~~ Explanation: -1. Shape is a sealed interface, allowing only Circle and Rectangle to implement it. -2. Circle and Rectangle are records, concisely defining the data they hold. -3. Shapes demonstrates how to use pattern matching with switch to handle different Shape types and perform operations like calculating area, perimeter or scaling. The compiler ensures that all possible Shape types are covered in the switch. +- ShapeVisitor Interface: + + Defines the visit methods for each shape type. + + The generic type T allows visitors to return different types of results. + +- accept Method in Shape: + + Each shape class implements the accept method, which takes a ShapeVisitor and calls the appropriate visit method on the visitor. + +- Concrete Visitors: + + AreaCalculator: Calculates the area of a shape. + + PerimeterCalculator: Calculates the perimeter of a shape. + + InfoVisitor: Generates a string with information about the shape (including area and perimeter). + + ScaleVisitor: Scales a shape by a given factor. + + +### Using Sum Type with pattern matching + + + + +Output: + +```jshelllanguage +Shapes: [Circle[radius=5.0], Triangle[side1=3.0, side2=3.0, side3=3.0], Rectangle[width=3.0, height=5.0], Pentagon[side=5.6]] +Circle with radius: 5.00, area: 78.54, perimeter: 31.42 +Triangle with sides: 3.00, 3.00, 3.00, area: 3.90, perimeter: 9.00 +Rectangle with width: 3.00 , height: 5.00, area: 15.00, perimeter: 16.00 +Pentagon with side: 5.60, area: 53.95, perimeter: 28.00 + +Shapes scaled by 2: [Circle[radius=5.0], Triangle[side1=3.0, side2=3.0, side3=3.0], Rectangle[width=3.0, height=5.0], Pentagon[side=5.6]] +Circle with radius: 10.00, area: 314.16, perimeter: 62.83 +Triangle with sides: 6.00, 6.00, 6.00, area: 15.59, perimeter: 18.00 +Rectangle with width: 6.00 , height: 10.00, area: 60.00, perimeter: 32.00 +Pentagon with side: 11.20, area: 215.82, perimeter: 56.00 +``` + +Explanation: +1. Shape is a sealed interface, allowing only permitting Circle, Rectangle, Triangle and Pentagon to implement it. +2. Circle and Rectangle Triangle and Pentagon are records, concisely defining the data they hold. +3. Shapes demonstrates using pattern matching with switch to handle different Shape types and perform operations like calculating area, perimeter or scaling. The compiler ensures that all possible Shape types are covered in the switch. +### Compare with the Visitor Pattern approach +- `Verbosity`: The Visitor pattern requires a lot of boilerplate code, with separate visitor interfaces and classes for each operation. +- `Openness to Extension`: Adding a new type to the hierarchy requires modifying the visitor interface and all its implementations, violating the Open/Closed Principle. +- `Lack of Exhaustiveness Checking`: The compiler cannot guarantee that all possible types are handled, leading to potential runtime errors. References: