Presentation on using the Arrow library for enhanced Functional Programming in the Kotlin Language. Delivered at the Northern Ireland Developer Conference 2018.
2. About Me
Experienced trainer
• 20 years in the trenches
• Over 1000 deliveries
The day job
• Head of Learning at Instil
• Coaching, mentoring etc…
The night job(s)
• Husband and father
• Krav Maga Instructor
3. A quick Kotlin refresher
• Kotlin compared to Java
Introducing Arrow
• What is it and why does it matter?
Arrow Pt1: Data Types
• Option, Try, Either and Validated
Arrow Pt2: Enhanced FP
• Partial Invocation, Currying and Composition
Arrow Pt3: Esoteric Stuff
• Lenses, IO etc…
Agenda For This Talk
5. Enter some numbers or three 'X' to finish
10
20
30
40
50
XXX
Total of numbers is: 150
Enter some numbers or three 'X' to finish
wibble
Ignoring wibble
12
13
14
XXX
Total of numbers is: 39
Enter some numbers or three 'X' to finish
XXX
Total of numbers is: 0
6. public class Program {
public static void main(String[] args) {
@SuppressWarnings("resource")
Scanner scanner = new Scanner(System.in);
List<Integer> numbers = new ArrayList<>();
System.out.println("Enter some numbers or three 'X' to finish");
Pattern endOfInput = Pattern.compile("X{3}");
while(scanner.hasNextLine()) {
if (scanner.hasNext(endOfInput)) {
break;
} else if (scanner.hasNextInt()) {
numbers.add(scanner.nextInt());
} else {
String mysteryText = scanner.nextLine();
System.out.printf("Ignoring %sn", mysteryText);
}
}
int total = numbers.stream().reduce((a,b) -> a + b).orElse(0);
System.out.printf("Total of numbers is: %sn",total);
}
}
A Sample Java Program
7. fun main(args: Array<String>) {
val numbers = mutableListOf<Int>()
val scanner = Scanner(System.`in`)
val endOfInput = Regex("X{3}")
println("Enter some numbers or three 'X' to finish")
while (scanner.hasNextLine()) {
if (scanner.hasNext(endOfInput.toPattern())) {
break
} else if (scanner.hasNextInt()) {
numbers += scanner.nextInt()
} else {
val mysteryText = scanner.nextLine()
println("Ignoring $mysteryText")
}
}
//Would be better to use ‘numbers.sum()’
val total = numbers.fold(0, Int::plus)
println("Total of numbers is: $total")
}
The Sample Re-Written in Kotlin
Points to note:
No redundant class
No semi-colons
Type inference
Both ‘val’ and ‘var’
Helper functions
String interpolation
Simplified collections
Interop with Java types
Simpler use of FP
8. • What is it?
• Why does it matter?
Introducing Arrow
13. Arrow is a functional programming library for Kotlin coders
• Launched in Jan when the two leading libraries joined forces
• To prevent the ‘Scalaz vs. Cats’ debate that exists in Scala
It is not a formal part of the Kotlin environment
• But is generating a lot of interest in the Kotlin community
• It has resulted in proposals for changes to the language
Learning Arrow can be a bit frustrating as:
• The documentation is incomplete and patchy
• Changes are still occurring between releases
• Sample code is hard to find….
I’m still learning what its all about!
• This is very much a report on my progress so far…
Introducing Arrow
17. fun readPropertyA(name: String): Option<String> {
val result = System.getProperty(name)
return if(result != null) Some(result) else None
}
fun readPropertyB(name: String) =
Option.fromNullable(System.getProperty(name))
Reading Property Values Safely Via Option
<<abstract>>
Option
Some None
Empty SetSet of One
18. fun print1(input: Option<String>): Unit {
when(input) {
is None -> println("Nothing was found")
is Some -> println("'${input.t}' was found")
}
}
fun print2(input: Option<String>): Unit {
println("${input.getOrElse { "Nothing" }} was found")
}
Reading Property Values Safely Via Option
19. fun print3(input: Option<String>): Unit {
val result = input.fold({ "Nothing" }, { it })
println("$result was found")
}
fun print4(input1: Option<String>, input2: Option<String>): Unit {
val result = input1.flatMap { first ->
input2.map { second ->
"$first and $second"
}
}
println("Results are ${result.getOrElse { "Nothing" }}")
}
Reading Property Values Safely Via Option
21. Reading Property Values Safely Via Option
'1.8.0_121' was found
Nothing was found
---------------
1.8.0_121 was found
Nothing was found
---------------
1.8.0_121 was found
Nothing was found
---------------
Results are 1.8.0_121 and 1.8.0_121
---------------
Results are Nothing
---------------
Results are Nothing
---------------
Results are Nothing
22. class Postcode(val input: String?) {
fun value() = Option.fromNullable(input)
}
class Address(val street: String, val postcode: Postcode?) {
fun location() = Option.fromNullable(postcode)
}
class Person(val name: String, val address: Address?) {
fun residence() = Option.fromNullable(address)
}
Using Option as a Monad
23. fun printPostcode(person: Person) {
val result = Option.monad().binding {
val address = person.residence().bind()
val location = address.location().bind()
location.value().bind()
}.fix()
println(result.fold( { "No postcode available" },
{ "Postcode of $it" }))
}
Using Option as a Monad
24. fun main(args: Array<String>) {
printPostcode(Person("Dave",
Address("10 Arcatia Road",
Postcode("ABC 123"))))
printPostcode(Person("Dave",
Address("10 Arcatia Road", null)))
printPostcode(Person("Dave", null))
}
Using Option as a Monad
Postcode of ABC 123
No postcode available
No postcode available
26. fun firstLine(path: String): Try<String> {
fun readFirstLine(path: String): String {
val reader = BufferedReader(FileReader(path))
return reader.use { it.readLine() }
}
return Try { readFirstLine(path) }
}
Reading the First Line from a File via Try
<<abstract>>
Try
Success Failure
Holds ErrorHolds Result
27. fun print1(input: Try<String>): Unit {
when(input) {
is Success -> println("Read '${input.value}'")
is Failure -> println("Threw '${input.exception.message}'")
}
}
fun print2(input: Try<String>): Unit {
val result = input.fold( { "Threw '${it.message}'" },
{ "Read '$it'" })
println(result)
}
fun print3(input: Try<String>): Unit {
input.map { println("Read '$it'") }
input.recover { println("Threw '${it.message}'") }
}
Reading the First Line from a File via Try
29. fun print4(input: String) {
fun fullPath(str: String) = "data/$str"
val finalResult = firstLine(fullPath(input)).flatMap { one ->
firstLine(fullPath(one)).flatMap { two ->
firstLine(fullPath(two)).flatMap { three ->
firstLine(fullPath(three)).flatMap { four ->
firstLine(fullPath(four)).map { result ->
result
}
}
}
}
}
val message = finalResult.fold({ it.message }, { it })
println("Path navigation produced '$message'")
}
Traversing Across Files
30. fun main(args: Array<String>) {
print1(firstLine("data/input4.txt"))
print1(firstLine("foobar.txt"))
printLine()
print2(firstLine("data/input4.txt"))
print2(firstLine("foobar.txt"))
printLine()
print3(firstLine("data/input4.txt"))
print3(firstLine("foobar.txt"))
printLine()
print4("input.txt")
print4("foobar.txt")
}
Reading the First Line from a File via Try
31. Traversing Across Files
Read 'Fortune favors the prepared mind'
Threw 'foobar.txt (No such file or directory)'
---------------
Read 'Fortune favors the prepared mind'
Threw 'foobar.txt (No such file or directory)'
---------------
Read 'Fortune favors the prepared mind'
Threw 'foobar.txt (No such file or directory)'
---------------
Path navigation produced 'Fortune favors the prepared mind'
Path navigation produced 'data/foobar.txt (No such file or directory)'
32. fun readFromFiles(input: String): String? {
val result = Try.monad().binding {
val one = firstLine(fullPath(input)).bind()
val two = firstLine(fullPath(one)).bind()
val three = firstLine(fullPath(two)).bind()
val four = firstLine(fullPath(three)).bind()
firstLine(fullPath(four)).bind()
}.fix()
return result.fold({ it.message }, { it })
}
Traversing Across Files
33. fun main(args: Array<String>) {
println("Path navigation produced '${readFromFiles("input.txt")}'")
println("Path navigation produced '${readFromFiles("foobar")}'")
}
Traversing Across Files
Path navigation produced 'Fortune favors the prepared mind'
Path navigation produced 'data/foobar (No such file or directory)'
35. fun genNumber() : Either<Int, Int> {
val number = (random() * 100).toInt()
return if(number % 2 == 0) Right(number)
else Left(number)
}
Using the Either Type
<<abstract>>
Either
Left Right
Happy PathAlternative
36. fun main(args: Array<String>) {
val results = (1 .. 10).map {
genNumber().flatMap { first ->
genNumber().map { second ->
Pair(first, second)
}
}
}
results.forEach { result ->
val msg = result.fold(
{ "Odd number $it" },
{ "Even numbers ${it.first} and ${it.second}" }
)
println(msg)
}
}
Using the Either Type Even numbers 50 and 40
Odd number 77
Even numbers 52 and 32
Odd number 25
Odd number 89
Even numbers 80 and 54
Odd number 65
Odd number 1
Odd number 1
Odd number 33
37. fun checkNum(number:Int) : Either<Int, Int> {
return if(number % 2 == 0) Right(number) else Left(number)
}
class EitherTest : ShouldSpec() {
init {
should("be able to detect Right") {
checkNum(4).shouldBeRight(4)
}
should("be able to detect Left") {
checkNum(5).shouldBeLeft(5)
}
}
}
All Arrow Types are Supported in KotlinTest
39. Our Sample Problem
Whats your ID?
ab12
How old are you?
19
Where do you work?
HR
Error: Bad ID
Whats your ID?
AB12
How old are you?
19
Where do you work?
IT
Result: AB12 of age 19 working in IT
Whats your ID?
ab12
How old are you?
14
Where do you work?
Mars
Error: Bad Dept Bad ID Bad Age
40. class Employee(val id: String, val age: Int, val dept: String) {
override fun toString() = "$id of age $age working in $dept"
}
fun askQuestion(question: String): String {
println(question)
return readLine() ?: ""
}
Reading and Validating Information
<<abstract>>
Validated
Invalid Valid
ResultMessage
41. fun checkID(): Validated<String, String> {
val regex = Regex("[A-Z]{2}[0-9]{2}")
val response = askQuestion("Whats your ID?")
return if(regex.matches(response)) Valid(response)
else Invalid("Bad ID")
}
fun checkAge(): Validated<String, Int> {
val response = askQuestion("How old are you?").toInt()
return if(response > 16) Valid(response) else Invalid("Bad Age")
}
fun checkDept(): Validated<String, String> {
val depts = listOf("HR", "Sales", "IT")
val response = askQuestion("Where do you work?")
return if(depts.contains(response)) Valid(response)
else Invalid("Bad Dept")
}
Reading and Validating Information
42. fun main(args: Array<String>) {
val sg = object : Semigroup<String> {
override fun String.combine(b: String) = "$this $b"
}
val id = checkID()
val age = checkAge()
val dept = checkDept()
val result = Validated.applicative(sg)
.map(id, age, dept, {
(a,b,c) -> Employee(a,b,c)
})
.fix()
println(result.fold({ "Error: $it" }, {"Result: $it"} ))
}
Reading and Validating Information
44. Our example of Validated contained a bug…
• We assumed the value given for ‘age’ would be an integer
Composing Types
Whats your ID?
AB12
How old are you?
abc
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at arrow.types.validated.ProgramKt.checkAge(Program.kt:22)
at arrow.types.validated.ProgramKt.main(Program.kt:37)
45. fun checkAge(): Validated<String, Int> {
val response = askQuestion("How old are you?").toInt()
return if(response > 16) Valid(response)
else Invalid("Bad Age")
}
Composing Types
46. We can solve the problem by composing types together
• Our methods should return a Try<Validated<T,U>>
Composing Types
47. fun checkID(): Try<Validated<String, String>> {
val regex = Regex("[A-Z]{2}[0-9]{2}")
val response = askQuestion("Whats your ID?")
return Try { if(regex.matches(response)) Valid(response)
else Invalid("Bad ID") }
}
fun checkAge(): Try<Validated<String, Int>> {
val response = Try { askQuestion("How old are you?").toInt() }
return response.map({ num -> if(num > 16) Valid(num)
else Invalid("Bad Age")})
}
Composing Types
48. fun checkSalary(): Try<Validated<String, Double>> {
val response = Try { askQuestion("What is your salary?")
.toDouble() }
return response.map({ num -> if(num > 15000.0) Valid(num)
else Invalid("Bad Salary") })
}
fun checkDept(): Try<Validated<String, String>> {
val depts = listOf("HR", "Sales", "IT")
val response = askQuestion("Where do you work?")
return Try { if(depts.contains(response)) Valid(response)
else Invalid("Bad Dept") }
}
Composing Types
49. fun success(emp: Employee)
= println("Created $emp")
fun exception(ex: Throwable)
= println("Exception occurred - ${ex.message}")
fun invalid(msg: String?)
= println("Validation error occurred - $msg")
class Employee(val id: String, val age: Int,
val dept: String, val salary: Double) {
constructor(t: Tuple4<String,Int,String,Double>) :
this(t.a,t.b,t.c,t.d)
override fun toString()
= "$id of age $age working in $dept earning $salary"
}
Composing Types
51. fun main(args: Array<String>) {
val app = Validated.applicative(object : Semigroup<String> {
override fun String.combine(b: String) = "$this $b"
})
Try.monad().binding {
val id = checkID().bind()
val age = checkAge().bind()
val dept = checkDept().bind()
val salary = checkSalary().bind()
app.map(id, age, dept, salary, ::Employee).fix()
}.fix().fold(
{exception(it)},
{it.fold(::invalid,::success)})
}
Composing Types
52. Composing Types
Whats your ID?
AB12
How old are you?
abc
Exception occurred - For input string: "abc"
Whats your ID?
AB12
How old are you?
21
Where do you work?
Sales
What is your salary?
abc
Exception occurred - For input string: "abc"
Whats your ID?
foo
How old are you?
21
Where do you work?
Sales
What is your salary?
30000.0
Validation error occurred - Bad ID
Whats your ID?
AB12
How old are you?
21
Where do you work?
foo
What is your salary?
30000.0
Validation error occurred - Bad Dept
53. Composing Types
Whats your ID?
AB12
How old are you?
21
Where do you work?
Sales
What is your salary?
30000.0
Created AB12 of age 21 working in Sales earning 30000.0
56. fun demo1() {
val addNums = { no1: Int, no2: Int ->
println("Adding $no1 to $no2")
no1 + no2
}
val addSeven = addNums.partially2(7)
val result = addSeven(3)
println(result)
}
Partial Invocation
Adding 3 to 7
10
57. fun demo2() {
val addNums = { no1: Int, no2: Int ->
println("Adding $no1 to $no2")
no1 + no2
}
val addSeven = addNums.partially2(7)
val addSevenToThree = addSeven.partially1(3)
val result = addSevenToThree()
println(result)
}
Partial Invocation
Adding 3 to 7
10
58. fun demo3() {
val addNums = { no1: Int, no2: Int ->
println("Adding $no1 to $no2")
no1 + no2
}
val addSeven = addNums.reverse().partially2(7)
val result = addSeven(3)
println(result)
}
Partial Invocation
Adding 7 to 3
10
59. fun grep(path: String, regex: Regex, action: (String) -> Unit) {
val reader = BufferedReader(FileReader(path))
reader.use {
it.lines()
.filter { regex.matches(it) }
.forEach(action)
}
}
val grepLambda = { a: String, b: Regex, c: (String) -> Unit ->
grep(a, b, c)
}
fun printLine() = println("-------------")
Here’s Something More Useful
60. val filePath = "data/grepInput.txt"
val regex = "[A-Z]{2}[0-9]{2}".toRegex()
grep(filePath, regex, ::println)
printLine()
grepLambda(filePath, regex, ::println)
printLine()
Here’s Something More Useful
AB12
CD34
EF56
-------------
AB12
CD34
EF56
-------------
61. val grepAndPrint = grepLambda.partially3(::println)
grepAndPrint(filePath, regex)
printLine()
val sb = StringBuilder()
val grepAndConcat = grepLambda.partially3 {sb.append(it)}
grepAndConcat(filePath, regex)
println(sb.toString())
printLine()
val grepAndPrintRegex = grepAndPrint.partially2(regex)
grepAndPrintRegex(filePath)
Here’s Something More Useful
AB12
CD34
EF56
-------------
AB12CD34EF56
-------------
AB12
CD34
EF56
63. val addThree = { a:Int, b: Int, c:Int ->
println("Adding $a, $b and $c")
a + b + c
}
fun printLine() = println("--------------------")
fun main(args: Array<String>) {
println(addThree(10,20,40))
printLine()
val f1 = addThree.curried()
val f2 = f1(10)
val f3 = f2(20)
val result = f3(40)
println(result)
printLine()
println(f1(10)(20)(40))
printLine()
val f4 = addThree.reverse().curried()
println(f4(10)(20)(40))
println()
}
The Basic Syntax of Currying
Adding 10, 20 and 40
70
--------------------
Adding 10, 20 and 40
70
--------------------
Adding 10, 20 and 40
70
--------------------
Adding 40, 20 and 10
70
64. fun grep(path: String, regex: Regex, action: (String) -> Unit) {
val reader = BufferedReader(FileReader(path))
reader.use {
it.lines()
.filter { regex.matches(it) }
.forEach(action)
}
}
val grepLambda = { a: String, b: Regex, c: (String) -> Unit ->
grep(a,b,c) }
fun printLine() = println("-------------")
A More Useful Example
65. fun main(args: Array<String>) {
val filePath = "data/grepInput.txt"
val regex1 = "[A-Z]{2}[0-9]{2}".toRegex()
val regex2 = "[a-z]{2}[0-9]{2}".toRegex()
val f1 = grepLambda.curried()
val grepInFile = f1(filePath)
val grepRegex1 = grepInFile(regex1)
val grepRegex2 = grepInFile(regex2)
grepRegex1(::println)
printLine()
grepRegex2(::println)
}
A More Useful Example
AB12
CD34
EF56
-------------
ab12
cd34
ef56
71. @optics
data class Postcode(val value: String) {
override fun toString() = "$value"
}
@optics
data class Address(val street: String, val postcode: Postcode) {
override fun toString() = "$street ($postcode)"
}
@optics
data class Person(val name: String, val address: Address) {
override fun toString() = "$name living at $address"
}
Copying Immutable Structures With Lenses
72. fun main(args: Array<String>) {
val oldPerson = Person("Dave",
Address("10 Arcatia Road",
Postcode("BT26 ABC")))
println(oldPerson)
val personAddressPostcode
= personAddress() compose addressPostcode() compose postcodeValue()
val newPerson
= personAddressPostcode.modify(oldPerson, { _ -> "BT37 DEF" })
println(newPerson)
}
Copying Immutable Structures With Lenses
Dave living at 10 Arcatia Road (BT26 ABC)
Dave living at 10 Arcatia Road (BT37 DEF)