720 likes | 752 Views
Advanced T opics in Functional and Reactive Programming: Java Functional Interfaces. Majeed Kassis. Java Functional Programming. Higher order functions f ilter, map, fold, zip, reduce Java Functional Interface Function, Predicate, Supplier, Consumer Lazy C omputing – Java Streams
E N D
Advanced Topics in Functional and Reactive Programming: Java Functional Interfaces MajeedKassis
Java Functional Programming • Higher order functions • filter, map, fold, zip, reduce • Java Functional Interface • Function, Predicate, Supplier, Consumer • Lazy Computing – Java Streams • Creating Streams, Intermediate Operations, Termination Operations • Java Collectors • Monads – Java Optional
Imperative vs Functional Programming public static void getEven(ArrayList<Integer> vals){ArrayList<Integer> odds = new ArrayList<>();for (Integer val : vals)if (val % 2 != 0) odds.add(val);vals.removeAll(odds);} • Example: • Input: [1,4,6,9,11] • Return all even values • Imperative Solution:
Functional Solution public static ArrayList<Integer> getEvenFunctional(ArrayList<Integer> vals){ Stream<Integer> valsStream = vals.stream();return valsStream.filter(s -> s % 2 == 0) .collect(Collectors.toCollection(ArrayList::new));} • We provide filter function a lambda, which is another function • Allows us to filter the items we wish to remove • This does not change original collection • This, instead, makes a new one, and returns it • Which means, no mutation occur
Higher Order Functions: Core Functions • Map (and FlatMap) • Used to apply an operation on each item of a collection. • Returns a new collection. • Has no side effect! • Filter • Used to eliminate elements based on a criteria. • Returns a filtered collection. • Reduce • Used to apply a function on the complete collection. • Returns a value as a result of the aggregating function • sum, min, max, average, mean, string concatenation
Higher Order Functions : More Functions • Fold • Same as Reduce, but requires an initialvalue. • Reduce is a special case of Fold. • Integer sum = integers.reduce(Integer::sum); //REDUCE • Integer sum = integers.reduce(0, Integer::sum); //FOLD • Zip • Two input sequences, and a function • Output sequence combines same index values of input sequences • After applying the function. • Example:
Java Function Interface Used for creating a an output object based on a given input and the logic and possibly chaining with other functions. A logic can be packed as a variable. public interfaceFunction<T,R>
Executing a function using apply() //defining a function receives String as input, //returns Integer as output using lambda expressionFunction<String, Integer> findWordCount = in -> {return in.split(" ").length;};//execute the implemented function using apply()System.out.println( findWordCount.apply("this sentence has five words"));
Java Functional Composition • To compose two functions means to arrange them so that the result of one function is applied as the input of the other function • Atechnique to combine multiple functions into a single function • It uses the combined functions internally • Java comes with built-in support for functional composition • To make the job easier to combine functions
Incorrect Composition of Predicates • This functional composition example first creates two Predicate implementations in the form of two lambda expressions. • The first Predicate returns true if the String you pass to it as parameter starts with an uppercase a (A). • The second Predicate returns true if the String passed to it ends with a lowercase x . • Note, that the Predicate interface contains a single unimplemented method named test() which returns a boolean. • It is this method the lambda expressions implement.
Java Predicate Composition Support java.util.function.Predicate contains assisting method for predicate composition:
Correct Composition of Predicates • This Predicate or() functional composition example first creates two basic Predicate instances. • Second, the example creates a third Predicate composed from the first two, • by calling the or() method on the first Predicate • and passing the second Predicate as parameter to the or() method. • The output of running the above example will be true
Java Function Composition Support java.util.function.Functioncontains a few methods that can be used to compose new Function instances from existing ones.
Function Composition Example: Compose • The Function returned by compose() will first call the Function passed as parameter to compose() • and then it will call the Function which compose() was called on. • When called with the value 3: • The composed Function will first call the add Function • and then the multiply Function. • The result of the calculation will be (3 + 3) * 2 =12.
Function Composition Example: andThen • andThen() method is called on the multiply Function to compose a new Function, • passing the add Function as parameter to andThen(). • Calling the Function composed by andThen() with the value 3 will result in: • 3 * 2 + 3 =9. • andThen() method works opposite of the compose() method: • a.andThen(b) is the same as calling b.compose(a) . • A Function composed with andThen() will first call the Function that andThen() was called on, • and then it will call the Function passed as parameter to the andThen() method
Java Consumer/Supplier Interfaces • Two Interfaces that follow the producer-consumer paradigm. • Consumer<T>: (BiConsumer<T,R>) • Represents an operation that accepts a single input argument and returns no result. • Since the interface does not return a result, the item is ‘consumed’ • Function: void accept(T t) (void accept(T t, U u)) • Supplier<T>: • Represents a supplier of results. • Execution of get() generates T and returns it. • Function: T get()
Supplier/Consumer Example //Supplier implemented to generate Fibonacci sequenceSupplier<Long> fibonacciSupplier = new Supplier<Long>() {long n1 = 1;long n2 = 1;@Overridepublic Long get() {long fibonacci = n1;long n3 = n2 + n1;n1 = n2;n2 = n3;return fibonacci; }};//Consumer implemented to printout received valuesConsumer<Long> beautifulConsumer = o -> System.out.print(o + "\t");//Stream uses Supplier to generate 50 items, // and applies Consumer on each item using forEachStream.generate(fibonacciSupplier).limit(50).forEach(beautifulConsumer);
Another Supplier Example Supplier<Long> fibonacciSupplier = new Supplier<Long>() {long n1 = 1;long n2 = 1;@Overridepublic Long get() { //this is not a pure function!long fibonacci = n1;long n3 = n2 + n1;n1 = n2;n2 = n3;return fibonacci; }};for (inti=0;i<10;i++) //this will print first 10 numbersSystem.out.print(fibonacciSupplier.get() + "\t");
Java Streams • Stream is the structure for processing a collection of objects in functional style • Original collection is not modified. • A Java Stream is a component that is capable of internal iteration of its elements, meaning it can iterate its elements itself. • In contrast, using Java Iterator or the Java for-eachloop, the programmer have to implement the iteration process themselves • Every Collection type may be converted to Stream • Can be created with stream() method of Collection. • Streams can also be created from scratch. • Stream<Integer> s = Stream.of(3, 4, 5) • IntStreamiStream = IntStream.range(1, 5);
Stream Processing • Stream may be processed only once. • After getting an output, the stream cannot be used again. • Processing is done by attaching listeners to a Stream. • These listeners are called when the Stream iterates the elements internally • The listeners of a stream form a chain: • The first listener in the chain can process the element in the stream, • and then return a new element for the next listener in the chain to process. • A listener can either return the same element or a new one • depending on what the purpose of that listener (processor) is.
Stream Characteristics • Processing streams lazily allows for significant efficiencies • Filtering, mapping, and summing can be fused into a single pass on the data with minimal intermediate state. • Laziness also allows avoiding examining all the data when it is not necessary. • Example: "find the first string longer than 1000 characters“ • Need to examine just enough strings to find one that has the desired characteristics • Without examining all of the strings available from the source. • This behavior becomes even more important when the input stream is infinite and not merely large.
Java Streams: Example // Create an ArrayListList<Integer> myList = new ArrayList<Integer>();myList.add(1);myList.add(5);myList.add(8);// Convert it into a StreamStream<Integer> myStream = myList.stream();
Java Streams: Pipeline • Stream operations are combined to form stream pipelines. • Source -> Intermediate Operations -> Terminal Operation • A stream pipeline begins with a source • Such as a Collection, an array, a generator function, or an I/O channel • Followed by zero or more intermediate operations • These operations yield a new Stream as a result. • Such as Stream.filter or Stream.map • And ending with a terminal operation • These operations yield a result that is not a Stream • Such as Stream.forEach or Stream.reduce.
Java Stream Creation static <T> Stream<T> of(T t) default Stream<E> stream() static <T> Stream<T> generate(Supplier<T> s) Stream<String> stream =Stream.generate(()->"test").limit(10); • From Arrays: • From Collections: • Using generate():
More Java Stream Creation Static <T> Stream<T> iterate(T seed, UnaryOperator<T> f) Stream<BigInteger> bigIntStream = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));bigIntStream.limit(100).forEach(System.out::println); public Stream<String> splitAsStream(CharSequence input) String sentence =“This is a six word sentence."; Stream<String>wordStream=Pattern.compile("\\W") .splitAsStream(sentence); Using iterate(): Using APIs:
‘Reusing’ of Streams Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a"));stream.anyMatch(s -> true); // okstream.noneMatch(s -> true); // exception Supplier<Stream<String>> streamSupplier = () -> Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a"));System.out.println(streamSupplier.get().anyMatch(s -> true)); //trueSystem.out.println(streamSupplier.get().noneMatch(s -> true)); //false • Streams can be ‘reused’ by wrapping them with a Supplier. • Streams are not really reused but instead are conveniently re-created • Each time we get(), a new stream is provided. • Cannot pause and resume operations on same stream. • Problem: • Solution:
Intermediate Operations Characteristics • Any operation is denoted as an intermediate operation if it return a new Stream. • These operations create a new Stream that contains the elements of the initial Stream that match the given Predicate. • Example: Stream.filter does not perform any real filtering! • Intermediate operations are always lazy. • Traversal of the pipeline source does not begin until the terminal operation of the pipeline is executed.
Java Example: Map //make a new streamString[] myArray = new String[]{"bob", "alice", "paul", "ellie"};Stream<String> myStream = Arrays.stream(myArray); //convert to upper caseStream<String> myNewStream = myStream.map(s -> s.toUpperCase()); //convert back to array of stringsString[] myNewArray = myNewStream.toArray(String[]::new);
Java Example: Filter //make a new array of stringsString[] myArray = new String[]{"bob", "alice", "paul", "ellie"}; //convert to stream, filter, and convert back to array of stringsString[] myNewArray = Arrays.stream(myArray) .filter(s -> s.length() > 4) .toArray(String[]::new);
Java Example: Reduce //make a new array of stringsList<String> myArray = Arrays.asList("bob", "alice", "paul", "ellie"); //convert to stream, map to int, reduce to min, printoutSystem.out.println(myArray.stream() .map(s -> s.length()) .reduce(Integer::min).get()); • Output? 3
Java Example: Fold //make a new array of stringsList<String> myArray = Arrays.asList("bob", "alice", "paul", "ellie"); //convert to stream, map to int, reduce to min between 0 //and min string length, printoutSystem.out.println(myArray.stream() .map(s -> s.length()) .reduce(1, Integer::min)); Output? 1
Java: FlatMap • Stream<String[]>->flatMap-> Stream<String> • Stream<Set<String>>->flatMap-> Stream<String> • Stream<List<String>>->flatMap-> Stream<String> • Stream<List<Object>>->flatMap-> Stream<Object> • { {1,2}, {3,4}, {5,6} } -> flatMap -> {1,2,3,4,5,6} • { {'a','b'}, {'c','d'}, {'e','f'} } -> flatMap -> {'a','b','c','d','e','f'} This is a combination of applying a “map” function, then flattening the result by one level. Effect of FlatMap on stream structure: Effect of FlatMap on stream data:
Java Example: FlatMap • Let’s say we have a Student object which contains: • Student Name – String • Set of Book names owned by the student – Set<String> • Data Structure Population: • We create two student objects and adding books for each student. • Then add them to a list of Students. • Query we wish to implement: • Get the list of distinct books names the students possess.
FlatMap Example: Creating students and population the list • Student std1 =new Student(); • std1.setName(“John"); • std1.addBook("Java 8 in Action"); • std1.addBook("Spring Boot in Action"); • std1.addBook("Effective Java (2nd Edition)"); • Student std2 =new Student(); • std2.setName(“Mark"); • std2.addBook("Learning Python, 5th Edition"); • std2.addBook("Effective Java (2nd Edition)"); • List<Student> list =newArrayList<>(); • list.add(std1);list.add(std2);
FlatMap Example: Executing the Query Output: Spring Boot in Action Effective Java (2nd Edition) Java 8 in Action Learning Python, 5th Edition List<String> result =list.stream() .map(x ->x.getBooks())//Stream<List<String>> .flatMap(x ->x.stream())//Stream<String> .distinct() .collect(Collectors.toList()); result.forEach(x ->System.out.println(x));
Termination Operations • These operations traverse the stream to produce a result or a side-effect. • After the terminal operation is performed, the stream pipeline is considered consumed, and can no longer be used! • If you need to traverse the same data source again • you must return to the data source to get a new stream. • Examples of termination operations: • Stream.forEach • IntStream.sum
collect() using Collectors • Collector is a reducer operation • Takes a sequence of input elements and combines them into a single summary result. • Result may be one single collection or any type of one object instance • Collectors can: • accumulates the input elements into a new List: • .collect(Collectors.toList()) • Accumulates the input elements into any container: • .collect(Collectors.toCollection(ArrayList::new)); • More in next slide.
Using Streams Example: Company Employees public class Employee {private String name;private String department;private intsalary;public Employee(String name, String department, intsalary){this.name = name;this.department= department;this.salary= salary; }public intgetSalary(){return salary; }public String getDepartment(){return department; }}
Two Departments, 2 Employees in each one List<Employee> company = new ArrayList<>();company.add(new Employee(new String("Emp1"),new String("HR"), 3000));company.add(new Employee(new String("Emp2"),new String("HR"), 2000));company.add(new Employee(new String("Emp3"),new String("Admin"), 5000));company.add(new Employee(new String("Emp4"),new String("Admin"), 4000));
Print out department name, salary cost company.stream() .collect(Collectors.groupingBy(Employee::getDepartment,Collectors.summingDouble(Employee::getSalary))) .forEach((s, d) -> System.out.println("Department: " + s +"\tTotal Salary: " + d));
partitionBy(): Find Number of employees with salary less than 3000