Calculator With Functor - HolmesJJ/OOP-FP GitHub Wiki

Input.java

/**
 * Class Input reads a line from the system console (System.in).
 * It displays a numbered prompt.
 */

import java.util.Scanner;

public class Input {

    // Static members and methods
    private static int lineNo = 0;
    static Scanner sc = new Scanner(System.in).useDelimiter("\\n");

    public static Input readInput() {
        return new Input();
    }

    // Instance members and methods
    private final String line;

    private Input() {
        lineNo++;
        System.out.printf("[%d]=> ", lineNo);
        this.line = sc.next();
    }


    public String getLine() {
        return this.line;
    }

    public int getLineNum() {
        return this.lineNo;
    }
}

Result.java

/**
 * This class serves only to store the result of the calculation
 * in the form of a message string. A display method is provided
 * to print the message to console (System.out).
 */

public class Result {
    private final String message;

    public Result(String m) {
        this.message = m;
    }

    public void display() {
        System.out.println(message);
    }
}

Statement.java

/**
 Class Statement is the workhorse of the calculator.
 It initializes the hashtable to store the command and its associated
 lambda function. To run the command, its name is used to lookup the table
 to retrieve the lambda, which is then applied to the arguments.

 There are only 2 places to detect for problems:
 i. when the command given is unknown
 ii. when there are too many arguments

 In both cases we simply throw an exception. We no longer have to
 handle exceptions thrown by the scanner.

 This class is immutable: we never change the state; instead,
 we return a new instance containing the new state.
 */

import java.util.Scanner;
import java.util.function.Function;
import java.util.Hashtable;

public class Statement {

    // static members and methods

    // This stores the function for each command
    private static Hashtable<String, Function<Double[],Double>> commandTable;

    // This stores the number of arguments for each command
    private static Hashtable<String, Integer> argsTable;

    // Initialization
    public static void initialize() {

        commandTable = new Hashtable<>();
        argsTable = new Hashtable<>();

        /**
         * Addition
         * Accept a double array with 2 elements = 2 arguments
         * Example: add 1 2
         */
        commandTable.put("add", x -> x[0] + x[1]);
        argsTable.put("add", 2);

        /**
         * Multiplication
         * Accept a double array with 2 elements = 2 arguments
         * Example: mult 3 4
         */
        commandTable.put("mult", x -> x[0] * x[1]);
        argsTable.put("mult", 2);

        /**
         * Reciprocal
         * Accept a double array with 1 element = 1 argument
         * Example: recip 5
         */
        commandTable.put("recip", x -> 1.0 / x[0]);
        argsTable.put("recip", 1);

        /**
         * Percentage after Multiplication
         * Accept a double array with 2 elements = 2 arguments
         * Example: % 6 7
         */
        commandTable.put("%", x -> x[0] * x[1] / 100.0);
        argsTable.put("%", 2);
    }

    // Instance member and methods
    private final String command;
    private final Double[] arguments;
    private final Scanner scanner;

    public Statement(Input input) {
        this(new Scanner(input.getLine()), null, null);
    }

    private Statement(Scanner sc, String c, Double[] args) {
        this.scanner = sc;
        this.command = c;
        this.arguments = args;
    }

    // Parse command
    public Statement parseCommand() {
        String c = scanner.next();
        // If the command is not err, add, mult, recip or %
        // Change all the error messages to throw Exceptions
        // unknown command
        if (!commandTable.containsKey(c)) {
            throw new RuntimeException(c);
        }
        return new Statement(this.scanner, c, null);
    }

    // Parse arguments
    public Statement parseArguments() {
	    int numberArgs = argsTable.get(this.command);
	    Double[] args = new Double[numberArgs];
	    for (int i = 0; i < numberArgs; i++) {
	        args[i] = scanner.nextDouble();
	    }
        // Change all the error messages to throw Exceptions
        // too many arguments
        if (scanner.hasNext()) {
            throw new IllegalArgumentException(String.format("Too many arguments: %s", scanner.nextLine()));
        }
        return new Statement(this.scanner, this.command, args);
    }

    public Result evaluate() {
        // commandTable.get(this.command): Function object
        // this.arguments: Double[] object
        double r = commandTable.get(this.command).apply(this.arguments);
        return new Result(String.format("%f", r));
    }
}

Sendbox.java

/**
 * Class Sandbox is a Functor, and is an immutable class.
 * It's purpose is to separate exception handling from the main
 * sequence of mapping.
 */

import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Consumer;
import java.util.Objects;

public final class Sandbox<T> {

    private final T thing;
    private final Exception exception;

    private Sandbox(T thing, Exception ex) {
        this.thing = thing;
        this.exception = ex;
    }

    public T getThing() {
        return this.thing;
    }

    public static <T> Sandbox<T> make(T thing) {
        // The instantiated object cannot be null
        return new Sandbox<T>(Objects.requireNonNull(thing), null);
    }

    public static <T> Sandbox<T> makeEmpty() {
        return new Sandbox<T>(null, null);
    }

    public boolean isEmpty() {
        return this.thing == null;
    }

    public boolean hasNoException() {
        return this.exception == null;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof Sandbox) {
            Sandbox<?> other = (Sandbox<?>) obj;
            return this.thing.equals(other.thing) &&
                    this.exception.equals(other.exception);
        }
        return false;
    }

    public <U> Sandbox<U> map(Function<T, U> f) {
        return map(f, "");
    }

    // return new Sandbox Object
    public <U> Sandbox<U> map(Function<T, U> f, String errorMessage) {
        if (this.isEmpty()) {
            return new Sandbox<U>(null, this.exception);
        }
        try {
            U result = f.apply(this.thing);
            return new Sandbox<U>(result, null);
        }
        // If result throws exception, such as unknown command, too many arguments
        catch (Exception ex) {
            return new Sandbox<U>(null,
                                  new Exception(errorMessage, ex));
        }
    }

    /**
     * consume calls the Consumer argument with the thing.
     * Exceptions, if any, are handled here.
     */
    public void consume(Consumer<T> eat) {
        if (this.isEmpty() && !this.hasNoException())
            handleException();

        if (!this.isEmpty() && this.hasNoException())
            eat.accept(this.thing);
    }

    private void handleException() {
        System.err.printf(this.exception.getMessage());
        Throwable t = this.exception.getCause();
        if (t != null) {
            String msg = t.getMessage();
            if (msg != null)
                System.err.printf(": %s", msg);
        }
        System.err.println("");
    }

    public <U> Sandbox<U> flatMap(Function<T, Sandbox<U>> f) {
        return flatMap(f, "");
    }

    public <U> Sandbox<U> flatMap(Function<T, Sandbox<U>> f, String errorMessage) {
        if (this.isEmpty()) {
            return new Sandbox<U>(null, this.exception);
        }
        try {
            return f.apply(this.thing);
        }
        // If result throws exception
        catch (Exception ex) {
            return new Sandbox<U>(null,
                    new Exception(errorMessage, ex));
        }
    }

    public <U,R> Sandbox<R> combine(Sandbox<U> s,
                                    BiFunction<T,U,R> binOp) {
        return this.flatMap(t1 ->
                s.map(t2 -> binOp.apply(t1, t2)));
    }
}

Validator.java

/**
 * Class Validator is a Functor, and is an immutable class.
 * It's purpose is to separate exception handling from the main
 * sequence of mapping.
 */

import java.util.function.Function;
import java.util.function.Consumer;
import java.util.Objects;

public final class Validator<T> {

    private final T thing;
    private final Exception exception;

    private Validator(T thing, Exception ex) {
        this.thing = thing;
        this.exception = ex;
    }
    
    public static <T> Validator<T> make(T thing) {
        return new Validator<T>(Objects.requireNonNull(thing), null);
    }

    public static <T> Validator<T> makeEmpty() {
        return new Validator<T>(null, null);
    }

    public boolean isEmpty() {
        return this.thing == null;
    }

    public boolean hasNoException() {
        return this.exception == null;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof Validator) {
            Validator<?> other = (Validator<?>) obj;
            return this.thing == other.thing &&
                    this.exception == other.exception;
        }
        return false;
    }

    public <U> Validator<U> map(Function<T,U> f) {
        return map(f, "");
    }
        
    public <U> Validator<U> map(Function<T,U> f, String errorMessage) {
        if (isEmpty()) {
            return new Validator<U>(null, this.exception);
        }
        try {
            return new Validator<U>(f.apply(thing), null);
        } catch (Exception ex) {
            return new Validator<U>(null,
                    new Exception(errorMessage, ex));
        }
    }

    /**
     * consume calls the Consumer argument with the thing.
     * Exceptions, if any, are handled here.
     */
    public void consume(Consumer<T> eat) {
        if (isEmpty() && hasNoException()) {
            return;
        }
        if (isEmpty() && !hasNoException()) {
            handleException();
        }
        if (!isEmpty() && hasNoException()) {
            eat.accept(thing); // eating may throw exception
        }
    }

    private void handleException() {
        System.err.printf(this.exception.getMessage());
        Throwable t = this.exception.getCause();
        if (t != null) {
            String msg = t.getMessage();
            if (msg != null)
                System.err.printf(": %s", msg);
        }
        System.err.println("");
    }
}

CalcM.java

import java.util.NoSuchElementException;
import java.util.function.Consumer;
import java.util.function.Function;

public class CalcM {
    public static void main(String[] args) {
        // Initialize the calculator
        Statement.initialize();
        try {
            while (true) {
                /**
                 * Instantiate new Sandbox Object with new Input Object
                 *
                 * What Sandbox.make(Input.readInput()) is actually doing:
                 * new Sandbox<Input>(new Input(), null);
                 */
                Sandbox.make(Input.readInput())
                    /**
                     * Instantiate new Sandbox Object with new Statement Object
                     *
                     * Further simplify: .map(Statement::new)
                     *
                     * What Sandbox.make(Input.readInput()).map(Statement::new) is actually doing:
                     * new Sandbox<Statement>(new Statement(new Input()), null);
                     */
                    .map(new Function<Input, Statement>() {
                        @Override
                        public Statement apply(Input input) {
                            return new Statement(input);
                        }
                    })
                    /**
                     * Check command first
                     * Instantiate new Sandbox Object with new Statement Object
                     *
                     * Further simplify: .map(Statement::parseCommand, "Bad command!")
                     *
                     * What Sandbox.make(Input.readInput())
                     *             .map(Statement::new)
                     *             .map(Statement::parseCommand, "Bad command!")
                     * is actually doing:
                     * try {
                     *     Statement newStatement = new Statement(new Input()).parseCommand();
                     *     new Sandbox<Statement>(newStatement, null);
                     * } catch (RuntimeException ex) {
                     *     new Sandbox<Statement>(null, new Exception("Bad command!", ex));
                     * }
                     */
                    .map(new Function<Statement, Statement>() {
                        @Override
                        public Statement apply(Statement statement) {
                            return statement.parseCommand();
                        }
                    }, "Bad command!")
                    /**
                     * Then check arguments
                     * Instantiate new Sandbox Object with new Statement Object
                     *
                     * Further simplify: .map(Statement::parseArguments, "Bad arguments!")
                     *
                     * What Sandbox.make(Input.readInput())
                     *             .map(Statement::new)
                     *             .map(Statement::parseCommand, "Bad command!")
                     *             .map(Statement::parseArguments, "Bad arguments!")
                     * is actually doing:
                     * try {
                     *     Statement newStatement = new Statement(new Input()).parseArguments();
                     *     new Sandbox<Statement>(newStatement, null);
                     * } catch (RuntimeException ex) {
                     *     new Sandbox<Statement>(null, new Exception("Bad arguments!", ex));
                     * }
                     */
                    .map(new Function<Statement, Statement>() {
                        @Override
                        public Statement apply(Statement statement) {
                            return statement.parseArguments();
                        }
                    }, "Bad arguments!")
                    /**
                     * Calculate the result
                     */
                    .map(new Function<Statement, Result>() {
                        @Override
                        public Result apply(Statement statement) {
                            return statement.evaluate();
                        }
                    })
                    /**
                     * Display the result
                     */
                    .consume(new Consumer<Result>() {
                        @Override
                        public void accept(Result result) {
                            result.display();
                        }
                    });

                /**
                 * Further simplify:
                 */
                Sandbox.make(Input.readInput())
                    .map(Statement::new)
                    .map(Statement::parseCommand, "Bad command!")
                    .map(Statement::parseArguments, "Bad arguments!")
                    .map(Statement::evaluate)
                    .consume(Result::display);
            }
        } catch (NoSuchElementException e) {
            //break out of while loop; end program
        }
    }

    public static void flatMapExample() {
        Sandbox<Integer> s1 = Sandbox.make(3);
        Sandbox<Integer> s2 = Sandbox.make(5);
        /**
         * What s2.map(t2 -> t1 + t2) is actually doing:
         * new Sandbox<Integer>(3 + 5, null);
         */
        Sandbox<Integer> s3 = s1.flatMap(new Function<Integer, Sandbox <Integer>>() {
            @Override
            public Sandbox<Integer> apply(Integer t1) {
                return s2.map(new Function<Integer, Integer> () {
                    @Override
                    public Integer apply(Integer t2) {
                        return t1 + t2;
                    }
                });
            }
        });
        /**
         * Further simplify:
         */
        Sandbox<Integer> s4 = s1.flatMap(t1 - > s2.map(t2 - > t1 + t2));
        System.out.println(s1.getThing());
        System.out.println(s2.getThing());
        System.out.println(s3.getThing());
        System.out.println(s4.getThing());
    }
}
⚠️ **GitHub.com Fallback** ⚠️