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}