Problemet är att så fort man börjar blanda in sidoeffekter på några ställen så tappar man tyvärr många av fördelarna. Jag programmerar inte i java men testade att göra den här uppgiften med bara referentiellt transparenta funktioner för att se ungefär hur det skulle se ut.
Så visst går det att programmera funktionellt i java men speciellt smidigt är det inte.
class Unit {
public final static Unit Unit = new Unit();
private Unit() {}
}
interface Hkt<F, A> {}
interface Hkt2<F, A, B> extends Hkt<Hkt<F, A>, B> {}
interface Monad<F> {
<A> Hkt<F, A> pure(A a);
<A, B> Hkt<F, B> flatMap(Hkt<F, A> fa, Function<A, Hkt<F, B>> f);
default <A, B> Hkt<F, B> flatMap_(Hkt<F, A> fa, Hkt<F, B> fb) { return flatMap(fa, dummy -> fb); }
default <A, B> Hkt<F, B> map(Hkt<F, A> fa, Function<A, B> f) { return flatMap(fa, f.andThen(this::pure)); }
}
abstract class IO<A> implements Hkt<IO.m, A> {
static final class m {}
static <A> IO<A> narrowK(Hkt<IO.m, A> Fa) { return (IO<A>) Fa; }
public static final IOMonad monad = new IOMonad() {};
public static final IOConsole console = new IOConsole() {};
abstract A unsafeRunSync();
}
interface IOMonad extends Monad<IO.m> {
@Override
default <A> IO<A> pure(A a) {
return new IO<>() {
@Override
A unsafeRunSync() {
return a;
}
};
}
@Override
default <A, B> IO<B> flatMap(Hkt<IO.m, A> fa, Function<A, Hkt<IO.m, B>> f) {
return new IO<>() {
@Override
B unsafeRunSync() {
return ((IO<B>) f.apply(((IO<A>) fa).unsafeRunSync())).unsafeRunSync();
}
};
}
}
interface Console<F> {
Hkt<F, Unit> putStrLn(String s);
Hkt<F, String> getStrLn();
}
interface IOConsole extends Console<IO.m> {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
default IO<Unit> putStrLn(String s) {
return new IO<>() {
@Override
Unit unsafeRunSync() {
System.out.println(s);
return Unit;
}
};
}
default IO<String> getStrLn() {
return new IO<>() {
@Override
String unsafeRunSync() {
try {
return in.readLine();
} catch (IOException e) {
throw new RuntimeException();
}
}
};
}
}
class Tuple<A, B> {
public final A _1;
public final B _2;
Tuple(A a, B b) {
_1 = a;
_2 = b;
}
}
class State<S, A> implements Hkt2<State.m, S, A> {
public static final class m {}
static <S, A> State<S, A> narrowK(Hkt<Hkt<State.m, S>, A> Fa) { return (State<S, A>) Fa; }
public static final StateMonad monad = new StateMonad() {};
public static final StateConsole console = new StateConsole() {};
public final Function<S, Tuple<S, A>> run;
State(Function<S, Tuple<S, A>> run) { this.run = run; }
public static <S, A> State<S, A> pure(A a) { return new State<>(s -> new Tuple<>(s, a)); }
public static <S> State<S, Unit> set(S s) { return new State<>(dummy -> new Tuple<>(s, Unit)); }
public static <S> State<S, S> get() { return new State<>(s -> new Tuple<>(s, s)); }
public static <S> State<S, Unit> modify(Function<S, S> f) { return new State<>(s -> new Tuple<>(f.apply(s), Unit)); }
public <B> State<S, B> flatMap(Function<A, State<S, B>> f) {
return new State<>(s -> {
var tmp = run.apply(s);
return f.apply(tmp._2).run.apply(tmp._1);
});
}
public <B> State<S, B> map(Function<A, B> f) { return flatMap(a -> pure(f.apply(a))); }
}
class TestData {
public final List<String> in;
public final List<String> out;
TestData(List<String> in, List<String> out) {
this.in = in;
this.out = out;
}
}
interface StateConsole extends Console<Hkt<State.m, TestData>> {
@Override
default State<TestData, Unit> putStrLn(String s) {
return State.modify(td -> {
var tdOut = new ArrayList<>(td.out);
tdOut.add(s);
return new TestData(td.in, List.copyOf(tdOut));
});
}
@Override
default State<TestData, String> getStrLn() {
return new State<>(td -> {
var tdIn = new ArrayList<>(td.in);
var head = tdIn.remove(0);
var tdOut = new ArrayList<>(td.out);
tdOut.add("(User input: " + head + ")");
return new Tuple<>(new TestData(List.copyOf(tdIn), List.copyOf(tdOut)), head);
});
}
}
interface StateMonad<S> extends Monad<Hkt<State.m, S>> {
@Override
default <A> State<S, A> pure(A a) {
return State.pure(a);
}
@Override
default <A, B> State<S, B> flatMap(Hkt<Hkt<State.m, S>, A> fa, Function<A, Hkt<Hkt<State.m, S>, B>> f) {
return ((State<S, A>) fa).flatMap(a -> (State<S, B>) f.apply(a));
}
}
public class Main {
private static Optional<Integer> parseInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
private static <A, B> B fold(Optional<A> oa, Supplier<B> empty, Function<A, B> present) {
if(oa.isPresent()) return present.apply(oa.get());
else return empty.get();
}
private static <F> Hkt<F, Integer> rentInput(Monad<F> M, Console<F> C) {
return
M.flatMap_(C.putStrLn("Ange din månadshyra:"),
M.flatMap(C.getStrLn(), rentStr ->
fold(parseInt(rentStr),
() -> M.flatMap_(C.putStrLn("Du måste ange månadshyran som ett heltal"), rentInput(M, C)),
rent -> rent < 0 ? M.flatMap_(C.putStrLn("Hyran kan inte vara negativ"), rentInput(M, C)) : M.pure(rent))));
}
private static <F> Hkt<F, Integer> sqmInput(Monad<F> M, Console<F> C) {
return
M.flatMap_(C.putStrLn("Ange lägenhetens storlek i kvadratmeter:"),
M.flatMap(C.getStrLn(), sqmStr ->
fold(parseInt(sqmStr),
() -> M.flatMap_(C.putStrLn("Du måste ange lägenhetens storlek som ett heltal"), sqmInput(M, C)),
sqm -> sqm <= 0 ? M.flatMap_(C.putStrLn("Lägenhetens storlek måste vara över 0 kvm"), sqmInput(M, C)) : M.pure(sqm))));
}
private static String rentPerSqm(int rent, int sqm) {
var formatter = new DecimalFormat("0.##");
return formatter.format((double) rent / sqm);
}
private static <F> Hkt<F, Unit> program(Monad<F> M, Console<F> C) {
return
M.flatMap_(C.putStrLn("Ange ditt namn:"),
M.flatMap(M.map(C.getStrLn(), String::toUpperCase), name ->
M.flatMap(rentInput(M, C), rent ->
M.flatMap(sqmInput(M, C), sqm ->
C.putStrLn(name + " du betalar " + rentPerSqm(rent, sqm) + "kr per kvadratmeter i månaden.")))));
}
public static void main(String[] args) {
IO<Unit> program1 = IO.narrowK(program(IO.monad, IO.console));
program1.unsafeRunSync();
State<TestData, Unit> program2 = State.narrowK(program(State.monad, State.console));
program2.run.apply(new TestData(List.of("John Doe", "2500kr", "-100", "4500", "0m2", "0", "55"),
List.of()))._1.out.forEach(System.out::println);
}
}