Download presentation
Presentation is loading. Please wait.
Published byHadi Cahyadi Modified over 6 years ago
1
Advanced Topics in Concurrency and Reactive Programming: Functional Programming
Majeed Kassis
2
Functional Programming Topics
History of functional programming Pure vs non pure 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
3
Evolution Of Programming Paradigms
4
Functional Programming
Not modified y = f (x) Not shared
5
Functional Programming
A programming style that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. No iteration No iterators! No loops No for loops, no while loops. No variables No assignment whatsoever. 𝑥 = 𝑥 + 1 is illegal! All functions! Function is not a method.
6
Function vs Method Method: (OOP) Function: (PFP)
They are found inside a class instance. They are not independent – they are bound to some context They may depend on other values other than their argument They may change values of its arguments or other static variables Function: (PFP) Lives independently of any class instance. Output of a function can be a value or a function Does not change any argument value nor static variables Functions maybe composed together regardless of context
7
g = map ○ f g = map(f ) map(f, [x1, x2, ..., xn]) High Order Functions
Result is a new function
8
Benefits of Functional Programming
No shared state makes it easier to reason about programs correctness (does the program do what it is supposed to) performance (how long does it take) scalability (how is performance affected with input) security (can the algorithm be maliciously misused) Concurrency problems simply, almost, don’t exist! Order of execution is not important Doesn’t happen in pure functional programing Testing and debugging is easy Functions can be easily be tested in isolation Writing test code that calls the pure function with typical values valid edge cases, and invalid edge cases. Algorithms are often more elegant Easier to read shorter, and easier to understand. To reason about, means to check program correctness, performance, scalability and security and provide a proof that the program is in optimal conditions in all of these aspects.
9
Functional Programming: Side Effects
A side effect occurs when evaluating an expression interacts with something other than the expression itself A pure function has no side effects: It does not use any values from outside (global scope). It only takes parameters It does not change any outside values (global scope) It only returns a result. Does not have a state Execution of the function does not rely on previous executions Does not receive user input or prints an output This causes the execution of the functions to be non-deterministic. Problems with side-effect functions: Hard to test and debug programs. Hard to make them work in parallel.
10
Non-pure functions: Examples
𝑓(𝑥) { 𝑟𝑒𝑡𝑢𝑟𝑛 𝑥 +𝑦 } 𝑦 is a free variable, 𝑦 is not bound to any condition Executing 𝑓(𝑥) with the same 𝑥 might return different value each time! 𝑓(𝑥) { 𝑦=𝑥; 𝑟𝑒𝑡𝑢𝑟𝑛 𝑥 +𝑦 } This is a side effect – it causes 𝑦 to change its value from within the function 𝑓(𝑥) { 𝑟𝑒𝑎𝑑 𝑦; 𝑟𝑒𝑡𝑢𝑟𝑛 𝑥 + 𝑦} The function output is defined by the user input. This might cause 𝑓(𝑥) to return different values each execution. Reading from command line and printing to screen makes a function non-pure! -> free variables makes the runs non-deterministic which increases the difficulty of debugging the programs.
11
Functional vs Imperative
Iteration Recursion Allows mutation Avoids mutation Uses memory to store state Has no state No use of high order functions Uses high order functions Based on Turing machine Based on Lambda calculus Because pure functional programming does not allow state, we do not use iterators which store state. Instead we use recursion or higher order functions to iterative over collections.
12
Imperative vs Functional Programming
Example: Input: [1,4,6,9,11] Return all even values Imperative Solution: 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); }
13
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 Applying filter returns a Stream<Integer>, using .collect and Collectors we are able to convert the stream back to ArrayList<Integer>
14
Pure Functional Characteristics
No side effects! Functions do not modify values Uses functions only. A function takes parameters and returns something Everything (even functions) is first class value! There is a way to write a “literal” (anonymous) function Functions can be stored in variables and in collections Functions can be passed as parameters to functions Functions can be returned as the result of a function There are operators to combine functions into new functions Anonymous functions in java are called lambda
15
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
16
Higher Order Functions : More Functions
Fold Same as Reduce, but requires an initial value. 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: 𝑧𝑖𝑝([1, 2, 3], [1, 2, 3], (𝑥, 𝑦−>𝑥∗𝑦)) [1, 4, 9]
17
Java Functional Programming Support
Functional Languages Java Object Oriented Java 8 Functions Independent Bound to class object, “methods” Uses functional interface Independent but bound to object or class Value State Immutable Mutable Immutable if required Function Chaining Yes Only if in same instance Lambdas allow chaining Concurrency and multicore support Requires synchronization tools Yes – if pure functional
18
Java Functional Interfaces
Input Output Purpose Function Single object of any type Apply logic on input, allow logic chaining BiFunction Two objects of any type Apply logic on both inputs, allow logic chaining Predicate Boolean Test if value conforms to Boolean logic Consumer/BiConsumer Single/double object of any type None Used a value and output, execute some side-effect Supplier Create an object of desired type
19
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 interface Function<T,R> R apply(T t) Applies logic to T and returns an object of R andThen(Function after) First applies its logic then the logic provided by Function after compose(Function before) First applies before logic then its own logic identity() Returns a function that always returns its input argument.
20
Executing a function using apply()
//defining a function receives String as input, //returns Integer as output using lambda expression Function<String, Integer> findWordCount = in -> { return in.split(" ").length; }; //execute the implemented function using apply() System.out.println( findWordCount.apply("this sentence has five words"));
21
Chaining functions //define a function which returns for each digit [1,5] its string value //otherwise it returns unknown Function<Integer,String> numberToString = in -> { switch(in){ case 1: return "one"; case 2: return "two"; case 3: return "three"; case 4: return "four"; case 5: return "five"; default: return "unknown"; } }; Using andThen() we can chain functions It is important to make sure that the output of the first function has the same class or superclass as the input of the second function otherwise ClassCastException will be thrown.
22
Chaining functions //chain the implemented functions using andThen() and apply() System.out.println( findWordCount.andThen(numberToString) .apply("this sentence has five words")); //chain the implemented functions using compose() and apply() System.out.println( numberToString.compose(findWordCount) Both examples return same result. ‘five’
23
Java Predicate Interface
Used to test if a data conforms to some value or logic Interface: public interface Predicate<T> boolean test(T t) Tests if t conforms to a logic and(Predicate otherPredicate) Logical and operation with another predicate or(Predicate otherPredicate) Logical or operation with another predicate negate() Logical not operation
24
Using Predicate Interface
String sentence = "this sentence has five words"; Predicate<String> specialWordChecker = in -> { return in.contains("five"); }; Predicate<String> sizeChecker = in -> { return (in.length() > 50); }; System.out.println(specialWordChecker.test(sentence)); System.out.println(sizeChecker.test(sentence)); System.out.println(sizeChecker.and(specialWordChecker).test(sentence)); System.out.println(sizeChecker.or(specialWordChecker).test(sentence)); System.out.println(sizeChecker.negate().test(sentence)); Result: True False
25
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() Consumer: Supplier: BiConsumer: BiConsumer:
26
Supplier/Consumer Example
//Supplier implemented to generate Fibonacci sequence Supplier<Long> fibonacciSupplier = new Supplier<Long>() { long n1 = 1; long n2 = public Long get() { long fibonacci = n1; long n3 = n2 + n1; n1 = n2; n2 = n3; return fibonacci; } }; //Consumer implemented to printout received values Consumer<Long> beautifulConsumer = o -> System.out.print(o + "\t"); //Stream uses Supplier to generate 50 items, // and applies Consumer on each item using forEach Stream.generate(fibonacciSupplier).limit(50).forEach(beautifulConsumer);
27
Another Supplier Example
Supplier<Long> fibonacciSupplier = new Supplier<Long>() { long n1 = 1; long n2 = 1; @Override public Long get() { //this is not a pure function! long fibonacci = n1; long n3 = n2 + n1; n1 = n2; n2 = n3; return fibonacci; } }; for (int i=0;i<10;i++) //this will print first 10 numbers System.out.print(fibonacciSupplier.get() + "\t"); This is not lazy evaluation! Just functional style.
28
Java Streams Stream is the structure for processing a collection in functional style. Original collection is not modified. Stream may be processed only once. After getting an output, the stream cannot be used again. 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) IntStream iStream = IntStream.range(1, 5); IntStream, DoubleStream, LongStream
29
Java Streams are Lazy 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.
30
Java 8 Streams // Create an ArrayList List<Integer> myList = new ArrayList<Integer>(); myList.add(1); myList.add(5); myList.add(8); // Convert it into a Stream Stream<Integer> myStream = myList.stream(); // Create an array Integer[] myArray = {1, 5, 8}; // Convert it into a Stream Stream<Integer> myStream = Arrays.stream(myArray);
31
Java 8 Stream Pipelining
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.
32
Java Stream Creation From Arrays: From Collections: Using generate():
static <T> Stream<T> of(T t) Stream<String> stream = Stream.of(“this", “is", “a", “string", “stream", “in", “Java"); default Stream<E> stream() List<String> list = new ArrayList<String>(); list.add("java"); list.add("php"); list.add("python"); Stream<String> stream = list.stream(); static <T> Stream<T> generate(Supplier<T> s) Stream<String> stream = Stream.generate(() -> "test").limit(10);
33
More Java Stream Creation
Using iterate(): Using APIs: 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) BigInteger is a class that can store integers larger than 64bit. Has no maximum limit! It is immutable and mostly slower than regular integer objects \\W – no-word character String sentence = “This is a six word sentence."; Stream<String> wordStream = Pattern.compile("\\W") .splitAsStream(sentence);
34
‘Reusing’ of Streams 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: Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c") filter(s -> s.startsWith("a")); stream.anyMatch(s -> true); // ok stream.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)); //true System.out.println(streamSupplier.get().noneMatch(s -> true)); //false
35
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.
36
Intermediate Operations
Stream Operation Purpose Input filter Filters item according to a predicate Predicate map/flatMap Applies function on stream items Function limit Returns first n elements of stream int sorted Sort stream items Comparator distinct Discards duplicates using equals method Reduce/Fold Applies function on all of the stream BinaryOperator peek Returns a copy of stream then applies Consumer function on result Consumer skip Discard first n elements of result stream Long flatMap combines the results, where map does not Fold is a special case where user can provide an initial value, while reduce does not have an initial value.
37
Java Example: Map //make a new stream String[] myArray = new String[]{"bob", "alice", "paul", "ellie"}; Stream<String> myStream = Arrays.stream(myArray); //convert to upper case Stream<String> myNewStream = myStream.map(s -> s.toUpperCase()); //convert back to array of strings String[] myNewArray = myNewStream.toArray(String[]::new); java.util.Arrays class can be used to generate a stream from an array object.
38
Java Example: Filter //make a new array of strings String[] myArray = new String[]{"bob", "alice", "paul", "ellie"}; //convert to stream, filter, and convert back to array of strings String[] myNewArray = Arrays.stream(myArray) filter(s -> s.length() > 4) toArray(String[]::new); Filtering creates a new stream. The original stream remains untouched.
39
Java Example: Reduce Output? 3
//make a new array of strings List<String> myArray = Arrays.asList("bob", "alice", "paul", "ellie"); //convert to stream, map to int, reduce to min, printout System.out.println(myArray.stream() map(s -> s.length()) reduce(Integer::min).get()); map() – maps each string to its length value reduce() – applies ‘min’ on the stream returning “Optional<T>”. which we get() the value from it. Output? 3
40
Java Example: Fold Output? 1
//make a new array of strings List<String> myArray = Arrays.asList("bob", "alice", "paul", "ellie"); //convert to stream, map to int, reduce to min between 0 //and min string length, printout System.out.println(myArray.stream() map(s -> s.length()) reduce(1, Integer::min)); Fold does the same thing as reduce but adds to it an initial value. You can look at Reduce as a special case of Fold. In Java, Reduce and Fold are implemented under “reduce” Fold accepts two parameters, while reduce accepts one. Output? 1
41
Java: FlatMap 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: Stream<String[]> -> flatMap -> Stream<String> Stream<Set<String>> -> flatMap -> Stream<String> Stream<List<String>> -> flatMap -> Stream<String> Stream<List<Object>> -> flatMap -> Stream<Object> FlatMap applies Map on each item of the stream, and the result is flattened. { {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'}
42
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.
43
FlatMap Example: Student Class
public class Student { private String name; private List<String> book; public void addBook(String book) { if (this.books == null) { this.books = new ArrayList<>(); } this.books.add(book); public Set<String> getBooks(){ return book;
44
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 = new ArrayList<>(); list.add(std1); list.add(std2);
45
FlatMap Example: Executing the Query
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)); x.stream() generates a stream from a Set<String> Applying flatMap will apply map on each item and asking from each Set<String> its stream() and them flattening them into one Stream of Strings Output: Spring Boot in Action Effective Java (2nd Edition) Java 8 in Action Learning Python, 5th Edition
46
Python Example: Zip Output: 10 11 20 21 30 31 1 2 3 4 5 6 7 8
list1 = [10, 20, 30, 40] list2 = [11, 21, 31] # Zip the two lists. result = zip(list1, list2) # The zipped result is only 3 elements long. # ... The fourth element of list1 was discarded. for element1, element2 in result: print(element1, element2) Output: 10 11 20 21 30 31 No ZIP functionality in Java8 unfortunately.
47
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
48
Termination Operations
Stream Operation Purpose Input forEach For each item, apply Consumer function Consumer count Count stream items collect Reduces stream into a desired collection Collector min/max Returns min/max element according to Compartor Comparator anyMatch/allMatch/noneMatch Returns whether any/all/none elements of stream match the provided predicate. Predicate findFirst/findAny Returns Optional containing first/any result anyMatch – any of the elements fulfill the predicate are returned allMatch – all of the elements must fulfill the predicate, otherwise not returned. noneMatch – none of elements fulfill predicate, otherwise not returned findAny – returns a result following maximal performance in parallel operations Complete API:
49
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.
50
Collectors – Part of API
Collectors.toList() Puts items into a list Collectors.toCollection(TreeSet::new) Puts items into a desired container. Container is supplied with a supplier Collectors.joining(", “) Joins multiple items into a single item by concatenating them Collectors.summingInt(item::getAge) Sums the values of each item with given supplier Collectors.groupingBy() Groups the items with given classifier and mapper – results in as many groups as needed Collectors.partitioningBy() Partitions the items with given predicate and mapper – results in two groups only!
51
Example: Company Employees
public class Employee { private String name; private String department; private int salary; public Employee(String name, String department, int salary){ this.name = name; this.department = department; this.salary = salary; } public int getSalary(){ return salary; } public String getDepartment(){ return department; } }
52
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));
53
groupingBy(): Find total salary cost for each department
54
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));
55
partitionBy(): Find Number of employees with salary less than 3000
56
Print out two groups: true, false, and their sizes
company.stream() collect(Collectors.partitioningBy(e -> e.getSalary() < 3000, Collectors.counting())) forEach((s, d) -> System.out.println("Group: " + s "\tTotal Employees: " + d));
57
peek() operation Returns a stream consisting of the elements of this stream. It also performs the provided action on each element from the resulting stream. This function is used mainly to support debugging API: Stream<T> peek(Consumer) Stream.of("one", "two", "three", "four") filter(e -> e.length() > 3) peek(e -> System.out.println("Filtered value: " + e)) map(String::toUpperCase) peek(e -> System.out.println("Mapped value: " + e)) collect(Collectors.toList());
58
Java Optional Interface
//make a new array of strings List<String> myArray = Arrays.asList("bob", "alice", "paul", "ellie"); //convert to stream, map to int, reduce to min between 0 and min string length, printout Optional<String> result = myArray.stream() filter(s -> s.length() > 4) findFirst(); if (result.isPresent()) System.out.println(result.get()); “result” may contain and may not contain a non-null value! Sometimes, filters may return “no result” as a result. The use of Optional here will assist us to check whether a value is returned. Null checks “litter” code, and it is frowned upon. Java 8 provides this special object and encourages this best-practice over null checks.
59
Functional Programming != Streams
Functional programming is not Streams! Functional programming is much more than Streams Streams incorporate functional programming technique in: its implementation its usage. Functional programming is about Avoiding shared state Avoiding mutation Building the software as functions - pure functions preferred! Recursion over iteration Lazy over eager computation So how do we allow mutation?
60
What about adding an element to a collection?
Not modified y = f (x) Not shared Each execution of a function will return a clone of the original collection. Very expensive memory wise! 𝑥 is a collection, 𝑥 is not modified What if we wish to add an element to 𝑥? What will 𝑦 be?
61
Persistent Data Structures
Immutable Collections – data structures (that have) Mutative API – allows add/remove/update! (which are) Persistent - preserves the previous version of itself when modified (and implemented with) Maximum Sharability and Runtime Efficiency Popular Implementation: Immutable.js
62
More Topics Parallelism using Java Streams
Wasn’t the main purpose of functional programming is to show how it works concurrently? Implementing Lazy (evaluation) Functions Unfortunately Java does not have support for lazy functions. Streams are lazy by design. Use of limit() is a way to limit the output. No reuse of streams by default. Each time a termination function is applied on a stream, the stream is closed. Must make a new stream for reuse!
63
True Lazy Evaluation: Python Example
def fib(): a, b = 1, 1 while True: yield a a, b = b, a + b fibonacciSequence = fib() for i in range(1, 10): print(next(fibonacciSequence))
Similar presentations
© 2025 SlidePlayer.com. Inc.
All rights reserved.