Add a partial Coeffect system into Java using Loom's ExtentLocals

Related tags

Spring Boot Coeffect
Overview

Coeffect

Maven Central license

Add a partial Coeffect system into Java using Loom's ExtentLocals.


In Java there are generally 2 strategies to manage the parameters a method needs:

  1. Passing a value as a parameter
  2. Having the value as fields of the class

Furthermore, to ensure thread safety we need to have more work. For the first method the problem is less apparent, but for the latter it is much harder to deal with.

One way to ensure safety is to use Java's ThreadLocal, which ensures that a parameter cannot pass through different threads:

public class Example {
    private static ThreadLocal<String> TL = new ThreadLocal<>();

    public void foo() {
        System.out.println(TL.get());
    }

    public static void main(String[] args) {
        var x = new Example();
        CompletableFuture.runAsync(() -> {
            TL.set("^o^");
            Thread.sleep(3000); // omitting exception handling
            x.foo();
        });
        CompletableFuture.runAsync(() -> {
            Thread.sleep(1000); // omitting exception handling
            TL.set("o7");
            x.foo();
        });
    }
}

This will print

o7

^o^

Project Loom has(/will) added ExtentLocal, which is basically a structured ThreadLocal.

One of the most problematic parts of ThreadLocal and ExtentLocal is that we lose type safety. For ThreadLocal you can get unexpected nulls, and for ExtentLocal you would get an exception.

Any use of ThreadLocal or ExtentLocal should be attached to a null-check or binding-check. Furthermore, have one of those 2 not being private creates coupling, security problems, ambiguous APIs.

On the other hand, sending dependencies as parameters have other problems, but the main two I want to talk about are:

  1. Parameter bloating
  2. Forced explicit binding

The first point is pretty clear, you can get methods with 5/6 parameters or more, which creates long signatures as well as long in the calling site.

The second point is easier to miss, but here is an example:

class clazz {
    public static void main(String[] args) {
        foo(666);
    }

    public static void foo(int x) {
        bar(x);
    }

    public static void bar(int x) {
        System.out.println(x);
    }
}

Notice that foo receive a parameter only to pass it to bar, it doesn't actually do anything with it.


The "solution"

The solution this library offers is to create a (partial) Coeffect System.

The idea is to use ExtentLocal and a compiler plugin to add safety and explicitness.

Implementation note: It is impossible to create this system with ThreadLocal because there is no control over the call of ThreadLocal#remove.

Before diving into the details, let's see how the above example will look like:

class clazz {
    public static void main(String[] args) {
        Coeffect.with(666)
                .run(() -> foo());
    }

    @WithContext(Integer.class)
    public static void foo() {
        bar();
    }

    @WithContext(Integer.class)
    public static void bar() {
        System.out.println(Coeffect.get(Integer.class));
    }
}

With can notice few parts:

  • We used Coeffect.get(Integer.class) in bar to get the top integer stored in the global Coeffect.
  • We annotated bar with @WithContext(Integer.class) to denote that we are using Integer in the method.
  • We called bar in foo.
  • We annotated foo with @WithContext(Integer.class) to denote that we are using a method that requires Integer in it.
  • We called Coeffect.with(666) to put 666 in the top of the stack of Integer.class.
  • We called run on the Coeffect.with(666) to run a Runnable with the current stack.
  • In the Coeffect.with(666).run clause we are running foo
  • We do not need to specify @WithContext(Integer.class) on the main method because we don't use any unbound dependency

Note that all of those points are enforced at compile time, remove any of the @WithContext and the compiler will yell at you.


The details

There are few basic rules one should keep in mind, let's go over them (every time I say run, it also applies to call , which is the same but also returns a value):

Enforcing of Coeffect#get

Any and all calls of Coeffect.get(T) must satisfy one of the 2 following conditions:

  1. Inside of Coeffect.with(T)#run block
  2. Inside a method annotated with @WithContext(T)

Methods annotated with @WithContext

Any use of a method annotated with @WithContext act similarly to Coeffect#get, with the exception that @WithContext can receive several types.

Coeffect stacks

Coeffect internally saves an ExtentLocal instance for each Class<T>. When calling Coeffect.with(v) it adds v to the top of the stack of v.getClass().

The value of Coeffect#get

When calling Coeffect.get(T) it will return the top value in the stack of T. Note that this is a peek, it does not remove it from the stack.

Implemention note: Coeffect#get should be used only with Class literals, e.g. String.class, and not "hi".getClass(), using non-class literals can either fail at complication, or create false negatives.

Extents

The lifetime of every binding is exactly the Coeffect.Carrier#run clause:

class clazz {
    void foo() {
        Coeffect.with(3)
                .with("Holo")
                .run(() -> {
                    Coeffect.with(6)
                            .run(() -> {
                                Coeffect.get(Integer.class); // 6
                                Coeffect.get(String.class); // Holo
                            });
                    Coeffect.get(Integer.class); // 3
                    Coeffect.get(String.class); // Holo
                });
    }
}

Inheritance

For similar reasoning as return types and checked exceptions, the classes in the @WithContext annotations are covariant.

That means that if method clazz::foo is annotated with @WithContext(...T) (where ...T means list of types), and clazz1 extends clazz as well as clazz1::foo is annotated with @WithContext(...Z) then we require that ...Z will be a subset of ...T:

import io.github.holo314.coeffect.compiletime.annotations.WithContext;

class clazz {
    @WithContext({String.class, Integer.class})
    void foo() {}
}

class class1
        extends clazz {
    // @WithContext({String.class, Integer.class}) // legal
    // @WithContext({String.class}) // legal
    // @WithContext({Integer.class}) // legal
    // @WithContext() // legal
    @WithContext(CharSequence.class)
    // illegal, `CharSequence.class` does not appear in the `@WithContext` annotation of `clazz::foo`
    @Override
    void foo() {}
}

Similar thing is true about interfaces and implementation

Threads

One of the most complicated parts of programming is multiprocessing, be it with threads/continuations or any other implementation.

Coeffect is built upon ExtentLocal that comes with project Loom to complement Structured Concurrency, that means that all work with threads and Coeffect together should use Structured Concurrency, any use of non-Structured Concurrency can cause false positives.

The Coeffect.Carrier object

When first binding an object using Coeffect#with the return type is Carrier<>.

This object is an immutable object contains within it both the actual stacks, and the types that your bound, so:

import io.github.holo314.coeffect.runtime.Coeffect;

class Example {
    void foo() {
        var carrier = Coeffect.with(":|");
        carrier.with("|:");
        carrier.run(() -> System.out.println(Coeffect.get(String.class))); // print ":|"
    }
}

Like I said above, this object holds the types that got bound, you can see that if you are use explicit typing, instead of var:

import io.github.holo314.coeffect.runtime.Coeffect;

class Example {
    void foo() {
        // Thanks god for type inference
        Coeffect.Carrier<String, Coeffect.Carrier<Void, Coeffect.Carrier<?, ?>>> carrier = Coeffect.with(":|");
        carrier.with("|:");
        carrier.run(() -> System.out.println(Coeffect.get(String.class))); // print ":|"
    }
}

The Coeffect plugin uses this type as a linked list:

null                            ⇔ Coeffect.Carrier<?, ?>
Node(Void, null)                ⇔ Coeffect.Carrier<Void, null>                  ⇔ Coeffect.Carrier<Void, Coeffect.Carrier<?, ?>>
Node(String, Node(Void, null))  ⇔ Coeffect.Carrier<String, Node(Void, null)>    ⇔ Coeffect.Carrier<String, Coeffect.Carrier<Void, null>> ⇔ Coeffect.Carrier<String, Coeffect.Carrier<Void, Coeffect.Carrier<?, ?>>>

Using this linked list it checks which types you used but didn't bind. This is why you should never downcast the carrier object.

Passing Coeffect.Carrier as a parameter

It is possible to think of Coeffect.Carrier as a set of types that represent some context, each instance of Coeffect.Carrier represent a set of parameters that you can use explicitly.

This is why it may be sometimes tempting to pass Coeffect.Carrier as a parameter to a method, but you should never do this.

This is several reasons, the first and most important of them is: the whole point of this library is to avoid passing contextual objects as parameters to a method. Passing Coeffect.Carrier as a parameter is basically using the Coeffect system to implement parameters!

Instead, any method that receive a Coeffect.Carrier parameter should transform it into @WithContext annotation:

import io.github.holo314.coeffect.compiletime.annotations.WithContext;
import io.github.holo314.coeffect.runtime.Coeffect;

class Example {
    void foo() {
        bar(Coeffect.with(":'("));
    }

    void bar(Coeffect.Carrier<String, Coeffect.Carrier<Void, Coeffect.Carrier<?, ?>>> x) {
        x.run(Example::qux);
    }

    @WithContext(String.class)
    void qux() {
        System.out.println(Coeffect.get(String.class));
    }
}

Into

import io.github.holo314.coeffect.compiletime.annotations.WithContext;
import io.github.holo314.coeffect.runtime.Coeffect;

class Example {
    void foo() {
        Coeffect.with(":')").run(Example::bar);
    }

    @WithContext(String.class)
    void bar() {
        qux();
    }

    @WithContext(String.class)
    void qux() {
        System.out.println(Coeffect.get(String.class));
    }
}

Lambda's problem

Currently, annotation's parameters must be known at compiletime, that means that there is not way to allow generics on the annotation level.

Why is this problematic? Let's take the following example:

import java.util.ArrayList;
import java.util.function.Function;

public class IntTransformer {
    ArrayList<Function<Integer, Integer>> transformers = new ArrayList<>();

    public void transform(Function<Integer, Integer> transform) {
        transformers.add(map);
    }

    public List<Integer> run(int i) {
        for (var t: transformers) {
            i = t.apply(i);
        }
        return i;
    }
}

Now we want to use it with combination of Coeffect:

import io.github.holo314.coeffect.runtime.Coeffect;

public class A {
    public static void main(String[] args) {
        var x = new intTransformer();
        x.transform(r -> {
            var z = Coeffect.get(Integer.class); // ?????
            return r + z;
        });
    }
}

We cannot dynamically bind objects to an effect, with generics we would "collect the effects" to the instance of IntTransformer and "discharge" it on "run".

Because of that the current implementation requires adding a context to the method that defines the lambda.

I am open for suggestions for better solutions.


Future Work and Extra notes

Currently, the compiletime component is a custom component of error-prone with is only an analysing tool.

In the future I want to add a functionality for more fluent access to the stacks.

In particular, I want to be able to do something like the following:

class clazz {
    void foo() {
        Coeffect.with(3)
                .with("Holo")
                .run(() -> {
                    Coeffect.with(6)
                            .run(() -> {
                                Integer.get(); // 6
                                String.get(); // Holo
                            });
                    Integer.get(); // 3
                    String.get(); // Holo
                });
    }
}

I was also toying with the idea of enabling named coeffects.

Effects

The name Coeffect comes, unsurprisingly, from Effect system.

Java does have a (partial) Effect System, the checked exceptions, the difference between an effect and a coeffect is relatively thin, I hope in the future to give the Coeffect type system the same strength as Checked Exceptions

There are languages that are completely built upon an Effect System, for example Koka and Effekt.


Usage

To use this project you first need to download Early Access Java 19-loom. The project currently use build 19-loom+6-625.

The plugin and library are available in Maven central and requires Error-prone.

Gradle

Because of a missing feature in gradle, it is not possible to use arbitrary Java versions, in particular, early access releases don't work.

Hence, it is not possible to use it with Gradle

Maven

Library

To use the library itself first add to your pom.xml the following dependency:

<dependency>
    <groupId>io.github.holo314</groupId>
    <artifactId>Coeffect</artifactId>
    <version>{coeffect.version}</version>
</dependency>

When running the program you need to add --add-modules jdk.incubator.concurrent to the JVM options.

Plugin

To run the plugin you need to add the following section to your maven-compiler-plugin:

<configuration>
    ...
    <compilerArgs>
        <arg>-XDcompilePolicy=simple</arg>
        <arg>-Xplugin:ErrorProne -XepDisableAllChecks -Xep:Coeffect</arg>
    </compilerArgs>
    <annotationProcessorPaths>
        <path>
            <groupId>io.github.holo314</groupId>
            <artifactId>Coeffect</artifactId>
            <version>{coeffect.version}</version>
        </path>
        <path>
            <groupId>com.google.errorprone</groupId>
            <artifactId>error_prone_core</artifactId>
            <version>${errorprone.version}</version>
        </path>
    </annotationProcessorPaths>
</configuration>

The -XepDisableAllChecks flag is optional, it is there to disable all the default Error-Prone checks

You might also like...

Ti.googlepay - Add Google Pay to your app

Ti.googlepay - Add Google Pay to your app

Google Pay Add Google Pay to your app: Events available success canceled error Methods setupPaymentGateway: name, apiKey createPaymentRequest: environ

Nov 7, 2022

Team 5468's 2022 FRC robot code. This code is written in Java and is based off of WPILib's Java control system and utilizes a command based system

FRC 2022 Team 5468's 2022 FRC robot code. This code is written in Java and is based off of WPILib's Java control system and utilizes a command based s

Oct 4, 2022

Flash Sale System AKA. seckill system

FlashSaleSystem Project highlights Distributed system scheme From a single machine to a cluster, it is easy to scale horizontally simply by adding ser

Sep 13, 2022

Human Resources Management System Using React with Java

Human Resources Management System Using React with Java

File Directory Main Adapters Abstract Concretes Business Abstract Concretes Core / Utilities Regex Results Upload Data Access Abstract Entities Concre

Jul 28, 2021

Library Management System made using java 📚

Library Management System 📚 About the project This project was made using java ☕ It uses MySQL database to store data User Interfaces made using java

Oct 18, 2021

Community extension to generate a Java client from the provided Camunda 7 OpenAPI descitpion and also warp it into Spring Boot

Camunda Engine OpenAPI REST Client Java and Spring Boot This community extension is a convenience wrapper around the generated Java client from the Ca

Dec 28, 2022

Team 2539's Java training, consisting of a series of projects that combine into a full robot code base.

Java Training Part 1 - Learning Java (Skip this part if you have taken AP Comp Sci) For learning Java, we currently recommend using codecademy, but mo

Aug 24, 2022

Hotel reservation system using spring boot and thymeleaf.

Hotel reservation system using spring boot and thymeleaf.

Hotel reservation system using spring boot and thymeleaf. This project implements a complex domain model powering a session based wizard form flow to

Jun 2, 2022

A high availability shopping(ecommerce) system using SpringBoot, Spring Cloud, Eureka Server, Spring Cloud Gateway, resillience4j, Kafka, Redis and MySQL.

A high availability shopping(ecommerce) system using SpringBoot, Spring Cloud, Eureka Server, Spring Cloud Gateway, resillience4j, Kafka, Redis and MySQL.

High-availability-shopping-system A high availability shopping(ecommerce) system using SpringBoot, Spring Cloud, Eureka Server, Spring Cloud Gateway,

Oct 26, 2022
Owner
Holo
Holo
Add custom auth options (e.g. face unlock) to your phone using Xposed.

UniversalAuth This project aims to bring a variety of custom authentication options to various Android ROMs. Your phone needs to have Xposed (or EdXpo

Andy Bao 134 Dec 29, 2022
A bukkit/spigot plugin to add custom music discs using the Simple Voice Chat API.

Custom Discs 1.19 A spigot/bukkit fork of henkelmax's Audio Player. Play custom music discs using the Simple Voice Chat API. (The voice chat mod is re

null 21 Dec 22, 2022
Imports waypoints from DCS F10 map into the plane navigation system, like a Data Transfer Cartidge.

DCSTheWay Imports waypoints from DCS F10 map into the plane navigation system, like a Data Transfer Cartridge. Special thanks to kukiric for helping o

Aaron Daniel 85 Dec 27, 2022
App to demonstrate the passage of adapter items into activity using interfaces

OnClickListenerExample This application shows how to pass adapter items into an activity using interfaces. The sample data set contains two fields rep

Rohan Bari 1 Feb 2, 2022
Add something to Minecraft LAN Server List

DiscoverMe Make a fortune... in Minecraft Server List! 0 dependencies, Java 8 compatible. Usage Example: DiscoverMe.INSTANCE.addEntry(SimpleChatColor.

iceBear 4 Oct 13, 2021
Patches for the old minecraft official launcher to add microsoft account support

MSA4Legacy Patches for the old minecraft official launcher to add microsoft account support My code here is quite atrocious in some parts (for example

Nep Nep 26 Nov 3, 2022
NPM Package - A react native component that let you to add a wavy background UI.

react-native-wavy-background A react native component that let you to add a wavy background UI. Installation npm install react-native-wavy-background

Shevon Soyza 10 Oct 19, 2022
Experiments on how to add Loom support for Netty

Netty Loom Experiment This repository contains Project Loom and Netty related test code. Contents / Goals I created these examples since I was curious

Johannes Schüth 5 Oct 14, 2022
Program that allows employees to clock in and clock out of work. Employees who are managers can add, edit and delete employees and shifts from the database.

Clock-In-Clock-Out-System Created by: Kennedy Janto, Taylor Vandenberg, Duc Nguyen, Alex Gomez, Janista Gitbumrungsin This is a semester long project

null 6 Nov 5, 2022
This repository is totally dedicated to Hacktoberfest 2021, feel free to add your project.

push-it-hacktoberfest This repository is totally dedicated to Hacktoberfest 2021, feel free to add your project. HacktoberFest21 Hello Hackers, Contri

Muhammad Bilal 3 Oct 31, 2021