001/* 002 * Copyright (c) 2012-2017 Institut National des Sciences Appliquées de Lyon (INSA-Lyon) 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 */ 009 010package gololang; 011 012import java.lang.invoke.MethodHandle; 013import java.lang.invoke.MethodHandles; 014import java.lang.invoke.MethodType; 015import java.util.Arrays; 016 017import static java.lang.invoke.MethodHandles.filterReturnValue; 018 019/** 020 * A reference to a function / closure. 021 * 022 * This class essentially boxes {@code MethodHandle} references, and provides as many delegations as possible. 023 * Previous versions of Golo used direct {@code MethodHandle} objects to deal with functions by reference, but that 024 * class does not provide any mean to attach local state, as required for, say, implementing named arguments. 025 * 026 * This boxed representation provides a sound abstraction while not hurting performance, as 027 * {@code org.eclipse.golo.runtime.ClosureCallSupport} still dispatches through a method handle. 028 * 029 * @see java.lang.invoke.MethodHandle 030 * @see org.eclipse.golo.runtime.ClosureCallSupport 031 */ 032public class FunctionReference { 033 034 private final MethodHandle handle; 035 036 private final String[] parameterNames; 037 038 /** 039 * Makes a function reference from a method handle. 040 * 041 * @param handle the method handle. 042 * @param parameterNames the target method parameter's names. 043 * @throws IllegalArgumentException if {@code handle} is {@code null}. 044 */ 045 public FunctionReference(MethodHandle handle, String[] parameterNames) { 046 if (handle == null) { 047 throw new IllegalArgumentException("A method handle cannot be null"); 048 } 049 this.handle = handle; 050 this.parameterNames = parameterNames; 051 } 052 053 /** 054 * Makes a function reference from a method handle. 055 * The parameter names will be {@code null}. 056 * 057 * @param handle the method handle. 058 * @throws IllegalArgumentException if {@code handle} is {@code null}. 059 */ 060 public FunctionReference(MethodHandle handle) { 061 this(handle, null); 062 } 063 064 /** 065 * Unboxes the method handle. 066 * 067 * @return the (boxed) method handle. 068 */ 069 public MethodHandle handle() { 070 return handle; 071 } 072 073 /** 074 * Get the target function parameter's names 075 * 076 * @return the array of parameter's names 077 */ 078 public String[] parameterNames() { 079 return Arrays.copyOf(parameterNames, parameterNames.length); 080 } 081 082 public MethodType type() { 083 return handle.type(); 084 } 085 086 public FunctionReference asCollector(Class<?> arrayType, int arrayLength) { 087 return new FunctionReference(handle.asCollector(arrayType, arrayLength), this.parameterNames); 088 } 089 090 public FunctionReference asCollector(int arrayLength) { 091 return asCollector(Object[].class, arrayLength); 092 } 093 094 public FunctionReference asFixedArity() { 095 return new FunctionReference(handle.asFixedArity(), this.parameterNames); 096 } 097 098 public FunctionReference asType(MethodType newType) { 099 return new FunctionReference(handle.asType(newType), this.parameterNames); 100 } 101 102 public FunctionReference asVarargsCollector(Class<?> arrayType) { 103 return new FunctionReference(handle.asVarargsCollector(arrayType), this.parameterNames); 104 } 105 106 public FunctionReference asVarargsCollector() { 107 return asVarargsCollector(Object[].class); 108 } 109 110 public FunctionReference bindTo(Object x) { 111 return new FunctionReference(handle.bindTo(x), dropParameterNames(0, 1)); 112 } 113 114 public boolean isVarargsCollector() { 115 return handle.isVarargsCollector(); 116 } 117 118 public FunctionReference asSpreader(Class<?> arrayType, int arrayLength) { 119 return new FunctionReference(handle.asSpreader(arrayType, arrayLength)); 120 } 121 122 public FunctionReference asSpreader(int arrayLength) { 123 return asSpreader(Object[].class, arrayLength); 124 } 125 126 public FunctionReference asSpreader() { 127 return asSpreader(Object[].class, arity()); 128 } 129 130 /** 131 * Returns the arity of the function. 132 * 133 * The arity is the number of declared parameter in the function signature. 134 * 135 * @return the number of declared parameter 136 */ 137 public int arity() { 138 return handle.type().parameterCount(); 139 } 140 141 /** 142 * Check if this function can be invoked with the given number of arguments. 143 */ 144 public boolean acceptArity(int nb) { 145 return arity() == nb || (arity() == nb + 1 && isVarargsCollector()); 146 } 147 148 public Object invoke(Object... args) throws Throwable { 149 return handle.invokeWithArguments(args); 150 } 151 152 /** 153 * Apply the function to the provided arguments. 154 * 155 * If the number of arguments corresponds to the function arity, the function is applied. 156 * Otherwise, a function partialized with the given arguments is returned. 157 * @return the result of the function or a partialized version of the function 158 */ 159 public Object invokeOrBind(Object... args) throws Throwable { 160 if (args.length < arity()) { 161 return insertArguments(0, args); 162 } 163 return handle.invokeWithArguments(args); 164 } 165 166 @Override 167 public String toString() { 168 return "FunctionReference{" + 169 "handle=" + (handle.isVarargsCollector() ? "(varargs)" : "") + handle + 170 ", parameterNames=" + Arrays.toString(parameterNames) + 171 '}'; 172 } 173 174 @Override 175 public boolean equals(Object obj) { 176 if (this == obj) { 177 return true; 178 } 179 if (obj == null || getClass() != obj.getClass()) { 180 return false; 181 } 182 FunctionReference that = (FunctionReference) obj; 183 return handle.equals(that.handle); 184 } 185 186 @Override 187 public int hashCode() { 188 return handle.hashCode(); 189 } 190 191 /** 192 * Converts a function reference to an instance of an interface. 193 * 194 * @param interfaceClass the interface, 195 * @return a proxy object that satisfies {@code interfaceClass} and delegates to {@code this}. 196 */ 197 public Object to(Class<?> interfaceClass) { 198 return Predefined.asInterfaceInstance(interfaceClass, this); 199 } 200 201 /** 202 * Compose a function with another function. 203 * 204 * @param fun the function that processes the results of {@code this} function. 205 * @return a composed function. 206 */ 207 public FunctionReference andThen(FunctionReference fun) { 208 MethodHandle other = null; 209 if (fun.isVarargsCollector() && fun.arity() == 1) { 210 other = fun.handle.asCollector(Object[].class, 1); 211 } else if (fun.isVarargsCollector() && fun.arity() == 2) { 212 other = MethodHandles.insertArguments(fun.handle, 1, new Object[]{new Object[0]}); 213 } else if (fun.arity() == 1) { 214 other = fun.handle; 215 } else { 216 throw new IllegalArgumentException("`andThen` requires a function that can be applied to 1 parameter"); 217 } 218 return new FunctionReference(filterReturnValue(this.handle, other), this.parameterNames); 219 } 220 221 /* 222 * Compose a function with another function. 223 * 224 * <p>This is equivalent to {@code fun.andThen(this)}. 225 * 226 * @param fun the function to apply before {@code this} function. 227 * @return a composed function. 228 */ 229 public FunctionReference compose(FunctionReference fun) { 230 if (!acceptArity(1)) { 231 throw new UnsupportedOperationException("`compose` must be called on function accepting 1 parameter"); 232 } 233 return fun.andThen(this); 234 } 235 236 /** 237 * Partial application. 238 * 239 * @param position the argument position (0-indexed). 240 * @param value the argument value. 241 * @return a partially applied function. 242 */ 243 public FunctionReference bindAt(int position, Object value) { 244 return new FunctionReference(MethodHandles.insertArguments(this.handle, position, value), dropParameterNames(position, 1)); 245 } 246 247 /** 248 * Partial application based on parameter's names. 249 * 250 * @param parameterName the parameter to bind. 251 * @param value the argument value. 252 * @return a partially applied function. 253 */ 254 public FunctionReference bindAt(String parameterName, Object value) { 255 int position = -1; 256 if (this.parameterNames == null) { 257 throw new RuntimeException("Can't bind on parameter name, " + this.toString() + " has none"); 258 } 259 for (int i = 0; i < this.parameterNames.length; i++) { 260 if (this.parameterNames[i].equals(parameterName)) { 261 position = i; 262 break; 263 } 264 } 265 if (position == -1) { 266 throw new IllegalArgumentException("'" + parameterName + "' not in the parameter list " + Arrays.toString(parameterNames)); 267 } 268 return bindAt(position, value); 269 } 270 271 /** 272 * Partial application. 273 * 274 * @param position the first argument position. 275 * @param values the values of the arguments from {@code position}. 276 * @return a partially applied function. 277 * @see java.lang.invoke.MethodHandles#insertArguments(MethodHandle, int, Object...) 278 */ 279 public FunctionReference insertArguments(int position, Object... values) { 280 if (values.length == 0) { 281 return this; 282 } 283 MethodHandle bounded = MethodHandles.insertArguments(handle, position, values); 284 if (handle.isVarargsCollector()) { 285 bounded = bounded.asVarargsCollector(Object[].class); 286 } 287 return new FunctionReference(bounded, dropParameterNames(position, values.length)); 288 } 289 290 /** 291 * Spread arguments over this function parameters. 292 * 293 * @param arguments arguments as an array. 294 * @return a return value. 295 * @throws Throwable ...because an exception can be thrown. 296 */ 297 public Object spread(Object... arguments) throws Throwable { 298 int arity = arity(); 299 if (this.handle.isVarargsCollector() && (arity > 0) && (arguments[arity - 1] instanceof Object[])) { 300 return this.handle 301 .asFixedArity() 302 .asSpreader(Object[].class, arguments.length) 303 .invoke(arguments); 304 } 305 return this.handle 306 .asSpreader(Object[].class, arguments.length) 307 .invoke(arguments); 308 } 309 310 private String[] dropParameterNames(int from, int size) { 311 if (this.parameterNames == null) { 312 return null; 313 } 314 String[] filtered = new String[this.parameterNames.length - size]; 315 if (filtered.length > 0) { 316 System.arraycopy(parameterNames, 0, filtered, 0, from); 317 System.arraycopy(parameterNames, from + size, filtered, from, this.parameterNames.length - size - from); 318 } 319 return filtered; 320 } 321}