Workshop java8 - "From Imperative to Declarative" - practical example
Plan
- We will start with something familiar which is an old style imperative loop which reads csv file.
- Then we will learn how to make computation configurable to reduce repetition.
- We will also see how FP simplifies testing
- Meanwhile we will start thinking in higher levels of abstraction
- At the end we will create reusable library which can be easily used to solve different problems.
Dependency Injection in FP
Before Java 8 Dependency Injection was mainly about composing objects with different objects and inside different object.
So in standard web app yellow circle could be a controller and blue could be DAO or something like this.
In FP we have a new very handy mechanism which can be used to implement DI - currying
Function<BLUE,Function<GREEN,Function<RED,YELLOW>>> injectDependency =
blue ->
green ->
red -> yellow(blue,green,red)
Exercise : Functional DI for logger
Exercise file : https://github.com/PawelWlodarski/workshops/blob/master/src/main/java/workshops/functional/fp2/exercises/LoggerModule.java
As a warm up let's define a curried logging function. It has signature
public static Function<Logging,Consumer<String>> createLogger
Where consumer is a Function String -> () which is the simplest definition of a logger.
Logging is an interface deined in the same class/module
public interface Logging{
default void log(String message){
System.out.println("FP2 : "+message);
}
}
private static class DefaultLogger implements Logging{};
Also try to implement unit test to check how easy will it be to inject testable version of logger
Consumer<String> testableLogger= null;
testableLogger.accept("message1");
testableLogger.accept("message2");
testableLogger.accept("message3");
/*
import static org.assertj.core.api.Assertions.*;
assertThat(loggerWithMemory.getLogs()).contains("message1","message2","message3");
*/
ADDITIONAL EXERCISE
Try to define second logger which also logs date when log was created. Try to use import java.time.Clock; which can be mocked in tests.
Reading CSV File
During this workshops we are going to parse following CSV file :
Login,Product,Price,Date
user1,tv,3000,01-02-2016
user2,tv,3000,02-02-2016
user1,console,1500,02-02-2016
user3,book,30,03-02-2016
user3,poster,15,03-02-2016
user1,stereo,1000,03-02-2016
user3,tv,3000,04-02-2016
user1,mouse,25,10-02-2016
In the first example we are working with monolithic imperative loop.
The plan is to start with a classical solution which we saw many times in Java7 or earlier and gradually move to functional composition.
Exercise : count users
Exercise code : https://github.com/PawelWlodarski/workshops/blob/master/src/main/java/workshops/functional/fp2/exercises/ImperativeLoop1.java
In the first exercise you have to count how many times given user has purchased a product. Save result in Map. Check the new Map functionality which was added in Java8 : Map.compute.
Map<String,Integer> counts=new HashMap<>();
//LAB
for (String line : lines) {
// split line
// take user
// fill counts map with [user -> number of occurences]
}
At the end run main and check the result.
Repetition - part 2
Our first task was to count users. The second one is to count how many given product was purchased . Sounds similar? Because it is VERY similar to the first example.
Exercise : count products
Exercise code : https://github.com/PawelWlodarski/workshops/blob/master/src/main/java/workshops/functional/fp2/exercises/ImperativeLoop2.java
Implement whichProductPurchasedTheMost() functionality.
ADDITIONAL
Implement dateWhenMostProductsWasPurchased()
DRY & passing an algorithm - part 3
Here we are going to solve problem with repetition introduced in the previous example by passing a recipe to our algorithm on which field use in logic.
Also this time we will be able to finally write a unit test for part of our funcitonality.
Exercise : Function to extract user
Exercise file : https://github.com/PawelWlodarski/workshops/blob/master/src/main/java/workshops/functional/fp2/exercises/ConfigurableLoop3.java
Write a function to extract user field from a csv line.
static Function<String,String> extractUser = ...
ADDITIONAL 1
Crate curried function which extracts any field.
static Function<Integer,Function<String,String>> extractField
ADDITIONAL 2
Write generic extract function
static <A> Function<String,A> extractToObject(Integer index,Function<String,A> fieldToObject){
throw new UnsupportedOperationException("lab not finished");
}
Make testing easy again - part 4
Now we are going to extract another part of the functionality to make testing easy.
By moving count functionality to external function we can test this part of code independently.
@Test
public void testFieldSummaryFunction() throws Exception {
List<String> input = Arrays.asList("user1", "user2", "user1", "user2", "user1");
Map<String, Integer> output = ConfigurableLoop4Answer.fieldsSummary.apply(input);
assertThat(output).containsEntry("user1",3).containsEntry("user2",2);
}
Exercise : Function to count occurences
Exercise File : https://github.com/PawelWlodarski/workshops/blob/master/src/main/java/workshops/functional/fp2/exercises/ConfigurableLoop4.java
static Function<List<String>,Map<String,Integer>> fieldsSummary
ADDITIONAL
Implement generic variant :
static <A> Function<Collection<A>,Map<A,Integer>> createGenericFieldsSummary(){
throw new UnsupportedOperationException("lab not implemented");
}
Configurable loop - part 5
It's time to move most of fixed functionality outside the loop. This time we are going to split our logic into extracting field and further processing. Where further processing may be anything built from simple functions.
We will also generalize result type.
So our loop function will look like this :
static <B> List<B> queryForMostUsages(Function<String,String> extractField, Function<List<String>,List<B>> createSummary){
Exercise : Function to sort counts and join them into the final report
Exercise File : https://github.com/PawelWlodarski/workshops/blob/master/src/main/java/workshops/functional/fp2/exercises/ConfigurableLoop5.java
static Function<Map<String,Integer>, List<String>> joinSorted
ADDITIONAL :
As a preparation for next phases implement lift high order function which transforms a simple function into a function which operates on a container level
static <A,B> Function<AbstractContainer<A>,AbstractContainer<B>> lift(Function<A,B> pure){
throw new UnsupportedOperationException("lab not completed");
}
Functional abstractions - part 6
Time to create general functional library which will provide all necessary abstraction to build composable logic.
We are going to create special function which is able to take pure function which operates on simple values and lift it into level of collections
Exercise : Implement lift to collection method
Exercise file: https://github.com/PawelWlodarski/workshops/blob/master/src/main/java/workshops/functional/fp2/exercises/ProcessCollection6.java
public static <A,B> Function<Collection<A>,Collection<B>> liftToCollection(Function<A,B> f)
ADDITIONAL
Implement lift to optional
public static <A,B> Function<Optional<A>,Optional<B>> liftToOptional(Function<A,B> f)
Processing engine - part 7
Here we are going to limit our main function into a method which reads a file and then process it's content according to provided algorithm.
Exercise File : https://github.com/PawelWlodarski/workshops/blob/master/src/main/java/workshops/functional/fp2/exercises/ProcessCollection7.java
Exercise : computation as external functions
Implement remove header and complete logic invocation in the main function.
static Function<Collection<String>,Collection<String>> removeHeader
More abstractions - part 8
Lets build another abstractions which could be moved to our FP library.
Exercise : more FP asbtarctions in library
Exercise File : https://github.com/PawelWlodarski/workshops/blob/master/src/main/java/workshops/functional/fp2/exercises/ProcessCollection8.java
Implement general method which produces count function
public static <A> Function<Collection<A>,Map<A,Integer>> countFunction()
ADDITIONAL
Implement sort function :
public static <A> Function<Collection<A>,Collection<A>> sortFunction(Comparator<A> comparator)
Computation pipeline - part 9
As the final step we will add reading file to ours computation pipeline. Because reading file has significant side effect then this facts needs to expressed properly in return type.
Exercise : implement read lines function
static Function<String,List<String>> readLines
ADDITIONAL
Implement safer variant of read line with optionality effect :
static Function<String,Optional<List<String>>> safeReadLines
ADDITIONAL
Use Try mechanism from JavaSlang library to perform calculation in Possible Error Effect instead of Optionality effect
static Function<String,Try<List<String>>> exceptionallySafeReadLines
Final Observations
Now take a look at the main method and observe how the whole computation can be executed and interpreted according to the effects which was used in types.
theProgram.apply("fpjava/purchases.csv").forEach(logger);
safeProgram.apply("fpjava/purchases.csv").orElse(Arrays.asList("THERE WAS UNKNOWN ERROR")).forEach(logger);
exceptionallySafeProgram.apply("fpjava/purchases.csv")
.getOrElseThrow(e->new RuntimeException("ERROR DURING COMPUTATION",e))
.forEach(logger);
Practice
And try to find a solution to the problem : "calculate final amount of money on the account 001". In Functional library you will also find a new function
static <A> Function<Collection<A>,Collection<A>> filterFunction(Predicate<A> p)
You may use following skeleton but it is not necessary.
Function<Collection<String>, Collection<Tuple3<String, Integer, String>>> toTuples = null;
Function<Collection<Tuple3<String, Integer, String>>, Collection<Tuple3<String, Integer, String>>> filter001 = null
Function<Collection<Tuple3<String, Integer, String>>, Collection<Integer>> mapToTransfers = null;
Function<Collection<String>, Collection<Integer>> transformation =
removeHeader.andThen(toTuples).andThen(filter001).andThen(mapToTransfers);
readLines.apply("fpjava/transactions.csv")
.map(transformation)
.map((Collection<Integer> sums)->sums.stream().reduce((i1,i2)->i1+i2))
.getOrElseThrow(e->new RuntimeException("ERROR DURING COMPUTATION",e))
.ifPresent(System.out::println);