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}