Exit Full View

Feather2 / feather2-runtime / src / main / java / uk / co / nickthecoder / feather / runtime / Function.java

package uk.co.nickthecoder.feather.runtime;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * Feather uses this to hold function objects.
 * i.e. functions are first class citizens in Feather (they aren't in Java).
 * You can assign a function to a variable, and pass functions into other functions/methods.
 * <p>
 * Parameter types A1..A9 are the function's parameter types.
 * For functions with fewer than 9 parameters, the later ones are of type [Void].
 * <p>
 * Note that functions with more than 9 arguments are not supported.
 * This means methods with more than 8 arguments are not supported,
 * as the first argument will be the object whose method we are calling.
 * <p>
 * R is the return type.
 * <p>
 * For example, the type : .
 * (String->int)
 * Will be of type :
 * Function<String, Void, Void..., Integer )
 * <p>
 * Primitive types ARE allowed in A1..9 and R. This is kind of illegal, but type erasure
 * means that the generic type won't be in the compiled code ;-)
 * <p>
 * NOTE, This is not in package uk.co.nickthecoder.feather.runtime, because that package
 * is safe to include in Feather's sandbox, whereas this classes may not be safe.
 */
public class Function<R, A1, A2, A3, A4, A5, A6, A7, A8, A9> {
    private final Method method;
    private final Object[] curriedArgs;

    /**
     * Called by FunctionStackEntry, which also looks up the [Method] in hard-coded bytecode.
     */
    public Function(Method method) {
        this.method = method;
        curriedArgs = new Object[0];
    }

    /**
     * Called by [curry], from an existing [Function] instance, so we already know the [Method].
     */
    private Function(Method method, Object[] curriedArgs) {
        this.method = method;
        this.curriedArgs = curriedArgs;
    }


    /**
     * Invokes (calls) the function.
     * If this is a curried function, then the [Method] will have more parameters than
     * the length of [args].
     * <p>
     * If [method] is not static, then the first argument (from the join of curriedArgs and arg)
     * is the receiver, and the remainder are the method's arguments.
     */
    public R invoke(Object... args) throws InvocationTargetException, IllegalAccessException {

        if (Modifier.isStatic(method.getModifiers())) {
            int parameterCount = method.getParameterCount();
            int expectedArgCount = parameterCount - curriedArgs.length;

            if (expectedArgCount != args.length) {
                throw new IllegalArgumentException("Expected " + expectedArgCount + " but found " + args.length);
            }

            Object[] allArgs;
            if (curriedArgs.length == 0) {
                allArgs = args;
            } else {
                allArgs = Arrays.copyOf(curriedArgs, parameterCount);
                // Copy args into allArgs after the curried args.
                System.arraycopy(args, 0, allArgs, curriedArgs.length, args.length);
            }
            @SuppressWarnings("unchecked")
            R result = (R) method.invoke(null, allArgs);
            return result;

        } else {
            int parameterCount = method.getParameterCount();
            int expectedArgCount = parameterCount + 1 - curriedArgs.length;

            if (expectedArgCount != args.length) {
                throw new IllegalArgumentException("Expected " + expectedArgCount + " arguments but found " + args.length);
            }

            Object[] allArgs; // Not including the receiver
            Object receiver;
            if (curriedArgs.length == 1) {
                receiver = curriedArgs[0];
                allArgs = args;
            } else if (curriedArgs.length == 0) {
                receiver = args[0];
                allArgs = new Object[method.getParameterCount()];
                System.arraycopy(args, 1, allArgs, 0, args.length - 1);
            } else {
                receiver = curriedArgs[0];
                allArgs = new Object[method.getParameterCount()];
                System.arraycopy(curriedArgs, 1, allArgs, 0, curriedArgs.length - 1);
                System.arraycopy(args, 0, allArgs, curriedArgs.length - 1, args.length);
            }
            @SuppressWarnings("unchecked")
            R result = (R) method.invoke(receiver, allArgs);
            return result;
        }
    }

    /**
     * Curries the function. If you don't know what that means google "curried functions" ;-)
     */
    public Function<R, A2, A3, A4, A5, A6, A7, A8, A9, Void> curry(A1 arg) {
        int newCurryLength = curriedArgs.length + 1;
        int requiredArgCount = method.getParameterCount();
        if (!Modifier.isStatic(method.getModifiers())) {
            ++requiredArgCount;
        }
        if (requiredArgCount < newCurryLength) {
            throw new IllegalStateException("Attempted to curry a function with no parameters");
        }
        Object[] newCurriedArgs = Arrays.copyOf(curriedArgs, newCurryLength);
        newCurriedArgs[newCurryLength - 1] = arg;
        return new Function<R, A2, A3, A4, A5, A6, A7, A8, A9, Void>(method, newCurriedArgs);
    }

    /**
     * Curry two arguments
     */
    public Function<R, A3, A4, A5, A6, A7, A8, A9, Void, Void> curry(A1 arg1, A2 arg2) {
        return curry(arg1).curry(arg2);
    }

    /**
     * Curry three arguments
     */
    public Function<R, A4, A5, A6, A7, A8, A9, Void, Void, Void> curry(A1 arg1, A2 arg2, A3 arg3) {
        return curry(arg1).curry(arg2).curry(arg3);
    }

    /**
     * Curry four arguments
     */
    public Function<R, A5, A6, A7, A8, A9, Void, Void, Void, Void> curry(A1 arg1, A2 arg2, A3 arg3, A4 arg4) {
        return curry(arg1).curry(arg2).curry(arg3).curry(arg4);
    }

    /**
     * Curry five arguments
     */
    public Function<R, A6, A7, A8, A9, Void, Void, Void, Void, Void> curry(A1 arg1, A2 arg2, A3 arg3, A4 arg4, A5 arg5) {
        return curry(arg1).curry(arg2).curry(arg3).curry(arg4).curry(arg5);
    }
}