简体   繁体   中英

Remember last value returned from a JDK 8 lambda

I threw together a quick Java implementation of a Taylor series expansion for the exponential function, because it was easy and fun:

package math.series;

import java.util.stream.IntStream;

/**
 * Created by Michael
 * Creation date 3/6/2016.
 * @link https://stackoverflow.com/questions/35826081/calculating-ex-in-c-sharp
 * @link https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80
 */
public class TaylorSeries {


    public static final int DEFAULT_NUM_TERMS = 10;

    public static void main(String[] args) {
        int n = (args.length > 0) ? Integer.parseInt(args[0]) : DEFAULT_NUM_TERMS;
        System.out.println("pi");
        System.out.println(String.format("%10s %10s %10s %10s", "n", "series", "expected", "error"));
        double expected = Math.PI;
        double series = TaylorSeries.pi(0.0, n);
        double error = expected - series;
        System.out.println(String.format("%10d %10.6f %10.6f %10.6f", n, series, expected, error));

        System.out.println("exp");
        System.out.println(String.format("%10s %10s %10s %10s", "x", "series", "expected", "error"));
        for (double x = 0.0; x <= 3.0; x += 0.25) {
            expected = Math.exp(x);
            series = TaylorSeries.exp(x, n);
            error = expected - series;
            System.out.println(String.format("%10.6f %10.6f %10.6f %10.6f", x, series, expected, error));
        }
    }

    public static double exp(double x, int n) {
        double sum = 1.0;
        double term = 1.0;
        for (int i = 1; i <= n; ++i) {
            term *= x / i;
            sum += term;
        }
        return sum;
    }

    public static double pi(double x, int n) {
        return IntStream.range(0, n)
                .mapToDouble(i -> 8.0/(4*i+1)/(4*i+3))
                .sum();
    }
}

I'm ashamed to admit that my employer is still using JDK 6 and JDK 7; I'm not writing on JDK 8 during my work day yet. I have not groked all new features in the JDK, including lambdas.

I warmed up by writing a Taylor series expansion for pi using lambda. It's easy and elegant. Surprisingly, it requires a million terms to converge to six digits of accuracy, but that's the nature of the series .

I decided to try and implement the exponential function using a lambda. I don't want to do the naive thing and use the Math.pow or factorial functions; the implementation I posted without lambdas accomplishes both nicely.

I can't see how to have each step in the lambda remember what the value of the previous term was. Can anyone help a lambda beginner and give an example?

A possible solution is to implement a stateful function for returning the next term in the sequence:

public static double exp(double x, int n) {
    return DoubleStream.iterate(1, new DoubleUnaryOperator() {

        private int i = 1;

        @Override
        public double applyAsDouble(double operand) {
            return operand * x / i++;
        }

    }).limit(n).sum();
}

This will create a DoubleStream with the help of the iterate(seed, f) method where the seed is 1 and the function, which returns the next value, simply increments the current iteration number i and multiplies the previous value with x / i . The Stream is limited to n element with limit and the sum is retrieved with sum() .

Sample calling code:

public static void main(String[] args) {
    System.out.println(exp(3, 500)); // prints "20.085536923187668"
}

with a result very close to the real one .

Added a small improvement to Tunaki's solution: replaced the DoubleUnaryOperator anonymous class with a lambda and the i attribute with an AtomicInteger instance:

public static double exp(double x, int n) {
    final AtomicInteger integer = new AtomicInteger(1);
    return DoubleStream.iterate(
       1.0, 
       operand -> operand * x / integer.getAndIncrement()
    ).limit(n).sum();
}

There is a solution which doesn't require stateful functions. All you need is a pair of two values so you can express a function which maps from a pair of two values to another pair. Due to the absence of a general purpose pair type in Java, you have two options:

  1. Use an array of length two:

     double exp=Stream.iterate(new double[]{1, 1}, a -> new double[]{ a[0]*x/a[1], a[1]+1}) .limit(n+1).collect(Collectors.summingDouble(a -> a[0])); 

    this is short, but cleaner is:

  2. Create a dedicated type holding the two values as instance variables:

     final class Item { final double term; final int index; Item(double t, int i) { term=t; index=i; } } double exp=Stream.iterate(new Item(1, 1), i -> new Item(i.term*x/i.index, i.index+1)) .limit(n+1).collect(Collectors.summingDouble(i -> i.term )); 

    This requires more code if you count the class but is simpler and more readable in the stream operation code and allows both variables to have an appropriate type.

These solutions are thread-safe, unlike the solutions bearing stateful functions, however, it's very unlikely to benefit from a parallel stream here, due to the dependency of each element to its previous one. Since your question is about fun and education, let's demonstrate, how we can do that easily in parallel, if we accept the requirement of storage depending on the number of iterations:

double[] array=new double[n+1];
Arrays.parallelSetAll(array, index -> index==0? 1: x/index);
Arrays.parallelPrefix(array, (a,b) -> a*b);
// we could do the last step as prefix op as well:
//Arrays.parallelPrefix(array, Double::sum);
//double exp=array[n];
// but a straight forward summing is better:
double exp=Arrays.stream(array).parallel().sum();

This solution still uses the low level functions, multiplication and addition instead of pow or factorial, as you wished, but it utilizes the fact that the required operations can be decomposed into operations which do not depend on all previous elements, so that parallel processing is possible. Of course, you'll need a sufficiently large n to benefit from parallel processing and then, you'll need appropriate RAM to hold the temporary results.

Maybe it's too late, but here's another approach that slightly improves Tunaki and Adam Siemion's answers:

public static double exp(double x, int n) {
    PrimitiveIterator.OfInt i = IntStream.rangeClosed(1, n).iterator();
    return DoubleStream.iterate(1.0, term -> term * x / i.next())
            .limit(n)
            .sum();
}

Instead of an anonymous inner class that holds state for the index or a lambda that references an AtomicInteger , it would be better to build a primitive iterator of int from a closed range stream.

I'd like to thank all those who responded here. I've incorporated your suggestions into a solution that still had some surprising subtleties. Both answers were important.

Here's the code, including lambda implementations for both sine and cosine. Getting the difference in the AtomicInteger index for each was key:

package math.series;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;

/**
 * Created by Michael
 * Creation date 3/6/2016.
 * @link https://stackoverflow.com/questions/35826081/calculating-ex-in-c-sharp
 * @link https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80
 */
public class TaylorSeries {


    public static final int DEFAULT_NUM_TERMS = 10;

    public static void main(String[] args) {
        int n = 10000000;
        double y = 1.0;
        System.out.println(String.format("pi using %d terms", n));
        System.out.println(String.format("%20s %20s %20s %20s", "n", "series", "expected", "error"));
        double expected = Math.PI;
        double series = TaylorSeries.pi(0.0, n);
        double error = expected - series;
        System.out.println(String.format("%20.16f %20.16f %20.16f %20.6e", y, series, expected, error));

        n = 50;
        System.out.println(String.format("exp using %d terms", n));
        System.out.println(String.format("%20s %20s %20s %20s", "x", "series", "expected", "error"));
        for (double x = 0.0; x <= 3.0; x += 0.25) {
            expected = Math.exp(x);
            series = TaylorSeries.expLambda(x, n);
            error = expected - series;
            System.out.println(String.format("%20.16f %20.16f %20.16f %20.6e", x, series, expected, error));
        }

        System.out.println(String.format("sin using %d terms", n));
        System.out.println(String.format("%20s %20s %20s %20s", "x", "series", "expected", "error"));
        for (double x = 0.0; x <= Math.PI; x += Math.PI/20.0) {
            expected = Math.sin(x);
            series = TaylorSeries.sinLambda(x, n);
            error = expected - series;
            System.out.println(String.format("%20.16f %20.16f %20.16f %20.6e", x, series, expected, error));
        }

        System.out.println(String.format("cos using %d terms", n));
        System.out.println(String.format("%20s %20s %20s %20s", "x", "series", "expected", "error"));
        for (double x = 0.0; x <= Math.PI; x += Math.PI/20.0) {
            expected = Math.cos(x);
            series = TaylorSeries.cosLambda(x, n);
            error = expected - series;
            System.out.println(String.format("%20.16f %20.16f %20.16f %20.6e", x, series, expected, error));
        }
    }

    public static double exp(double x, int n) {
        double sum = 1.0;
        double term = 1.0;
        for (int i = 1; i <= n; ++i) {
            term *= x / i;
            sum += term;
        }
        return sum;
    }

    public static double pi(double x, int n) {
        return IntStream.range(0, n)
                .mapToDouble(i -> 8.0/(4*i+1)/(4*i+3))
                .sum();
    }

    /**
     * A JDK 8 implementation for exp
     * @param x function argument
     * @param n terms to include in the series sum
     * @return exp(x)
     * @link https://stackoverflow.com/questions/35830072/taylor-series-using-jdk-8-lambdas
     */
    public static double expLambda(double x, int n) {
        final AtomicInteger i = new AtomicInteger(1);
        return DoubleStream.iterate(
                1.0,
                term -> term*x/i.getAndIncrement()
        ).limit(n).sum();
    }

    public static double sinLambda(double x, int n) {
        final AtomicInteger i = new AtomicInteger(0);
        return DoubleStream.iterate(
            0.0,
            term -> ((i.get() & 1) == 0 ? 1 : -1)*((i.get() == 0) ? x/i.incrementAndGet() : term*x*x/i.incrementAndGet()/i.incrementAndGet())
        ).limit(n).sum();
    }


    public static double cosLambda(double x, int n) {
        final AtomicInteger i = new AtomicInteger(0);
        return DoubleStream.iterate(
                0.0,
                term -> ((i.get() & 1) == 0 ? 1 : -1)*((i.get() == 0) ? 1.0/i.incrementAndGet() : term*x*x/i.getAndIncrement()/i.getAndIncrement())
        ).limit(n).sum();
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM