package org.enumerable.lambda; import org.enumerable.lambda.annotation.LambdaLocal; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.*; import java.util.ArrayList; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import java.util.regex.Pattern; import static java.lang.Boolean.FALSE; import static org.enumerable.lambda.exception.UncheckedException.uncheck; /** * A function that takes no arguments. */ @SuppressWarnings("serial") public abstract class Fn0<R> implements Serializable { /** * Creates a constant function always returning the provided value. */ public static <R> Fn0<R> constant(final R value) { return new Fn0<R>() { public R call() { return value; } }; } public static boolean isNotFalseOrNull(Object obj) { return obj != FALSE && obj != null; } public static boolean isFalseOrNull(Object result) { return !isNotFalseOrNull(result); } public static int getAndCheckArityForMethod(Class<?> aClass, String methodName) { int basicArity = 0; for (Method method : aClass.getDeclaredMethods()) if (method.getName().equals(methodName)) basicArity = method.getParameterTypes().length; SortedSet<Integer> defaultValues = new TreeSet<Integer>(); for (Method method : aClass.getDeclaredMethods()) if (method.getName().startsWith("default$")) defaultValues.add(Integer.valueOf(method.getName().substring("default$".length()))); boolean consecutive = true; if (!defaultValues.isEmpty()) { int lastIndex = -1; for (int index : defaultValues) { if (lastIndex > 0) consecutive = index == lastIndex + 1; lastIndex = index; } if (lastIndex != basicArity || !consecutive) throw new IllegalArgumentException("parameter " + lastIndex + " cannot have a default value when there are parameters follwing without, arity is " + basicArity); return -(basicArity - defaultValues.size() + 1); } return basicArity; } public Fn0() { getAndCheckArityForMethod(getClass(), "call"); } public abstract R call(); /** * Applies args to this function, padded with null if needed to match the * number of arguments this function takes. */ public R apply(Object... args) { return call(); } /** * Wraps this function in an interface using a {@link Proxy}, and forwards * all calls to {@link #apply(Object...)}. */ @SuppressWarnings("unchecked") public <I> I as(Class<I> anInterface) { return (I) Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[] { anInterface }, new ApplyMethodInvocationHandler(".*", new Class<?>[0])); } /** * Wraps this function in an interface using a {@link Proxy}, and forwards * calls with names matching the regular expression and arguments matching * the parameter types to {@link #apply(Object...)}. */ @SuppressWarnings("unchecked") public <I> I as(Class<I> anInterface, String regex, Class<?>... parameterTypes) { return (I) Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[] { anInterface }, new ApplyMethodInvocationHandler(regex, parameterTypes)); } public int arity() { return 0; } class ApplyMethodInvocationHandler implements InvocationHandler { Pattern pattern; Class<?>[] parameterTypes; ApplyMethodInvocationHandler(String regex, Class<?>[] parameterTypes) { this.parameterTypes = parameterTypes; this.pattern = Pattern.compile(regex); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (!pattern.matcher(method.getName()).matches()) return null; for (int i = 0; i < parameterTypes.length; i++) { Class<?> type = parameterTypes[i]; if (args[i] != null && !type.isAssignableFrom(args[i].getClass())) return null; } return apply(args != null ? args : new Object[0]); } } public class Binding { /** * Gets the current value of a variable that was captured when creating * this lambda. */ public Object get(String name) { try { Field field = findField(name); if (field == null) return null; Object value = field.get(Fn0.this); if (!field.getAnnotation(LambdaLocal.class).isReadOnly()) value = Array.get(value, 0); return value; } catch (Exception e) { throw uncheck(e); } } /** * Sets the value of a variable that was captured and when creating this * lambda. Only works if the variable is changed somewhere in the * original method body or inside the lambda expression. */ public Object set(String name, Object value) { try { Field field = findField(name); if (field == null) throw new IllegalArgumentException("No such variable " + name + " in " + Fn0.this); if (field.getAnnotation(LambdaLocal.class).isReadOnly()) throw new IllegalArgumentException("Variable " + name + " " + field.getType().getName() + " is not modifiable from " + Fn0.this); Object array = field.get(Fn0.this); Object previousValue = Array.get(array, 0); Array.set(array, 0, value); return previousValue; } catch (Exception e) { throw uncheck(e); } } Field findField(String name) { for (Field field : Fn0.this.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(LambdaLocal.class)) { LambdaLocal lambdaLocal = field.getAnnotation(LambdaLocal.class); if (name.equals(lambdaLocal.name())) { field.setAccessible(true); return field; } } } return null; } } /** * Returns a limited execution context of this function. It contains local * variables (including the outer this) and parameters that are captured. */ public Binding binding() { return new Binding(); } public String toString() { return getClass().getName(); } /** * Returns a function that will give the opposite boolean value of this * function. Uses null as false when used with non boolean functions. */ public Fn0<Boolean> complement() { return new Fn0<Boolean>() { public Boolean call() { return isFalseOrNull(Fn0.this.call()); } }; } /** * Executes this function if the argument is false. */ public R unless(boolean test) { if (!test) return call(); return null; } /** * Calls this function repeatedly with no arguments and executes the given * block while the result is true or non null. */ public <B> B whileTrue(Fn0<B> block) { B result = null; while (isNotFalseOrNull(call())) result = block.call(); return result; } /** * Calls this function with no arguments and executes the given block if the * result is true or non null. */ public <B> B ifTrue(Fn0<B> block) { if (isNotFalseOrNull(call())) return block.call(); return null; } /** * Calls this function with no arguments and executes the given block if the * result is false or null. */ public <B> B ifFalse(Fn0<B> block) { if (isFalseOrNull(call())) return block.call(); return null; } /** * Returns true if both this function and the given block evaluates to true * of non null. */ public <B> boolean and(Fn0<B> block) { return isNotFalseOrNull(call()) && isNotFalseOrNull(block.call()); } /** * Returns true if either this function or the given block evaluates to true * of non null. */ public <B> boolean or(Fn0<B> block) { return isNotFalseOrNull(call()) || isNotFalseOrNull(block.call()); } public static Method getLambdaMethod(Class<?> aClass) { Method[] methods = aClass.getDeclaredMethods(); if (methods.length != 1) throw new IllegalArgumentException(aClass.getName() + " has more than one declared method"); return methods[0]; } public Method getLambdaMethod() { return getLambdaMethod(getClass()); } public List<LambdaLocal> getParameters() { Method lambdaMethod = getLambdaMethod(getClass()); List<LambdaLocal> parameters = new ArrayList<LambdaLocal>(); Annotation[][] annotations = lambdaMethod.getParameterAnnotations(); for (Annotation[] annotation : annotations) parameters.add((LambdaLocal) annotation[0]); return parameters; } public List<Field> getParameterFields() { try { List<Field> result = new ArrayList<Field>(); for (LambdaLocal parameter : getParameters()) result.add(getClass().getClassLoader().loadClass(parameter.parameterClass()).getField(parameter.name())); return result; } catch (Exception e) { throw uncheck(e); } } }