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