Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/imrafaelmerino/jio
Jio is a powerful Java library designed to simplify and enhance input/output operations by leveraging the power of expressions and functions
https://github.com/imrafaelmerino/jio
functional-programming java property-based-testing virtual-threads
Last synced: 28 days ago
JSON representation
Jio is a powerful Java library designed to simplify and enhance input/output operations by leveraging the power of expressions and functions
- Host: GitHub
- URL: https://github.com/imrafaelmerino/jio
- Owner: imrafaelmerino
- License: apache-2.0
- Created: 2024-08-27T10:07:30.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2024-11-23T21:51:18.000Z (2 months ago)
- Last Synced: 2024-11-23T22:26:23.614Z (2 months ago)
- Topics: functional-programming, java, property-based-testing, virtual-threads
- Language: Java
- Homepage:
- Size: 5.65 MB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
Awesome Lists containing this project
README
- [Code wins arguments](#cwa)
- [Introduction](#Introduction)
- [jio-exp](#jio-exp)
- [Creating effects](#creating-effects)
- [Lambdas](#lambdas)
- [Operations with effects](#operations-with-effects)
- [Expressions](#expressions)
- [Clocks](#Clocks)
- [Debugging and JFR integration](#Debugging-and-JFR-integration)
- [Installation](#exp-installation)
- [jio-http](#jio-http)
- [HTTP server](#httpserver)
- [HTTP client](#httpclient)
- [OAUTH HTTP client](#oauth)
- [Installation](#http-installation)
- [jio-test](#jio-test)
- [Junit integration](#junit)
- [Stubs](#stubs)
- [IO stubs](#iostubs)
- [Clock stubs](#clockstubs)
- [Http Server Stubs](#httpserverstubs)
- [Property based testing](#pbt)
- [Installation](#test-installation)
- [jio-mongodb](#jio-mongodb)
- [MongoLambda](#monglambda)
- [API](#jio-mongodb-gs)
- [Find Operations](#find-operations)
- [Insert Operations](#insert-operations)
- [Delete Operations](#delete-operations)
- [Update and Replace Operations](#update-and-replace-operations)
- [Count](#count)
- [FindAndXXX operations](#findoneandxxx-operations)
- [Aggregate](#aggregate)
- [Watcher](#watcher)
- [Specifying an Executor](#mongo-executors)
- [Configuring options](#mongo-options)
- [Transactions](#transactions)
- [Common exceptions](#common-exceptions)
- [Debugging and JFR integration](#mongo-Debugging-and-JFR-integration)
- [Installation](#mongo-installation)
- [jio-cli](#jio-cli)
- [jio-jdbc](#jio-jdbc)I think the age-old "Hello world" example has outlived its usefulness. While it once served as a
foundational teaching tool, its simplicity no longer suffices in today's world. In the current
landscape, where real-world scenarios are markedly more intricate, I present a "Hello world" example
that truly mirrors the complexity of modern development.### Signup Service specification
Let's jump into the implementation of a signup service with the following requirements:
1. The signup service takes a JSON input containing at least two fields: email and address, both
expected as strings. The service's first step is to validate and standardize the address using
the Google Geocode API. The results obtained from Google are then presented to the frontend for
user selection or rejection.
2. In addition to address validation, the service stores the client's information in a MongoDB
database. The MongoDB identifier returned becomes the client identifier, which must be sent back
to the frontend. If the client is successfully saved in the database and the user doesn't exist
in an LDAP system, two additional actions occur:- The user is sent to the LDAP service.
- If the previous operation succeeds, an activation email is sent to the user.3. The signup service also provides information about the total number of existing clients in the
MongoDB database. This information can be utilized by the frontend to display a welcoming message
to the user, such as "You're the user number 3000!" If an error occurs the service returns -1,
and the frontend will not display the message.
4. Crucially, the signup service is designed to perform all these operations in parallel. This
includes the request to Google for address validation and the MongoDB operations, which encompass
both data persistence and counting.### Response Structure
The response from the signup service follows this structure:
```code
{
"number_users": integer,
"id": string,
"addresses": array
}
```### Signup Service implementation
The `SignupService` orchestrates all the operations with elegance and efficiency. This service is
constructed with a set of [lambdas](#lambdas), where a lambda is essentially a function that takes
an input and produces an output. Unlike traditional functions, lambdas don't throw exceptions;
instead, they gracefully return exceptions as regular values.```java
import jio.*;
import jio.time.Clock;
import jsonvalues.*;import java.time.Instant;
import static java.util.Objects.requireNonNull;
public class SignupService implements Lambda {
Lambda persistLDAP;
Lambda normalizeAddress;
IO countUsers;
Lambda persistMongo;
Lambda sendEmail;
Lambda existsInLDAP;//constructor
@Override
public IO apply(JsObj user) {String email = user.getStr("email");
String address = user.getStr("address");
String context = "signup";Lambda LDAPFlow =
id -> IfElseExp.predicate(existsInLDAP.apply(email))
.consequence(() -> IO.succeed(id))
.alternative(() -> PairExp.seq(persistLDAP.apply(user),
sendEmail.apply(user)
)
.debugEach(context)
.map(n -> id)
)
.debugEach(context);return JsObjExp.par("number_users",
countUsers.recover(exc -> -1)
.map(JsInt::of),"id",
persistMongo.then(LDAPFlow)
.apply(user)
.map(JsStr::of),"addresses",
normalizeAddress.apply(address)
)
.debugEach(context);
}
}```
Noteworthy points:
- **JsObjExp**: The `JsObjExp` expression is highly expressive. It allows us to define the structure
of the resulting JSON object in a clear and declarative manner. In our code, we use it to
construct a JSON object with multiple key-value pairs, each representing a specific piece of
information (`"number_users"`, `"id"`, `"addresses"`, `"timestamp"`, etc.). This approach
simplifies the creation of complex JSON structures and enhances code readability.- Error handling is handled gracefully with the `recover` functions, providing alternative values
(e.g., -1 for `countUsers`) in case of errors.- **IfElseExp**: The `IfElseExp` expression is a clear and concise way to handle conditional logic.
It enables us to specify the consequence and alternative branches based on a predicate
(`existsInLDAP.apply(email)` in this case). This expressiveness makes it evident that if the user
exists in LDAP, we succeed with an ID, otherwise, we perform a sequence of operations using
`PairExp`. It enhances the readability of the code, making it easy to understand the branching
logic.- **PairExp**: The `PairExp` expression streamlines the execution of two effects, either
sequentially or in parallel, and then combines their results into a pair. In this scenario, we
utilize `PairExp.seq` to execute the `persistLDAP` and `sendEmail` operations sequentially.
However, it's essential to emphasize that in this particular example, our primary concern is the
successful completion of both operations. Therefore, in the absence of any failures, the result
will be a pair containing two `null` values: (null, null), as both operations return `Void`.- **debugEach**: Debugging plays a pivotal role in software development, and real-world applications
often handle a multitude of messages from various users and requests. When issues arise,
identifying which log events are pertinent to the problem can be challenging, particularly in
high-traffic scenarios. JIO streamlines the debugging process and enhances contextual logging
through its `debug` and `debugEach` methods.- **JFR (Java Flight Recorder)**: JIO leverages JFR for logging and debugging purposes. This choice
offers several advantages. First, it's Java-native, which means it seamlessly integrates with the
Java ecosystem, ensuring compatibility and performance. Second, it avoids the complexities and
potential conflicts associated with using external logging libraries, of which there are many in
the Java landscape. By relying on JFR, we maintain a lightweight and efficient approach to logging
that is both reliable and highly effective.- Last but not least, the backbone of JIO is the `IO` class that we'll explore in detail in the next
section.### Testing the Signup Service with JIO
JIO offers an elegant and efficient approach to testing. It eliminates the need for external
libraries like Mockito, making your testing experience smoother and more expressive. Since Lambdas
are just functions, you can implement them in your test class, directly. This approach enables you
to tailor the behavior of each lambda to your specific test scenario, making your tests highly
adaptable and expressive:```java
public class SignupTests {
@RegisterExtension
static Debugger debugger = Debugger.of(Duration.ofSeconds(2));@Test
public void test() {Lambda persistLDAP = _ -> IO.NULL();
Lambda normalizeAddress =
_ -> IO.succeed(JsArray.of("address1",
"address2"));IO countUsers =
IO.lazy(() -> ThreadLocalRandom.current()
.nextInt(0,
10));Lambda persistMongo = _ -> IO.succeed("id");
Lambda sendEmail = _ -> IO.NULL();
Lambda existsInLDAP = _ -> IO.FALSE;
JsObj user = JsObj.of("email",
JsStr.of("[email protected]"),
"address",
JsStr.of("Elm's Street")
);JsObj resp = new SignupService(persistLDAP,
normalizeAddress,
countUsers,
persistMongo,
sendEmail,
existsInLDAP
)
.apply(user) //returns an IO effect, nothing is computed
.compute() //computes the effect a return a Result (either Success of Failure)
.getOutput(); //get the sucessful output or throws RuntimeExceptionAssertions.assertTrue(resp.containsKey("number_users"));
}
}
```
### Debugging with the Debugger Extension
When it comes to debugging your code during testing, having access to detailed information is
invaluable. JIO's Debugger extension simplifies this process by creating an event stream for a
specified duration and printing all the events sent to the Java Flight Recorder (JFR) system during
that period.Here's a breakdown of how it works:
1. **Debugger Extension Registration**: In your test class, you register the Debugger JUnit
extension using the `@RegisterExtension` annotation. You specify the duration for which the
debugger captures events.2. **Using `debug` and `debugEach`**: Within your code, you utilize the `debug` and `debugEach`
methods provided by JIO. These methods allow you to send events to the JFR system after a value
or expression is evaluated.3. **Event Printing**: During the execution of the test for the specified duration, the Debugger
extension prints out all the events that were sent to the JFR system. These events include
information about the expressions being evaluated, their results, execution durations, contextual
data, and more.4. **Stream Ordering**: Importantly, the event stream is ordered. Events are printed in the order in
which they occurred, providing a clear chronological view of your code's execution.5. **Pinpointing Bugs and Issues**: With the event stream and detailed logs in hand, you can easily
pinpoint any bugs, unexpected behavior, or performance bottlenecks.In summary, the Debugger extension in JIO transforms the testing and debugging process into a
streamlined and informative experience with minimal effort from developers. It empowers developers
to gain deep insights into their code's behavior without relying on external logging libraries or
complex setups.Find below all the events that are printed out during the execution of the previous JUnit test.
```text
Started JFR stream for 2,000 sg in SignupTests
------ eval-exp --------
| Expression: count_number_users
| Result: SUCCESS
| Duration: 69,833 µs
| Output: 3
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.920563792+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar[number_users]
| Result: SUCCESS
| Duration: 1,418 ms
| Output: 3
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.91973025+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar[addresses]
| Result: SUCCESS
| Duration: 4,583 µs
| Output: ["address1","address2"]
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.921166292+01:00
------------------------------- eval-exp --------
| Expression: IfElseExp-predicate
| Result: SUCCESS
| Duration: 5,958 µs
| Output: false
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924032792+01:00
------------------------------- eval-exp --------
| Expression: PairExpSeq[1]
| Result: SUCCESS
| Duration: 4,709 µs
| Output: null
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924848208+01:00
------------------------------- eval-exp --------
| Expression: PairExpSeq[2]
| Result: SUCCESS
| Duration: 4,291 µs
| Output: null
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924969417+01:00
------------------------------- eval-exp --------
| Expression: PairExpSeq
| Result: SUCCESS
| Duration: 284,875 µs
| Output: (null, null)
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924846917+01:00
------------------------------- eval-exp --------
| Expression: IfElseExp-alternative
| Result: SUCCESS
| Duration: 2,744 ms
| Output: id
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924842+01:00
------------------------------- eval-exp --------
| Expression: IfElseExp
| Result: SUCCESS
| Duration: 3,568 ms
| Output: id
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924030958+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar[id]
| Result: SUCCESS
| Duration: 4,058 ms
| Output: id
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.923546958+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar[timestamp]
| Result: SUCCESS
| Duration: 219,208 µs
| Output: 2024-02-13T09:08:09.927Z
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.927616125+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar
| Result: SUCCESS
| Duration: 8,925 ms
| Output: {"addresses":["address1","address2"],"number_users":3,"timestamp":"2024-02-13T09:08:09.927Z","id":"id"}
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.919238167+01:00
-------------------------```
The displayed events are self-explanatory. If you're wondering whether this is the actual format of
events, the answer is yes. When testing, it's preferable to opt for a format that's easy to read and
comprehend, rather than cramming all the information into a single line.In summary, these traces are like breadcrumbs that guide you through your code, making testing and
debugging more efficient and effective. They enable you to pinpoint issues, optimize performance,
and gain a deeper understanding of how your code behaves during testing.In the previous example, you may have observed that all evaluations were performed by the main
thread. This is because the IO effects returned by the lambdas were essentially constants, and no
specific `Executor` was defined. Even if an `Executor` were specified, there are cases where the
CompletableFuture framework, heavily relied upon by JIO, may choose not to switch contexts between
threads if it deems it unnecessary.However, you can introduce random delays and leverage virtual threads to create a more realistic
example. To achieve this, more complex stubs are used from the `jio-test` library through the
`StubBuilder` class. These stubs allow you to specify generators for their creation, ensuring
different values are returned every time. Here's how you can utilize them:```java
@Test
public void test() {Gen delayGen = IntGen.arbitrary(0,
200)
.map(Duration::ofMillis);IO countUsers =
StubBuilder.ofSucGen(IntGen.arbitrary(0,
100000))
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();Lambda persistMongo =
_ -> StubBuilder.ofSucGen(StrGen.alphabetic(20,
20))
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();Lambda sendEmail =
_ -> StubBuilder.ofSucGen(Gen.cons(null))
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();Lambda existsInLDAP =
_ -> StubBuilder.ofSucGen(BoolGen.arbitrary())
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();Lambda persistLDAP =
_ -> StubBuilder.ofSucGen(Gen.cons(null))
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();Lambda normalizeAddresses =
_ -> StubBuilder.ofSucGen(JsArrayGen.ofN(JsStrGen.alphabetic(),
3))
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();
}
```These `StubBuilder` instances are essentially builders that create IO stubs. They allow you to
introduce variability and randomness into your tests, making them more realistic and ensuring your
code can handle different scenarios effectively. I recommend you take a look at
[jio-test](#jio-test) and [property-based-testing](#pbt).Using these stubs, the following events were printed out:
```text
Started JFR stream for 2,000 sg in SignupTests------ eval-exp --------
| Expression: JsObjExpPar[timestamp]
| Result: SUCCESS
| Duration: 293,209 µs
| Output: 2024-02-13T09:18:21.071Z
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:18:21.071499125+01:00
------------------------------- eval-exp --------
| Expression: count_number_users
| Result: SUCCESS
| Duration: 65,372 ms
| Output: 32634
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.066073417+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar[number_users]
| Result: SUCCESS
| Duration: 66,663 ms
| Output: 32634
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.065248375+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar[addresses]
| Result: SUCCESS
| Duration: 116,944 ms
| Output: ["m","n","g"]
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.071124667+01:00
------------------------------- eval-exp --------
| Expression: IfElseExp-predicate
| Result: SUCCESS
| Duration: 37,233 ms
| Output: true
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.218791959+01:00
------------------------------- eval-exp --------
| Expression: IfElseExp-consequence
| Result: SUCCESS
| Duration: 9,667 µs
| Output: QREuMrvmtunCvhbZxykT
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.256122125+01:00
------------------------------- eval-exp --------
| Expression: IfElseExp
| Result: SUCCESS
| Duration: 37,386 ms
| Output: QREuMrvmtunCvhbZxykT
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.218788042+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar[id]
| Result: SUCCESS
| Duration: 184,728 ms
| Output: QREuMrvmtunCvhbZxykT
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.071473417+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar
| Result: SUCCESS
| Duration: 191,692 ms
| Output: {"addresses":["m","n","g"],"number_users":32634,"timestamp":"2024-02-13T09:18:21.071Z","id":"QREuMrvmtunCvhbZxykT"}
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.064957792+01:00
-------------------------```
To enhance the resilience of our code, let's introduce some retry logic for the `countUsers`
supplier. We want to allow up to three retries:```code
// let's add up to three retries
countUsers.debug(EventBuilder.of("count_users", context))
.retry(RetryPolicies.limitRetries(3))
.recover(_ -> -1)
```In this code:
- The `countUsers` supplier is executed, and for each execution, the `debug` method creates an
event. The `EventBuilder` allows you to specify the name of the expression being evaluated
("count_users") and the context. This helps customize the events sent to the JFR system.- The `retry` method is used to introduce retry logic. In case of failure, `countUser` will be
retried up to three times.- The `recover` method specifies what value to return in case of a failure.
And to test it, let's change the stub for the `countUser` supplier:
```java
//let's change the delay of every stub to 1 sec, for the sake of clarity
Gen delayGen = Gen.cons(1)
.map(Duration::ofSeconds);IO countUsers =
StubBuilder.ofGen(Gen.seq(n -> n <= 4 ?
IO.fail(new RuntimeException(n + "")) :
IO.succeed(n)
)
)
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();```
In this code:
- The generator `delayGen` provides a constant delay of 1 second.
- The `countUsers` effect is defined to use the `StubBuilder` with a sequence generator (`Gen.seq`)
that allows you to choose different values for each call. In this case, the first four calls
trigger a failure, which is treated as a value that can be returned.This setup allows you to test and observe the retry logic in action:
```text
Started JFR stream for 10,000 sg in SignupTests------ eval-exp --------
| Expression: JsObjExpPar[timestamp]
| Result: SUCCESS
| Duration: 281,583 µs
| Output: 2024-02-13T09:30:42.681Z
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:30:42.681326792+01:00
------------------------------- eval-exp --------
| Expression: count_number_users
| Result: FAILURE
| Duration: 1,010 sg
| Output: java.lang.RuntimeException: 1
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:42.678849959+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar[addresses]
| Result: SUCCESS
| Duration: 1,007 sg
| Output: ["l","e","B"]
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:42.681127792+01:00
------------------------------- eval-exp --------
| Expression: IfElseExp-predicate
| Result: SUCCESS
| Duration: 1,006 sg
| Output: false
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:43.6904075+01:00
------------------------------- eval-exp --------
| Expression: count_number_users
| Result: FAILURE
| Duration: 1,006 sg
| Output: java.lang.RuntimeException: 2
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:43.690528334+01:00
------------------------------- eval-exp --------
| Expression: count_number_users
| Result: FAILURE
| Duration: 1,004 sg
| Output: java.lang.RuntimeException: 3
| Context: signup
| Thread: not recorded
| Event Start Time: 2024-02-13T10:30:44.696579667+01:00
------------------------------- eval-exp --------
| Expression: PairExpSeq[1]
| Result: SUCCESS
| Duration: 1,001 sg
| Output: null
| Context: signup
| Thread: not recorded
| Event Start Time: 2024-02-13T10:30:44.702844292+01:00
------------------------------- eval-exp --------
| Expression: PairExpSeq[2]
| Result: SUCCESS
| Duration: 1,003 sg
| Output: null
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:45.704042667+01:00
------------------------------- eval-exp --------
| Expression: count_number_users
| Result: FAILURE
| Duration: 1,006 sg
| Output: java.lang.RuntimeException: 4
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:45.700588667+01:00
------------------------------- eval-exp --------
| Expression: PairExpSeq
| Result: SUCCESS
| Duration: 2,004 sg
| Output: (null, null)
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:44.702836584+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar[number_users]
| Result: SUCCESS
| Duration: 4,030 sg
| Output: -1
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:42.67800425+01:00
------------------------------- eval-exp --------
| Expression: IfElseExp-alternative
| Result: SUCCESS
| Duration: 2,015 sg
| Output: adnhvqDPCgmEINgqiteV
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:44.702804125+01:00
------------------------------- eval-exp --------
| Expression: IfElseExp
| Result: SUCCESS
| Duration: 3,028 sg
| Output: adnhvqDPCgmEINgqiteV
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:43.690404584+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar[id]
| Result: SUCCESS
| Duration: 4,037 sg
| Output: adnhvqDPCgmEINgqiteV
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:42.681302584+01:00
------------------------------- eval-exp --------
| Expression: JsObjExpPar
| Result: SUCCESS
| Duration: 4,042 sg
| Output: {"addresses":["l","e","B"],"number_users":-1,"timestamp":"2024-02-13T09:30:42.681Z","id":"adnhvqDPCgmEINgqiteV"}
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:42.676874584+01:00
-------------------------```
Key points:
1. After the first failure and three retries, the value -1 from the `recover` function is returned
2. The `retry` method can accept a predicate, allowing you to specify which errors should trigger a
retry. This fine-grained control is valuable for handling specific error scenarios.3. Retry policies in JIO are composable, making it easy to build complex retry strategies. For
example, you can create a policy like this:```code
RetryPolicies.constantDelay(Duration.ofMillis(50))
.limitRetriesByCumulativeDelay(Duration.ofMillis(300))
```This policy specifies a constant delay of 50 milliseconds between retries and limits retries by a
cumulative delay of 300 milliseconds.4. JIO excels at scalability. Even when dealing with complex logic, it maintains simplicity in the
expressions you write, avoiding the complexities of callback hell or other frameworks.5. JIO offers a high signal-to-noise ratio. It reduces verbosity, allowing you to express complex
operations succinctly and clearly.Functional Programming is all about working with pure functions and values. That's all. **However,
where FP especially shines is dealing with effects**.But what is an effect?
First take a look at the following piece of code:
```code
int a = sum(1,2) + 3;
int b = sum(1,2) + 1;
```
As far as the function `sum` is **pure**, you can refactor the previous piece of code and call the
function just once:```code
int c = sum(1,2);
int a = c + 3;
int b = c + 1;
```
Both programs are equivalents and wherever you see `sum(1,2)` you can replace it by `c` without
changing the meaning of the program at all.An effect, on the other hand, is something you can't call more than once unless you intended to:
```code
Instant a = Instant.now().plus(Period.ofDays(1));
Instant b = Instant.now().plus(Period.ofDays(2));
```
Because _now()_ returns a different value each time it's called and therefore is not a pure
function, the following refactoring would change completely the meaning of the program (and still
your favourite IDE suggests you to do it at times!):```code
Instant now = Instant.now();
Instant a = now.plus(Period.ofDays(1));
Instant b = now.plus(Period.ofDays(2));
```
Here's when laziness comes into play. Since Java 8, we have suppliers. They are indispensable to do
FP in Java. The following piece of code is equivalent to the original one without changing the
meaning of the program:```code
Supplier now = () -> Instant.now();
Instant a = now.get().plus(Period.ofDays(1));
Instant b = now.get().plus(Period.ofDays(2));
```
This property that allows you to factor out expressions is called **referential transparency**, and
it's fundamental to create and compose expressions.What can you expect from JIO:
- Simple and powerful API
- Errors are first-class citizens
- Simple and powerful testing tools ([jio-test](#jio-test))
- Easy to extend and get benefit from all the above. Examples are [jio-http](#jio-http),
[jio-mongodb](#jio-mongodb) and [jio-jdbc](#jio-jdbc). And you can create your own integrations!
- I don't fall into the logging-library war. This is something that sucks in Java. I just use Java
Flight Recording!
- Almost zero dependencies (just plain Java!)
- JIO doesn't transliterate any functional API from other languages. This way, any standard Java
programmer will find JIO quite easy and familiar.---
[![Maven](https://img.shields.io/maven-central/v/com.github.imrafaelmerino/jio-exp/3.0.0-RC2)](https://search.maven.org/artifact/com.github.imrafaelmerino/jio-exp/3.0.0-RC2/jar
"jio-ex")Let's model a functional effect in Java!
```code
import java.util.function.Supplier;
import java.util.concurrent.CompletableFuture;public sealed abstract class IO implements Callable> permits Exp, Val {
@Override
Result call() throws Exception;Result compute();
//other methods
}
public sealed interface Result permits Result.Success, Result.Failure {
//methods
}
```
Key Concepts:
- **`IO` Definition**: The `IO` class is a fundamental component of JIO. It's an abstract class
designed to represent functional effects or computations.- **Lazy Computation**: `IO` is a lazy computation and is realized as a `Callable`. In essence, it
merely outlines a computation without immediate execution, awaiting the explicit invocation of
methods like `call()` or `compute()`. It's important to note that both operations are blocking,
which isn't an issue when employing virtual threads.- **Handling Errors**: A critical aspect of JIO is that `Result` can represent both successful and
failed computations. This approach ensures that errors are treated as first-class citizens,
avoiding the need to throw exceptions whenever an error occurs.- According to Erik Meyer, as mentioned in [this
video](https://www.youtube.com/watch?v=z0N1aZ6SnBk), honesty is at the core of functional
programming. I find this perspective to be quite insightful. Latency and failures hold such a
significance that they should be explicitly denoted in a function or method's signature with the
`IO` type. Without this distinction, it becomes impossible to differentiate functions that are
free from failure and latency from those that aren't, making our code difficult to reason about.- The `call` and `compute` methods exhibit significant similarity. The `call` method is essential
due to IO's implementation of `Callable`, facilitating seamless integration with the structural
concurrency API:```code
IO first = ???;
IO second = ???;try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// Since IO is a callable, we can pass it in the `fork` method
Subtask> first = scope.fork(first);
Subtask> second = scope.fork(second);
....
}```
In most cases, apart from the one described above, it is advisable to use the `compute` method. This
method does not throw a checked exception and returns a `Result` object encapsulating the outcome of
the computation (either success or failure)."- According to its `permits` definition, IO has two distinct subclasses: `Val` and `Exp`
- **`Val`**: This subclass denotes an effect that is computed and returned as a result.
- **`Exp`**: This subclass signifies an expression composed of multiple effects, which will be
computed and combined into the final result through an expression. Some examples of expressions
that we will see later are `PairExp`,`JsObjExp`,`CondExp`, `ListExp`, `SwitchExp` etc.---
Now that we got the ball rolling, let's learn how to create IO effects.
**From a constant or a computed value**
```code
IO effect = IO.succeed("hi");
JsObj get(int id) { ??? }
IO effect = IO.succeed(get(1)); //get(1) is invoked before constructing the effect```
In both of the above examples, the effect will always compute the same value: either "hi" or the
result of calling `get(1)`. There is no lazynes here, a value is computed right away and used to
create the IO effect**From an exception**
```code
IO effect = IO.fail(new RuntimeException("something went wrong :("));
```
Like with `succeed`, the effect will always produce the same result, in this case it fails always
with the same exception, which is instantiated before creating the effect. Do notice that no
exception is thrown!**From a lazy computation or a supplier**
This is a very common case, and you will use it all the time to create effects.
```code
Suplier computation = ???;
IO effect = IO.lazy(computation);```
In this example and effect is created but not like in `succeed` and `fail`, **nothing is evaluated**
since a `Supplier` is lazy. It's very important to notice the difference. On the other hand, each
time the `get` or `result` methods are invoked a potentially new value can be returned.**From a callable**
We can think of a `Callable` as lazy computations like `Suppliers`, but with the important
difference that they can fail.```code
Callable callable = ???;
IO effect = IO.task(callable);
```
**From a Future**:
```code
Future get(String id){ ??? }
IO effect = IO.effect( () -> get(1) );
```
Like with `lazy` and `task`, the previous example doesn't evaluate anything to create the effect
since the effect method takes in a `Supplier`.**From auto-closable resources**
The `resource` method is used to create an IO effect that manages a resource implementing the
`AutoCloseable` interface. It takes a `Callable` that supplies the closable resource and a mapping
function to transform the resource into a value. This method ensures proper resource management,
including automatic closing of the resource after the map function is executed, to prevent memory
leaks. It returns an IO effect encapsulating both the resource handling and mapping.```code
static IO resource(Callable resource,
Lambda map
);
```and an example:
```code
Callable callable = () -> new FileInputStream("example.txt");
// Create an IO effect using the resource method
IO resultEffect =
IO.resource(callable,
inputStream -> {
try {
// Read the content of the file and return it as a String
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
return IO.succeed(new String(bytes,
StandardCharsets.UTF_8));
}
catch (IOException e) {
return IO.fail(e);
}
}
);
```**Other regular IO effects**
```code
IO t = IO.TRUE
IO f = IO.FALSE;
IO s = IO.NULL();
IO s = IO.NULL();```
The `NULL` method creates an IO effect that always produces a result of null. It is a generic method
that captures the type of the caller, allowing you to create null effects with different result
types. This can be useful when you need to type of the caller, allowing you to create null effects
with different result types. This can be useful when you need to represent a null result in your
functional code. These constants, `TRUE` and `FALSE`, represent IO effects that always succeed with
`true` and `false`, respectively.---
In the world of JIO, working with effectful functions is a common practice. The following functions
return `IO` effects, and you'll often encounter them in your code:```code
Function>
BiFunction>
```
To make our code more concise and readable, we can give these effectful functions an alias. Let's
call them "Lambdas":```code
interface Lambda extends Function> {}
interface BiLambda extends BiFunction> {}
```
Lambdas are similar to regular functions, but there's one key difference: they never throw
exceptions. In JIO, exceptions are treated as first-class citizens, just like regular values.Converting regular functions or predicates into Lambdas is straightforward using the lift methods:
```
Function opposite = n -> -n;
BiFunction sum = (a,b) -> a + b;
Predicate isOdd = n -> n % 2 == 1;Lambda l1 = Lambda.liftFunction(opposite);
Lambda l2 = Lambda.liftPredicate(isOdd);
BiLambda l3 = BiLambda.liftFunction(sum);```
The `then` method is a powerful feature of the `Lambda` interface in JIO that allows you to compose
and sequence effects in a functional and expressive manner. When you have two `Lambda` instances,
you can use the `then` method to create a new `Lambda` that combines the effects of the two original
lambdas. When you apply the composed `Lambda` to an input, it executes the first `Lambda`, followed
by the second `Lambda`, creating a sequence of effects. This composition is especially useful when
building complex workflows or pipelines of operations. It enhances the readability and
expressiveness of your code by chaining together effects in a natural and intuitive way.```code
Lambda first = ???;
Lambda second = ???Lambda third = first.then(second);
```
---
#### Making our code more resilient being persistent!
Retrying failed operations is a crucial aspect of handling errors effectively in JIO. In real-world
scenarios, errors can sometimes be transient or caused by temporary issues, such as network glitches
or resource unavailability.
By incorporating retry mechanisms, JIO empowers you to gracefully recover from such errors without
compromising the stability of your application. Whether it's a network request, database query, or
any other effect, JIO's built-in retry functionality allows you to define retry policies, such as
exponential backoff or custom strategies, to ensure that your operations have a higher chance of
succeeding. This approach not only enhances the robustness of your application but also minimizes
the impact of transient errors, making JIO a valuable tool for building resilient and reliable
systems.```code
public sealed abstract class IO implements Callable> permits Exp, Val {
IO retry(Predicate predicate,
RetryPolicy policy
);IO repeat(Predicate predicate,
RetryPolicy policy
);
}```
While the `retry` method is primarily used to retry an operation when an error occurs (based on a
specified exception condition), the `repeat` method allows you to repeat an operation based on the
result or outcome of the effect itself,
providing flexibility for scenarios where retries are needed for reasons other than errors. Retry
policies are created in a very declarative and composable way, for example:```code
Duration oneHundredMillis = Duration.ofMillis(100);
Duration oneSec = Duration.ofSeconds(1);
// up to five retries waiting 100 ms
RetryPolicies.constantDelay(oneHundredMillis)
.append(limitRetries(5))// during 3 seconds up to 10 times
RetryPolicies.limitRetries(10)
.limitRetriesByCumulativeDelay(Duration.ofSeconds(3))// 5 times without delay and then, if it keeps failing,
// an incremental delay from 100 ms up to 1 second
RetryPolicies.limiteRetries(5)
.followedBy(incrementalDelay(oneHundredMillis)
.capDelay(oneSec))```
There are very interesting policies implemented based on [this
article](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/): exponential
backoff, full jitter, equal jitter, decorrelated jitter etc#### Making our code more resilient having a Backup plan!
In scenarios where errors persist despite retries, JIO offers robust error-handling mechanisms to
ensure your application maintains resilience. Three key methods come into play:```code
public sealed abstract class IO implements Callable> permits Exp, Val {
IO recover(Function fn);
IO recoverWith(Lambda fn);
IO fallbackTo(Lambda fn);
}
```
**recover**: This method allows you to gracefully recover from an error by providing a function that
maps the encountered exception to a fallback value of type 'O.' It ensures that your application can
continue its operation even in the face of unexpected errors.**recoverWith**: This method allows you to gracefully recover from an error by providing a function
that maps the encountered exception to a fallback value of type 'O.' It ensures that your
application can continue its operation even in the face of unexpected errors.**fallbackTo**: Similar to 'recoverWith,' 'fallbackTo' allows you to switch to an alternative effect
(specified by the provided function) when an error occurs. However, it introduces an important
distinction: if the alternative effect also encounters an error, 'fallbackTo' will return the
original error from the first effect. This ensures that error propagation is maintained while
enabling you to gracefully handle errors and fallback to alternative operations when needed.#### Being Functional!
JIO encourages a functional programming style with the following methods:
```code
public sealed abstract class IO implements Callable> permits Exp, Val {
IO map(Function fn);
IO mapFailure(Function);
IO then(Lambda fn);
}
```
- `map`: Transforms the successful result of an effect using a provided function, allowing you to
map values from one type to another.
- `mapFailure`: Transforms the failure result of an effect using a provided function, allowing you
to map exceptions from one type to another.
- `then` (akin to `flatMap` or `bind` in other languages and a core function in monads): Applies a
lambda function to
the result of the effect, creating a new effect that depends on the previous result. The name
'then' is used here for conciseness.#### Houston, we have a problem!
```code
public sealed abstract class IO implements Callable> permits Exp, Val {
IO debug();
IO debug(EventBuilder builder);
}```
Debugging and logging events play a pivotal role in software development. Some of the advantages
that you'll get are:1. **Simplifying Debugging**: Debugging is the process of identifying and fixing issues in your
code. When testing an application, developers often need to trace the execution of specific parts
of their code to locate bugs or performance bottlenecks. These debugging methods allow you to
attach debug mechanisms to expressions, making it easier to monitor and log the execution of each
operand individually. This granular level of insight is invaluable when diagnosing and resolving
problems.2. **Testing Efficiency**: During the testing phase, the ability to debug and log events efficiently
is a time-saving and productivity-enhancing feature. These methods let you generate and send
events to the Flight Recorder system, which can then be analyzed to gain insights into the
behavior of your code. You can customize these events using the provided `EventBuilder`,
tailoring the information collected to suit your specific testing needs.3. **No Setup Overhead**: One of the key advantages of these methods is that they require minimal
setup. This means that you can integrate debugging and event logging into your codebase without
the need for extensive configuration or external tools. It's a straightforward and hassle-free
way to gather essential information about the execution of your code.4. **Recursive Debugging with debugEach**: When you apply the debugEach method to an expression, it
attaches a debugging mechanism to that expression and all its subexpressions. If any
subexpression within the main expression is itself an expression, debugEach is applied to it
recursively, passing along the same context.5. **Customization with EventBuilder**
Customization with the `EventBuilder` is a powerful feature that streamlines your debugging and
event logging. It allows you to tailor events to your specific needs and focus on the most relevant
information:- **Event Naming**: You can categorize events by specifying the name for the expression being
evaluated and the context you're observing, making it easier to analyze different aspects of your
code.- **Mapping Success Output**: Transform the result of a successful expression into a format that
provides clear and concise information, helping you capture expected outcomes.- **Mapping Failure Output**: Customize how exceptions are presented by mapping `Throwable` objects
into a format that aids in debugging and troubleshooting. This can include error messages, stack
traces, or other relevant details.In conclusion, the ability to debug and log events with minimal setup is highly valuable for
developers. It simplifies the debugging process, enhances testing efficiency, and provides data-rich
insights into code execution. These methods are indispensable tools for maintaining software
quality, diagnosing issues, and optimizing performance during the development and testing phases.#### Being Impatient!
```code
public sealed abstract class IO implements Callable> permits Exp, Val {
IO async();
}
```- `async`: Allows executing an action without waiting for its result and returns immediately. It is
useful when you are not interested in the outcome of the effect (returns void) and want to trigger
it asynchronously.#### Being sneaky!
Sometimes, you need to sneak a peek into the execution of an effect:
```code
public sealed abstract class IO implements Callable> permits Exp, Val {IO peekFailure(Consumer failConsumer);
IO peekSuccess(Consumer successConsumer);
IO peek(Consumer successConsumer,
Consumer failureConsumer
);
}```
- `peekFailure`: Allows you to observe and potentially handle failures by providing a consumer that
logs exceptions in the JFR (Java Flight Recorder) system.
- `peekSuccess`: Similarly, you can observe and process successful results using a success consumer.
- `peek`: Combines both success and failure consumers, giving you full visibility into the effect's
execution.
Exceptions occurring here are logged in the JFR system and do not alter the result of the effect.### I race you!
When you require a result as quickly as possible among multiple alternatives, and you're uncertain
which one will be the fastest:```code
public sealed abstract class IO implements Callable> permits Exp, Val {
static IO race(IO first, IO... others);
}
```
`race` method returns the result of the first effect that completes successfully, allowing you to
make quick decisions based on the outcome. "It employs foundational structural concurrency, which is
a preview functionality introduced in Java 21. Specifically, it leverages the
`StructuredTaskScope.ShutdownOnSuccess` scope.#### Pulling the trigger!
"To initiate the computation for deriving the final result, one must block and await the completion
by invoking the `compute()` method. Notably, blocking is not a concern in this context as it is
expected to be executed from a virtual thread. This operation does not throw exceptions directly;
instead, it encapsulates potential outcomes within the `Result` type:```code
//nothing is evaluated here
IO countUsers = ???;//evaluation is triggered
Result countUsersResult = countUsers.compute();//we take advantage of patter matching to process the result for
//both possible outcomes: success or failureString message =
switch (countUsersResult) {
case Success success -> {
switch (success.output()) {
case 0 -> {
yield "no users!";
}case Integer users when users > 0 -> {
yield "number of users is greater than zero!";
}case Integer _ -> {
yield "number of users is lower than zero";
}
}
}
case Failure failure -> {
switch (failure.exception()) {
case ConnectException _ -> {
yield "Try again!";
}
case SocketTimeoutException _ -> {
yield "so impatient!";
}
case Exception _ -> {
yield "maybe the next time!";
}
}
}
};```
When dealing with a `Result`, pattern matching is preferred whenever feasible. However, you can
still utilize the `Result` API to retrieve the final output:```code
Result countUsersResult;//throws an unchecked exception in case of failure (wraps the real failure in a `CompletionException`)
Integer users = effect.getOutput();//throws a checked exception (the real failure) in case of failure
Integer users = effect.getOutputOrThrow();```
`getOutput` is more suitable when the API being used does not align well with checked exceptions,
such as with `Stream` or `Function`. Conversely, `getOutputOrThrow` mandates handling the potential
exception and ensures that the thrown exception is the real failure and not a CompletionException---
**Using expressions and function composition is how we deal with complexity in Functional
Programming**.
With the following expressions, you will have a comprehensive toolkit to model effects, combine them
in powerful ways, and tame complexity effectively.### IfElseExp
The `IfElseExp` expression allows you to conditionally choose between two computations based on a
predicate. If the
predicate evaluates to true, the consequence is computed, and if it's false, the alternative
computation is chosen. Both the consequence and alternative are represented as lazy computations of
IO effects.```code
IO exp = IfElseExp.predicate(IO condition)
.consequence(Supplier> consequence)
.alternative(Supplier> alternative);```
In this code, the `consequence` and `alternative` parameters are represented as `Supplier`
instances, which means they are not computed during the construction of the expression but rather
evaluated only when needed, based on the result of the condition. This deferred execution allows for
efficient conditional evaluation of IO effects.### SwitchExp
Your original sentence is mostly correct but could benefit from a minor improvement for clarity:
The `SwitchExp` expression simulates the behavior of a switch construct, enabling multiple
pattern-value branches. It evaluates an effect or a value of type `I` and facilitates the use of
multiple clauses based on the evaluation. The `match` method compares the value or effect with
patterns and selects the corresponding lambda (which takes in the value of type `I`). Patterns can
encompass values, lists of values, or even predicates. It's possible to specify a default branch, in
case no pattern matches the input (otherwise the expression is reduced to `IO.NULL`).```code
// matches a value of type I
IO exp =
SwitchExp.eval(I value)
.match(I pattern1, Lambda lambda1,
I pattern2, Lambda lambda2,
I pattern3, Lambda lambda3,
Lambda otherwise //optional
);// matches an effect of type I
IO exp=
SwitchExp.eval(IO effect)
.match(I pattern1, Lambda lambda1,
I pattern2, Lambda lambda2,
I pattern3, Lambda lambda3,
Lambda otherwise //optional
);// For example, the following expression reduces to "3 is Wednesday"
IO exp=
SwitchExp.eval(3)
.match(1, n -> IO.succedd(n + " is Monday"),
2, n -> IO.succedd(n + " is Tuesday"),
3, n -> IO.succedd(n + " is Wednesday"),
4, n -> IO.succedd(n + " is Thursday"),
5, n -> IO.succedd(n + " is Friday"),
n -> IO.succedd(n + " is weekend")
);
```The same as before but using lists instead of constants as patterns.
```code
IO exp =
SwitchExp.eval(I value)
.matchList(List pattern1, Lambda lambda1,
List pattern2, Lambda lambda2,
List pattern3, Lambda lambda3,
Lamda otherwise
);// For example, the following expression reduces to "20 falls into the third week"
IO exp=
SwitchExp.eval(20)
..matchList(List.of(1, 2, 3, 4, 5, 6, 7),
n -> IO.succeed(n + " falls into the first week"),
List.of(8, 9, 10, 11, 12, 13, 14),
n -> IO.succeed(n + " falls into the second week"),
List.of(15, 16, 17, 18, 19, 20),
n -> IO.succeed(n + " falls into the third week"),
List.of(21, 12, 23, 24, 25, 26, 27),
n -> IO.succeedd(n + " falls into the forth week"),
n -> IO.succeed(n + " falls into the last days of the month")
);
```Last but not least, you can use predicates as patterns instead of values or list of values:
```code
IO exp=
SwitchExp.eval(IO value)
.matchPredicate(Predicate pattern1, Lambda lambda1,
Predicate pattern2, Lambda lambda2,
Predicate pattern3, Lambda lambda3
);// For example, the following expression reduces to the default value:
// "20 is greater or equal to ten"IO exp=
SwitchExp.eval(IO.succeed(20))
.matchPredicate(n -> n < 5,
n -> IO.succeed(n + "is lower than five"),
n -> n < 10,
n -> IO.succeed(n + "is lower than ten"),
n-> n > 10,
n -> IO.succeed(n + "is greater or equal to ten")
);
```### CondExp
`CondExp` is a set of branches and a default effect. Each branch consists of an effect that computes
a boolean (the
condition) and its associated effect. The expression is reduced to the value of the first branch
with a true condition, making the order of branches significant. If no condition is true, it
computes the default effect if specified (otherwise the expression is reduced to `IO.NULL`)```code
IO exp=
CondExp.seq(IO cond1, Supplier> effect1,
IO cond2, Supplier> effect2,
IO cond3, Supplier> effect3,
Supplier> otherwise //optional
);IO exp =
CondExp.par(IO cond1, Supplier> effect1,
IO cond2, Supplier> effect2,
IO cond3, Supplier> effect3,
Supplier> otherwise //optional
);```
### AllExp and AnyExp
`AllExp` and `AnyExp` provide idiomatic boolean expressions for "AND" and "OR." They allow you to
compute multiple boolean effects, either sequentially or in parallel.```code
IO allPar = AllExp.par(IO cond1, IO cond2,....);
IO allSeq = AllExp.seq(IO cond1, IO cond2,....);IO anyPar = AnyExp.par(IO cond1, IO cond2,...);
IO anySeq = AnyExp.seq(IO cond1, IO cond2,...);```
You can also create AllExp or AnyExp from streams of IO using the `parCollector` and
`seqCollector````code
Lambda isFerrari = ???
List vehicles = ???;
AllExp allFerrariPar = vehicles.stream()
.map(isFerrary)
.collector(AllExp.parCollector());AllExp allFerrariSeq = vehicles.stream()
.map(isFerrary)
.collector(AllExp.seqCollector());AnyExp anyFerrariSeq = vehicles.stream()
.map(isFerrary)
.collector(AnyExp.seqCollector());```
### PairExp and TripleExp
`PairExp` and `TripleExp` allow you to zip effects into tuples of two and three elements,
respectively. You can compute each element either in parallel or sequentially.```code
IO pairPar = PairExp.par(IO effect1,
IO effect2);IO pairSeq = PairExp.seq(IO effect1,
IO effect2);IO triplePar = TripleExp.par(IO effect1,
IO effect2,
IO effect3);IO tripleSeq = TripleExp.seq(IO effect1,
IO effect2,
IO effect3);```
### JsObjExp and JsArrayExp
`JsObjExp` and `JsArrayExp` are data structures resembling raw JSON. You can compute their values
sequentially or in parallel. You can mix all the expressions discussed so far and nest them,
providing you with immense flexibility and
power in handling complex data structures.```code
IfElseExp a = IfElseExp.predicate(IO cond1)
.consequence(Supplier> consequence)
.alternative(Supplier> alternative);JsArrayExp b =
JsArrayExp.seq(SwitchExp.match(n)
.patterns(n -> n <= 0, Supplier> e1,
n -> n > 0, Supplier> e2
),
CondExp.par(IO cond2, Supplier> e3,
IO cond3, Supplier> e4,
Supplier> otherwise
)
);JsObjExp c = JsObjExp.seq("d", AnyExp.seq(IO cond1, IO cond2)
"e", AllExp.par(IO cond2, IO cond3)
"f", JsArrayExp.par(IO e5, IO e6)
);JsObjExp exp = JsObjExp.par("a",a,
"b",b,
"c",c
);Result json = exp.compute();
```
Here are some key points about the code example:
1. **Readability**: The code is relatively easy to read and understand, thanks to the fluent API
style provided by JIO's expressions. This makes it straightforward to follow the logic of
constructing a `JsObj` with multiple key-value pairs.2. **Modularity**: Each key-value pair is constructed separately, making it easy to add, modify, or
remove components without affecting the others. This modularity is a significant advantage when
dealing with complex data structures.3. **Parallelism**: The example demonstrates the ability to perform computations in parallel when
constructing
the `JsObj`. By using expressions like `JsObjExp.par`, you can take advantage of multicore
processors and improve performance.4. **Nesting**: The example also shows that you can nest expressions within each other, allowing for
recursive data
structures. This is valuable when dealing with deeply nested expressions or other complex data
formats.Overall, the code example effectively illustrates how JIO's expressions enable you to create,
manipulate, and compose functional effects to handle complex data scenarios. It highlights the
conciseness and expressiveness of the library
when dealing with such tasks.### ListExp
Represents an expression that is reduced to a list of values. You can create ListExp expressions
using the `seq` method to evaluate effects sequentially or using the `par` method to evaluate
effects in parallel. If one effect fails, the entire expression fails.```code
ListExp par = ListExp.par(IO effect1, IO effect2, ...)
ListExp seq = ListExp.seq(IO effect3, IO effect3, ...)
Result> xs = par.compute();
Result> ys = seq.compute();```
It's possible to create ListExp from stream of effects of the same type using the collectors
`parCollector` and `seqCollector`:```code
Lambda getPersonFromId = ???;
List ids = ???;
ListExp xs = ids.stream()
.filter(id -> id > 0)
.map(getPersonFromId)
.collect(ListExp.parCollector());Result> persons = xs.compute();
```
---
In functional programming, it's crucial to maintain a clear separation between inputs and outputs of
a function. When dealing with time-related operations, such as retrieving the current date or time,
it becomes even more critical to adhere to this principle. This is where the concept of clocks in
JIO comes into play. A clock in JIO is essentially a supplier that returns a numeric value,
representing time. There are three types of
clocks available:- Realtime: This clock is affected by Network Time Protocol (NTP) adjustments and can move both
forwards and backward in time. It is implemented using the System.currentTimeMillis() method.
Realtime clocks are typically used when you need to work with the current wall-clock time.
- Monotonic: Monotonic clocks are useful for measuring time intervals and performing time-related
comparisons. They are not affected by NTP adjustments and provide a consistent and continuous time
source. Monotonic clocks are implemented using the System.nanoTime() method.
- Custom: JIO allows you to create your custom clocks. Custom clocks are particularly valuable for
testing scenarios
where you want to control the flow of time, possibly simulating the past or future.```code
sealed interface Clock extends Supplier permits Monotonic,RealTime, CustomClock {}
```
Every time you write _new Date()_ or _Instant.now()_ in the body of a method or function, you are
creating a side effect.
Remember that in FP, all the inputs must appear in the signature of a function. Dealing with time,
it's even more
important. Also, it's impossible to control by any test the value of that timestamp which leads to
code difficult to
test.### Why It Matters
The reason why dealing with time as an input is crucial in functional programming is to make code
more predictable,
testable, and less error-prone. Consider the following scenario, which is a common source of bugs:**Bug Scenario**:
```code
public class PaymentService {
public boolean processPayment(double amount) {
// Get the current date and time
Instant currentTime = Instant.now();// Perform payment processing logic
// ...// Check if the payment was made within a specific time window
Instant windowStart = currentTime.minus(Duration.ofHours(1));
Instant windowEnd = currentTime.plus(Duration.ofHours(1));return paymentTime.isAfter(windowStart) && paymentTime.isBefore(windowEnd);
}
}```
**Better Version Using a Clock**
A better approach is to pass a clock as a method parameter:```code
public class PaymentService {
public boolean processPayment(double amount, Clock clock) {
// Get the current time from the provided clock
Instant currentTime = Instant.ofEpochMilli(clock.get());// Perform payment processing logic
// ...// Check if the payment was made within a specific time window
Instant windowStart = currentTime.minus(Duration.ofHours(1));
Instant windowEnd = currentTime.plus(Duration.ofHours(1));return paymentTime.isAfter(windowStart) && paymentTime.isBefore(windowEnd);
}
}```
In this improved version, we pass a Clock object as a parameter to the processPayment method. This
approach offers
several advantages:- Testability: During testing, you can provide a custom clock that allows you to control the current
time, making tests more predictable and reliable.
- Predictability: The behavior of the method is consistent regardless of when it's called since it
depends on the
provided clock.By using a clock as a parameter, you enhance the reliability and maintainability of your code,
especially in scenarios
where time plays a critical role.## Debugging and Java Flight Recorder (JFR) Integration
### Why I chose JFR
In the world of Java, there has long been a multitude of logging libraries and frameworks, each with
its strengths and limitations. However, the introduction of Java Flight Recorder (JFR) has been a
game-changer. JFR is a native and highly efficient profiling and event recording mechanism embedded
within the Java Virtual Machine (JVM). Its native integration means it operates seamlessly with your
Java applications, imposing minimal performance overhead. JFR provides unparalleled visibility into
the inner workings of your code, allowing you to capture and analyze events with precision.Unlike external logging libraries, JFR doesn't rely on third-party dependencies or introduce
additional complexity to
your projects. By using JFR within JIO, you harness the power of this built-in tool to gain deep
insights into the
behavior of your functional effects and expressions, all while keeping your codebase clean and
efficient. JFR is the
dream solution for Java developers seeking robust debugging and monitoring capabilities with minimal
hassle."Debugging and monitoring the behavior of your JIO-based applications is essential for
troubleshooting, performance
optimization, and gaining insights into your functional effects and expressions. JIO provides
comprehensive support for debugging and integration with Java Flight Recorder (JFR) to capture and
analyze events.---
### Debugging Individual Effects
You can enable debugging for individual effects using the `debug` method. When this method is used,
a new effect is created that generates a `RecordedEvent` and sends it to the Flight Recorder system.
You can also customize the event by providing an `EventBuilder`. Here's an overview:The `IO` class has the following methods for debugging:
```code
public sealed abstract class IO implements Callable> permits Exp, Val
{IO debug();
IO debug(EventBuilder builder);
}
```The resulting JFR event is defined as follows:
```code
import jdk.jfr.*;@Label("Expression Evaluation Info")
@Name("jio.exp.EvalExp")
@Category({"JIO", "EXP"})
@Description("Duration, output, context and other info related to a computation")
@StackTrace(value = false)
class ExpEvent extends Event {@Label("exp")
public String expression;@Label("value")
public String value;@Label("context")
public String context;@Label("result")
public String result;
public enum RESULT {SUCCESS, FAILURE}@Label("exception")
public String exception;}
```You can use the [JIO debugger](#junit) to print the events sent to the JFR system. Here's an
example:```code
@RegisterExtension
static Debugger debugger = Debugger of (Duration.ofSeconds(1));@Test
public void test() {Result value =
IO.succeed(10)
.debug()
.compute();Result failure =
IO.fail(new RuntimeException("JIO is great!"))
.debug()
.compute();
}
```The result includes events like this:
```text
------ eval-exp --------
| Expression: Val
| Result: SUCCESS
| Duration: 14,249 ms
| Output: 10
| Context:
| Thread: main
| Event Start Time: 2024-03-23T11:23:28.745121208+01:00
------------------------------- eval-exp --------
| Expression: Val
| Result: FAILURE
| Duration: 39,583 µs
| Output: java.lang.RuntimeException: JIO is great!
| Context:
| Thread: main
| Event Start Time: 2024-03-23T11:23:28.759822+01:00
-------------------------```
The event type is always "eval-exp" (for evaluation), and the expression is "Val" (for Value),
reflecting the evaluation of two irreducible expressions. The result is "SUCCESS" for the first
evaluation, and "FAILURE" for the second. The context in both cases is the default (an empty
string).You can customize event messages using an `EventBuilder`. For example:
```code
EventBuilder eb =
EventBuilder.of("other_exp_name", "fun")
.withSuccessOutput(output -> "XXX")
.withFailureOutput(Throwable::getMessage);Result value =
IO.succeed(10)
.debug(eb)
.compute();Result failure =
IO.fail(new RuntimeException("JIO is great!"))
.debug(eb)
.compute();```
The result with this customization is:
```text
------ eval-exp --------
| Expression: other_exp_name
| Result: SUCCESS
| Duration: 19,285 ms
| Output: XXX
| Context: fun
| Thread: main
| Event Start Time: 2024-03-23T11:26:56.386859042+01:00
------------------------------- eval-exp --------
| Expression: other_exp_name
| Result: FAILURE
| Duration: 44,417 µs
| Output: JIO is great!
| Context: fun
| Thread: main
| Event Start Time: 2024-03-23T11:26:56.407043375+01:00
-------------------------
```The `EventBuilder` provides key points for customization, including specifying event messages for
successful and failed computations and associating events with specific expressions and contexts.### Debugging Expressions
JIO's debugging capabilities extend beyond individual effects. You can attach a debug mechanism to
each operand of an expression using the `debugEach` method. This allows you to monitor and log the
execution of each operand individually. Here's an overview:```code
sealed abstract class Exp extends IO
permits AllExp, AnyExp, CondExp, IfElseExp, JsArrayExp,
JsObjExp, ListExp, PairExp, SwitchExp, TripleExp {Exp debugEach(EventBuilder builder);
Exp