package fj.demo; import fj.F; import fj.F1Functions; import fj.F1W; import fj.Unit; import fj.data.IO; import fj.data.IOFunctions; import fj.data.IOW; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import static fj.data.IOFunctions.stdinReadLine; import static fj.data.IOFunctions.stdoutPrint; import static fj.data.IOFunctions.stdoutPrintln; import static fj.data.IOFunctions.toSafeValidation; /** * Demonstrates how to use <code>IO</code> and basic function composition. */ public class IOWalkthrough { public static void main(String[] args) { // IO is just a container to defer a computation (lazy), with the intention to encapsulate computations that either // consume and/or produce side-effects // the computation is not (yet) executed on creation hence it can be treated like a value final IO<Unit> askName = () -> { System.out.println("Hi, what's your name?"); return Unit.unit(); }; // fj.data.IOFunctions contains a lot of convenience functions regarding IO, the above example could be rewritten with IOFunctions.stdoutPrintln // we now create an IO value to prompt for the name if executed IO<Unit> promptName = IOFunctions.stdoutPrint("Name: "); // we can compose these two values with fj.data.IOFunctions.append, since they both are not interested in any runtime value IO<Unit> askAndPromptName = IOFunctions.append(askName, promptName); // now we create an IO value to read a line from stdin final IO<String> readName = () -> new BufferedReader(new InputStreamReader(System.in)).readLine(); // this is the same as IOFunctions.stdinReadLine() // now we create a function which takes a string, upper cases it and creates an IO value that would print the upper cased string if executed final F<String, IO<Unit>> upperCaseAndPrint = F1Functions.<String, IO<Unit>, String>o(IOFunctions::stdoutPrintln).f(String::toUpperCase); // we now want to compose reading the name with printing it, for that we need to have access to the runtime value that is returned when the // IO value for read is executed, hence we use fj.data.IOFunctions.bind instead of fj.data.IOFunctions.append final IO<Unit> readAndPrintUpperCasedName = IOFunctions.bind(readName, upperCaseAndPrint); // so append is really just a specialised form of bind, ignoring the runtime value of the IO execution that was composed before us final IO<Unit> program = IOFunctions.bind(askAndPromptName, ignored -> readAndPrintUpperCasedName); // this is the same as writing IOFunctions.append(askAndPromptName, readAndPrintUpperCasedName) // we have recorded the entire program, but have not run anything yet // now we get to the small dirty part at the end of our program where we actually execute it // we can either choose to just call program.run(), which allows the execution to escape // or we use safe to receive an fj.data.Either with the potential exception on the left side toSafeValidation(program).run().on((IOException e) -> { e.printStackTrace(); return Unit.unit(); }); // doing function composition like this can be quite cumbersome, since you will end up nesting parenthesis unless you flatten it out by // assigning the functions to variables like above, but you can use the fj.F1W syntax wrapper for composing single-argument functions and fj.data.IOW // for composing IO values instead, the entire program can be written like so: IOW.lift(stdoutPrintln("What's your name again?")) .append(stdoutPrint("Name: ")) .append(stdinReadLine()) .bind(F1W.lift((String s) -> s.toUpperCase()).andThen(IOFunctions::stdoutPrintln)) .safe().run().on((IOException e) -> { e.printStackTrace(); return Unit.unit(); }); } }