Skip to content

Instantly share code, notes, and snippets.

@tonivade
Last active June 16, 2024 07:13
Show Gist options
  • Save tonivade/3db0e57e3eff11426ba297c7c96df03b to your computer and use it in GitHub Desktop.
Save tonivade/3db0e57e3eff11426ba297c7c96df03b to your computer and use it in GitHub Desktop.
MTL in Java
//usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 21
//JAVAC_OPTIONS --enable-preview -source 21
//JAVA_OPTIONS --enable-preview
//DEPS com.github.tonivade:purefun-typeclasses:5.0
//DEPS com.github.tonivade:purefun-instances:5.0
//DEPS com.github.tonivade:purefun-transformer:5.0
import static com.github.tonivade.purefun.core.Precondition.checkNonEmpty;
import static com.github.tonivade.purefun.core.Precondition.checkNonNull;
import static com.github.tonivade.purefun.core.Precondition.checkRange;
import static java.util.Comparator.comparing;
import static java.util.concurrent.ThreadLocalRandom.current;
import java.util.Comparator;
import com.github.tonivade.purefun.Kind;
import com.github.tonivade.purefun.core.Tuple;
import com.github.tonivade.purefun.core.Tuple2;
import com.github.tonivade.purefun.core.Unit;
import com.github.tonivade.purefun.data.ImmutableMap;
import com.github.tonivade.purefun.instances.MonadMTL;
import com.github.tonivade.purefun.instances.MonadMTL.EffectS;
import com.github.tonivade.purefun.monad.IO;
import com.github.tonivade.purefun.monad.IOOf;
import com.github.tonivade.purefun.type.Either;
import com.github.tonivade.purefun.type.Option;
import com.github.tonivade.purefun.type.Try;
import com.github.tonivade.purefun.typeclasses.Console;
import com.github.tonivade.purefun.typeclasses.Instances;
import com.github.tonivade.purefun.typeclasses.Monad;
import com.github.tonivade.purefun.typeclasses.MonadError;
import com.github.tonivade.purefun.typeclasses.MonadReader;
import com.github.tonivade.purefun.typeclasses.MonadState;
public class MTL<F extends Kind<F, ?>, E extends Console<F> & Monad<F> & MonadError<F, Error> & MonadState<F, Requests> & MonadReader<F, Config>> {
private final E $;
public MTL(E ev) {
this.$ = checkNonNull(ev);
}
public Kind<F, Unit> program() {
return $.use()
.andThen(this::mainLoopAndRecover)
.andThen(this::program)
.run();
}
private Kind<F, Unit> mainLoopAndRecover() {
return $.handleErrorWith(mainLoop(), error -> $.printf("Error: %s", error));
}
private Kind<F, Unit> mainLoop() {
return $.use()
.andThen(this::hostAndPort)
.flatMap(t -> t.applyTo((host, port) -> $.printf("connecting to %s:%d", host, port)))
.andThen(this::askAndFetchAndPrint)
.run();
}
private Kind<F, Tuple2<String, Integer>> hostAndPort() {
return $.use()
.andThen(this::host)
.andThen(this::port)
.tuple();
}
private Kind<F, String> host() {
return $.reader(Config::host);
}
private Kind<F, Integer> port() {
return $.reader(Config::port);
}
private Kind<F, Unit> askAndFetchAndPrint() {
return $.use()
.andThen(this::askAndFetch)
.flatMap(t -> t
.applyTo((city, forecast) -> $.printf("Forecast for city %s is %s", city.name(), forecast.temperature())))
.andThen(this::hottestCity)
.flatMap(hottest -> $.printf("Hottest city so far: %s", hottest))
.run();
}
private Kind<F, Tuple2<City, Forecast>> askAndFetch() {
return $.use()
.andThen(this::askCity)
.flatMap(this::fetchForecast)
.apply(Tuple::of);
}
private Kind<F, String> hottestCity() {
return $.inspect(Requests::hottest);
}
private Kind<F, Forecast> fetchForecast(City city) {
return $.use()
.then($.inspect(requests -> requests.get(city)))
.flatMap(forecast -> forecast.fold(() -> forecast(city), $::pure))
.flatMap(forecast -> $.modify(requests -> requests.put(city, forecast)))
.apply((a, b, c) -> b);
}
private Kind<F, City> askCity() {
return $.use()
.then($.println("What city?"))
.andThen($::readln)
.flatMap(this::cityByName)
.run();
}
private Kind<F, City> cityByName(String name) {
return switch (name) {
case "Madrid", "Getafe", "Elche" -> $.pure(new City(name));
default -> $.raiseError(new UnknownCity(name));
};
}
private Kind<F, Forecast> forecast(City city) {
return $.use()
.and(current().nextInt(30))
.map(Forecast::new)
.run();
}
public static void main(String[] args) {
var mtl = new MTL<>(new AllInstances());
var result = mtl.program().fix(EffectS::<IO<?>, Requests, Config, Error, Unit>toEffectS);
result.run(new Requests()).run(new Config("localhost", 8080)).run().fix(IOOf::toIO).unsafeRunSync();
}
}
sealed interface Error permits UnknownError, UnknownCity {
static <T> Either<Error, T> fromTry(Try<T> value) {
return value.toEither().mapLeft(UnknownError::new);
}
}
record UnknownCity(String name) implements Error {
UnknownCity {
checkNonEmpty(name);
}
}
record UnknownError(Throwable error) implements Error {
UnknownError {
checkNonNull(error);
}
@Override
public String toString() {
return error.toString();
}
}
record Config(String host, int port) {
Config {
checkNonEmpty(host);
checkRange(port, 1024, 65535);
}
}
record Requests(ImmutableMap<String, Forecast> map) {
Requests {
checkNonNull(map);
}
public Requests() {
this(ImmutableMap.empty());
}
Option<Forecast> get(City city) {
return map.get(city.name());
}
String hottest() {
Comparator<Tuple2<String, Forecast>> comparator = comparing(entry -> entry.get2().temperature());
return map.entries().asList().sort(comparator.reversed()).head().map(Tuple2::get1).getOrElseNull();
}
Requests put(City city, Forecast forecast) {
return new Requests(map.put(city.name(), forecast));
}
}
record Forecast(int temperature) {
}
record City(String name) {
City {
checkNonEmpty(name);
}
}
class AllInstances extends MonadMTL<IO<?>, Requests, Config, Error> implements Console<EffectS<IO<?>, Requests, Config, Error, ?>> {
private final Console<IO<?>> ioConsole = Instances.<IO<?>>console();
public AllInstances() {
super(Instances.<IO<?>>monad());
}
@Override
public EffectS<IO<?>, Requests, Config, Error, String> readln() {
var readln = ioConsole.readln().fix(IOOf::<String>toIO).attempt().map(Error::fromTry);
return effect(readln);
}
@Override
public EffectS<IO<?>, Requests, Config, Error, Unit> println(String text) {
var println = ioConsole.println(text).fix(IOOf::<Unit>toIO).attempt().map(Error::fromTry);
return effect(println);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment