Last active
June 16, 2024 07:13
-
-
Save tonivade/3db0e57e3eff11426ba297c7c96df03b to your computer and use it in GitHub Desktop.
MTL in Java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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