All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.protelis.Builtins Maven / Gradle / Ivy

There is a newer version: 17.6.0
Show newest version
/*
 * Copyright (C) 2022, Danilo Pianini and contributors listed in the project's build.gradle.kts file.
 *
 * This file is part of Protelis, and is distributed under the terms of the GNU General Public License,
 * with a linking exception, as described in the file LICENSE.txt in this project's top directory.
 */

package org.protelis;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import org.protelis.lang.datatype.DatatypeFactory;
import org.protelis.lang.datatype.DeviceUID;
import org.protelis.lang.datatype.Field;
import org.protelis.lang.datatype.FunctionDefinition;
import org.protelis.lang.datatype.Option;
import org.protelis.lang.datatype.Tuple;
import org.protelis.vm.ExecutionContext;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.IntPredicate;

import static org.protelis.lang.interpreter.util.JavaInteroperabilityUtils.runProtelisFunctionWithJavaArguments;

/**
 * Collection of static methods automatically imported by Protelis.
 */
public final class Builtins {

    private static final String UNCHECKED = "unchecked";
    /**
     * This variable is used by the interpreter for providing compatibility hints in the Eclipse plugin.
     * See https://github.com/Protelis/Protelis/issues/245.
     */
    public static final ImmutableList MINIMUM_PARSER_VERSION = ImmutableList.of(11, 0, 0);
    /**
     * This variable is used by the interpreter for providing compatibility hints in the Eclipse plugin.
     * See https://github.com/Protelis/Protelis/issues/245.
     */
    public static final ImmutableList MAXIMUM_PARSER_VERSION = ImmutableList.of(11, 1, 0);

    private Builtins() { }

    /**
     * @param target the field
     * @return true if all the elements of the field are true
     */
    public static boolean all(final Field target) {
        return target.foldValuesIncludingLocal(Boolean::logicalAnd);
    }

    /**
     * @param target the field
     * @return true if all the elements of the field are true. The local element is
     *         not considered.
     */
    public static boolean allButSelf(final Field target) {
        return target.foldValuesExcludingLocal(true, Boolean::logicalAnd);
    }

    /**
     * @param target the field
     * @return true if any of the elements of the field is true
     */
    public static boolean any(final Field target) {
        return target.foldValuesIncludingLocal(Boolean::logicalOr);
    }

    /**
     * @param target the field
     * @return true if any of the elements of the field are true. The local element
     *         is not considered.
     */
    public static boolean anyButSelf(final Field target) {
        return target.foldValuesExcludingLocal(false, Boolean::logicalOr);
    }

    @SuppressWarnings(UNCHECKED)
    private static  R byReflection(final String name, final X a, final Y b) {
        try {
            return (R) a.getClass().getMethod(name, b.getClass()).invoke(a, b);
        } catch (
            IllegalAccessException
             | IllegalArgumentException
             | InvocationTargetException
             | NoSuchMethodException
             | SecurityException e
        ) {
            throw new IllegalStateException(
                "Unable to perform operation a." + name
                    + "(b) where a=" + a + " and b=" + b
                    + ", a of type " + a.getClass()
                    + ", b of type " + b.getClass(),
                e
            );
        }
    }

    private static  Function conversionFunction(final Class in, final Class out) {
        if (in.equals(out) || out.isAssignableFrom(in)) {
            return out::cast;
        }
        throw new IllegalStateException("Impossible conversion between " + in + " and " + out);
    }

    @SuppressWarnings(UNCHECKED)
    private static  O convert(final Class out, final I in) {
        return conversionFunction((Class) in.getClass(), out).apply(in);
    }

    /**
     * Reifies a Field into a Java Map for interoperability and debug purposes.
     *
     * @param field the field to reify
     * @return a map having {@link DeviceUID}s as keys and field values as values
     * @param  the field (and return map values) type
     */
    public static  Map fieldToMap(final Field field) {
        return field.toMap();
    }

    /**
     * Folds a field, excluding the local value. This method requires a base
     * value to begin the computation with. The base value is used as initial
     * element. If the field only contains the local value, then the base value is
     * returned.
     *
     * @param                field and result type
     * @param context           {@link ExecutionContext}
     * @param target            the field to be reduced
     * @param reductionFunction the reduction function
     * @return an {@link Option} with the result of the field reduction, or an empty
     *         option.
     * @param base the base value
     */
    public static  T foldHood(
        @Nonnull final ExecutionContext context,
        @Nonnull final T base,
        @Nonnull final Field target,
        @Nonnull final FunctionDefinition reductionFunction
    ) {
        @SuppressWarnings(UNCHECKED)
        final Class defaultType = (Class) Objects.requireNonNull(base).getClass();
        final Class fieldType = Objects.requireNonNull(target).getExpectedType();
        final Class expectedType;
        if (defaultType.isAssignableFrom(fieldType)) {
            expectedType = defaultType;
        } else if (fieldType.isAssignableFrom(defaultType)) {
            expectedType = fieldType;
        } else {
            throw new IllegalArgumentException("Default type " + defaultType.getName()
                + " and field expected type " + fieldType.getName()
                + " do not seem to be compatible");
        }
        return target.foldValuesExcludingLocal(base, reductionFunction(context, expectedType, target, reductionFunction));
    }

    /**
     * Folds the field, including the local value.
     *
     * @param  field and result type
     * @param context {@link ExecutionContext}
     * @param target target field
     * @param reductionFunction a Protelis function (T, T)=>T
     * @return the folded value
     */
    public static  T foldHoodPlusSelf(
        @Nonnull final ExecutionContext context,
        @Nonnull final Field target,
        @Nonnull final FunctionDefinition reductionFunction
    ) {
        return target.foldValuesIncludingLocal(reductionFunction(context, target.getExpectedType(), target, reductionFunction));
    }

    /**
     * Folds the field, including the local value, by picking the maximum value.
     * This method requires the field elements to implement {@link Comparable}.
     *
     * @param                field and result type
     * @param target            target field
     * @return the maximum value among the field values
     */
    public static > T foldMax(final Field target) {
        return target.foldValuesIncludingLocal(Builtins::max);
    }

    /**
     * Folds a field by picking the maximum of its values, excluding the local
     * value. This method requires a base value to begin the computation with.
     * The base value is used as initial element. If the field only contains the
     * local value, then the base value is returned. This method requires the field
     * elements to implement {@link Comparable}.
     *
     * @param     field and result type
     * @param target the field to be reduced
     * @return the maximum value among the field values and base.
     * @param base the base value
     */
    public static > T foldMax(final T base, final Field target) {
        return target.foldValuesExcludingLocal(base, Builtins::max);
    }

    /**
     * Folds a field of numbers by computing the mathematical mean. Includes the
     * local value.
     *
     * @param target the field to be reduced
     * @return a {@link Double} with the arithmetic mean of the values
     */
    public static double foldMean(final Field target) {
        return foldSum(target).doubleValue() / (target.size() + 1);
    }

    /**
     * Folds the field, including the local value, by picking the minimum value.
     * This method requires the field elements to implement {@link Comparable}.
     *
     * @param                field and result type
     * @param target            target field
     * @return the minimum value among the field values
     */
    public static > T foldMin(final Field target) {
        return target.foldValuesIncludingLocal(Builtins::min);
    }

    /**
     * Folds a field by picking the minimum of its values, excluding the local
     * value. This method requires a base value to begin the computation with.
     * The base value is used as initial element. If the field only contains the
     * local value, then the base value is returned. This method requires the field
     * elements to implement {@link Comparable}.
     *
     * @param                field and result type
     * @param target            the field to be reduced
     * @return the minimum value among the field values and base.
     * @param base the base value
     */
    public static > T foldMin(final T base, final Field target) {
        return target.foldValuesExcludingLocal(base, Builtins::min);
    }

    /**
     * Folds the field, including the local value, by computing the sum.
     * The sum operation must be well defined for the field type.
     *
     * @param                field and result type
     * @param target            target field
     * @return the sum of the field values
     */
    public static  T foldSum(final Field target) {
        return target.foldValuesIncludingLocal(Builtins::sum);
    }

    /**
     * Folds a field by computing the sum of its values, excluding the local
     * value. This method requires a base value to begin the computation with.
     * The base value is used as initial element. If the field only contains the
     * local value, then the base value is returned. The sum operation must be well
     * defined for the field type.
     *
     * @param     field and result type
     * @param target the field to be reduced
     * @return the sum of the field values and base
     * @param base the base value
     */
    public static  T foldSum(final T base, final Field target) {
        return target.foldValuesExcludingLocal(base, Builtins::sum);
    }

    /**
     * Folds the field, including the local value, by computing the union. The union
     * operation must be well defined for the field type, e.g., this operation can
     * work on a field of tuples.
     *
     * @param     field and result type
     * @param target target field
     * @return the union of the field values
     */
    public static  T foldUnion(final Field target) {
        return target.foldValuesIncludingLocal(Builtins::union);
    }

    /**
     * Folds a field by computing the union, excluding the local value. This
     * method requires a base value to begin the computation with. The base value is
     * used as initial element. If the field only contains the local value, then the
     * base value is returned. The union operation must be well defined for the
     * field type, e.g., this operation can work on a field of tuples.
     *
     * @param     field and result type
     * @param target the field to be reduced
     * @return the sum of the field values and base
     * @param base the base value
     */
    public static  T foldUnion(final T base, final Field target) {
        return target.foldValuesExcludingLocal(base, Builtins::union);
    }

    /**
     * Picks the local field value (same operation of the previously available
     * "localHood" and "pickHood").
     *
     * @param     field and result type
     * @param target the field to be reduced
     * @return the local field value
     */
    public static  T local(final Field target) {
        return target.getLocalValue();
    }

    private static > T max(final T a, final T b) {
        return selectComparable(a, b, c -> c > 0);
    }

    /**
     * Given a nullable reference, builds an {@link Option}.
     *
     * @param     the input and {@link Option} type
     * @param object the input object
     * @return an Option containing the provided object, or an empty {@link Option}
     *         if null is passed
     */
    public static  Option maybe(@Nullable final T object) {
        return Option.of(object);
    }

    /**
     * Compares two arbitrary {@link Comparable}s and returns the smaller.
     *
     * @param a a comparable
     * @param b another comparable
     * @return the smallest between the two parameters
     * @param  the type of objects being compared
     */
    public static > T min(final T a, final T b) {
        return selectComparable(a, b, c -> c < 0);
    }

    private static > T selectComparable(final T a, final T b, final IntPredicate selector) {
        if (a.getClass() == b.getClass() || a.getClass().isAssignableFrom(b.getClass())) {
            // Same types, or A is superclass (hence should be comparable with B)
            return selector.test(a.compareTo(b)) ? a : b;
        }
        if (b.getClass().isAssignableFrom(a.getClass())) {
            // B is superclass of A
            return selector.test(b.compareTo(a)) ? b : a;
        }
        // Compare different numbers as double
        if (a instanceof Number && b instanceof Number) {
            return selector.test(Double.compare(((Number) a).doubleValue(), ((Number) b).doubleValue())) ? a : b;
        }
        // Give up
        throw new IllegalArgumentException("Tried to compare '"
            + a + ": " + a.getClass().getSimpleName() + "' with "
            + b + ": " + b.getClass().getSimpleName() + "', but such comparison of types was not possible."
        );
    }

    /**
     * Returns true if all the elements of the field are false, including the local value.
     *
     * @param target the field
     * @return true if all the elements of the field are false
     */
    public static boolean none(final Field target) {
        return !any(target);
    }

    /**
     * Returns true if all the elements of the field are false, not considering the local value.
     *
     * @param target the field
     * @return true if any of the elements of the field are false. The local value
     *         is not considered.
     */
    public static boolean noneButSelf(final Field target) {
        return !anyButSelf(target);
    }

    /**
     * Reduces a field, excluding the local value. This method wraps the
     * result in an {@link Option}. If the field only contains the local value, then
     * an empty {@link Option} is returned.
     *
     * @param                field and result type
     * @param context           {@link ExecutionContext}
     * @param target            the field to be reduced
     * @param reductionFunction the reduction function
     * @return an {@link Option} with the result of the field reduction, or an empty
     *         option.
     */
    public static  Option reduceHood(
        @Nonnull final ExecutionContext context,
        @Nonnull final Field target,
        @Nonnull final FunctionDefinition reductionFunction
    ) {
        return target.reduceValues(reductionFunction(context, target.getExpectedType(), target, reductionFunction));
    }

    /**
     * Produces an Option value. If the passed object is null, then an empty option
     * is returned. Otherwise, an Option enclosing the value is returned.
     * Recommended way to interact with Java method that may return null.
     *
     * @param     Object type
     * @param object the nullable object
     * @return If the passed object is null, then an empty option is returned.
     *         Otherwise, an Option enclosing the value is returned.
     */
    public static  Option optionally(@Nullable final T object) {
        return Option.fromNullable(object);
    }

    /**
     * Reduces a field, excluding the local value, by picking the maximum
     * value. This method wraps the result in an {@link Option}. If the field only
     * contains the local value, then an empty {@link Option} is returned.
     *
     * @param                field and result type
     * @param target            the field to be reduced
     * @return an {@link Option} with the result of the field reduction, or an empty
     *         option.
     */
    public static > Option reduceMax(final Field target) {
        return target.reduceValues(Builtins::max);
    }

    /**
     * Reduces a field, excluding the local value, by computing the
     * arithmetic mean of the values. This method wraps the result in an
     * {@link Option}. If the field only contains the local value, then an empty
     * {@link Option} is returned.
     *
     * @param target the field to be reduced
     * @return an {@link Option} with the result of the field reduction, or an empty
     *         option.
     */
    public static Option reduceMean(final Field target) {
        return reduceSum(target).map(it -> it.doubleValue() / target.size());
    }

    /**
     * Reduces a field by picking the minimum of its values, excluding the local
     * value. This method wraps the result in an {@link Option}. If the field
     * only contains the local value, then an empty {@link Option} is returned.
     *
     * @param                field and result type
     * @param target            the field to be reduced
     * @return an {@link Option} with the result of the field reduction, or an empty
     *         option.
     */
    public static > Option reduceMin(final Field target) {
        return target.reduceValues(Builtins::min);
    }

    /**
     * Reduces a field, excluding the local value, by computing the sum of
     * the values. This method wraps the result in an {@link Option}. If the field
     * only contains the local value, then an empty {@link Option} is returned.
     *
     * @param     field and result type
     * @param target the field to be reduced
     * @return an {@link Option} with the result of the field reduction, or an empty
     *         option.
     */
    public static  Option reduceSum(final Field target) {
        return target.reduceValues(Builtins::sum);
    }

    /**
     * Reduces a field, excluding the local value, by computing the union of
     * the values. This method wraps the result in an {@link Option}. If the field
     * only contains the local value, then an empty {@link Option} is returned.
     *
     * @param     field and result type
     * @param target the field to be reduced
     * @return an {@link Option} with the result of the field reduction, or an empty
     *         option.
     */
    public static  Option reduceUnion(final Field target) {
        return target.reduceValues(Builtins::union);
    }

    @Nonnull
    private static  BinaryOperator reductionFunction(
        @Nonnull final ExecutionContext context,
        @Nonnull final Class expectedType,
        @Nonnull final Field target,
        @Nonnull final FunctionDefinition reductionFunction
    ) {
        return (a, b) -> {
            final Object reductionResult = runProtelisFunctionWithJavaArguments(
                context,
                reductionFunction,
                ImmutableList.of(a, b)
            );
            if (expectedType.isAssignableFrom(reductionResult.getClass())) {
                return expectedType.cast(reductionResult);
            }
            throw new IllegalStateException(
                "Reduction operation over target field " + target
                    + " with type " + target.getExpectedType()
                    + " failed because the provided reduction function reduced "
                    + a + " of type " + a.getClass().getName()
                    + " and " + b + " of type " + b.getClass().getName()
                    + " into " + reductionResult
                    + " of type " + reductionResult.getClass().getName()
                    + " which does not conform to expected type "
                    + expectedType.getName()
            );
        };
    }

    private static  R run(
        final Class xClass,
        final Object x,
        final Class yClass,
        final Object y,
        final BiFunction fun
    ) {
        return fun.apply(convert(xClass, x), convert(yClass, y));
    }

    private static  R run(final Class inClass, final Object x, final Object y, final BiFunction fun) {
        return run(inClass, x, inClass, y, fun);
    }

    private static  X runBi(final Class inClass, final Object x, final Object y, final BinaryOperator fun) {
        return run(inClass, x, y, fun);
    }

    @SuppressWarnings(UNCHECKED)
    private static  T sum(final T a, final T b) {
        if (a instanceof CharSequence || b instanceof CharSequence) {
            return (T) (a.toString() + b.toString());
        }
        if (a instanceof BigDecimal || b instanceof BigDecimal) {
            return (T) runBi(BigDecimal.class, a, b, BigDecimal::add);
        }
        if (a instanceof BigInteger || b instanceof BigInteger) {
            return (T) runBi(BigInteger.class, a, b, BigInteger::add);
        }
        if (a instanceof Double || b instanceof Double) {
            return (T) runBi(Double.class, a, b, Double::sum);
        }
        if (a instanceof Float || b instanceof Float) {
            return (T) runBi(Float.class, a, b, Float::sum);
        }
        if (a instanceof Long || b instanceof Long) {
            return (T) runBi(Long.class, a, b, Long::sum);
        }
        if (a instanceof Integer || b instanceof Integer
            || a instanceof Byte || b instanceof Byte
            || a instanceof Short || b instanceof Short
        ) {
            return (T) runBi(Integer.class, a, b, Integer::sum);
        }
        if (a instanceof Boolean && b instanceof Boolean) {
            return (T) runBi(Boolean.class, a, b, Boolean::logicalAnd);
        }
        if (a instanceof Tuple && b instanceof Tuple) {
            return (T) runBi(Tuple.class, a, b, Tuple::mergeAfter);
        }
        return byReflection("plus", a, b);
    }

    @SuppressWarnings(UNCHECKED)
    private static  T union(final T a, final T b) {
        if (a instanceof Tuple && b instanceof Tuple) {
            return (T) runBi(Tuple.class, a, b, Tuple::union);
        }
        if (a instanceof Set && b instanceof Set) {
            return (T) runBi(Set.class, a, b, Sets::union);
        }
        if (b instanceof Tuple) {
            return (T) runBi(Tuple.class, DatatypeFactory.createTuple(a), b, Tuple::union);
        }
        if (a instanceof Tuple) {
            return (T) runBi(Tuple.class, a, DatatypeFactory.createTuple(b), Tuple::union);
        }
        return byReflection("union", a, b);
    }

}