5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## The official repository for the Advanced Scala 3 & Functional Programming course
3 |
4 | Powered by [Rock the JVM!](rockthejvm.com)
5 |
6 | This repository contains the code we wrote during the Rock the JVM [Advanced Scala 3 & Functional Programming course](https://rockthejvm.com/course/advanced-scala). Unless explicitly mentioned, the code in this repository is exactly what was caught on camera.
7 |
8 | ### Installation
9 |
10 | How to install:
11 | - either clone the repo or download as zip
12 | - open with IntelliJ as it's a simple IDEA project
13 |
14 | ### Getting Started
15 |
16 | Run this command in a git terminal to reset the code in its starting/clean state:
17 |
18 | ```
19 | git checkout start
20 | ```
21 |
22 | This repo also has Git tags for intermediate states of the code while we were working in the course. You can check out the appropriate tags for the different stages of the course. Useful especially for longer exercises where we modify the same code over multiple videos.
23 |
24 | The tags are:
25 |
26 | - `start`
27 | - `1.1-scala-recap`
28 | - `1.2-dark-sugars`
29 | - `1.3-advanced-pattern-matching`
30 | - `2.1-partial-functions`
31 | - `2.2-functional-set`
32 | - `2.3-functional-collections`
33 | - `2.3-functional-set-part-2`
34 | - `2.4-curries-pafs`
35 | - `2.5-lazy-eval`
36 | - `2.6-lazy-lists`
37 | - `2.7-lazy-lists-part-2`
38 | - `2.8-monads`
39 | - `2.9-monads-exercise`
40 | - `3.1-jvm-thread-intro`
41 | - `3.2-jvm-concurrency-problems`
42 | - `3.3-jvm-thread-communication`
43 | - `3.4-jvm-thread-communication-2`
44 | - `3.5-jvm-thread-communication-3`
45 | - `3.6-futures-intro`
46 | - `3.7-futures-fp`
47 | - `3.8-futures-blocking`
48 | - `3.9-futures-promises`
49 | - `3.10-futures-exercises`
50 | - `3.11-parallel-collections`
51 | - `4.1-givens`
52 | - `4.2-extension-methods`
53 | - `4.3-organization`
54 | - `4.4-type-classes`
55 | - `4.5-type-classes-practice-json`
56 | - `4.6-context-functions`
57 | - `4.7-implicit-conversions`
58 | - `4.8-implicits`
59 | - `5.1-advanced-inheritance`
60 | - `5.2-variance`
61 | - `5.3-variance-positions`
62 | - `5.4-type-members`
63 | - `5.5-path-dependent-types`
64 | - `5.6-opaque-types`
65 | - `5.7-lit-union-intersection-types`
66 | - `5.8-self-types`
67 | - `5.9-f-bounded-polymorphism`
68 | - `5.10-structural-types`
69 | - `5.11-higher-kinded-types`
70 |
71 |
72 | ### Contributions
73 |
74 | If you have changes to suggest to this repo, either
75 | - submit a GitHub issue
76 | - tell me in the course Q/A forum
77 | - submit a pull request!
78 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | val scala3Version = "3.0.0"
2 |
3 | lazy val root = project
4 | .in(file("."))
5 | .settings(
6 | name := "scala-3-advanced",
7 | version := "0.1.0",
8 |
9 | scalaVersion := scala3Version,
10 |
11 | libraryDependencies ++= Seq(
12 | "com.novocode" % "junit-interface" % "0.11" % "test",
13 | "org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.3"
14 | )
15 | )
16 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.9.9
2 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part1as/AdvancedPatternMatching.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part1as
2 |
3 | object AdvancedPatternMatching {
4 |
5 | /*
6 | PM:
7 | - constants
8 | - objects
9 | - wildcards
10 | - variables
11 | - infix patterns
12 | - lists
13 | - case classes
14 | */
15 |
16 | // use-case: you want to pattern match against an existing class (maybe from a Java library)
17 | class Person(val name: String, val age: Int)
18 |
19 | object Person {
20 | def unapply(person: Person): Option[(String, Int)] = // person match { case Person(string, int) => }
21 | if (person.age < 21) None
22 | else Some((person.name, person.age))
23 |
24 | def unapply(age: Int): Option[String] = // int match { case Person(string) => ... }
25 | if (age < 21) Some("minor")
26 | else Some("legally allowed to drink")
27 | }
28 |
29 | val daniel = new Person("Daniel", 102)
30 | val danielPM = daniel match { // Person.unapply(daniel) => Option((n, a))
31 | case Person(n, a) => s"Hi there, I'm $n"
32 | }
33 |
34 | val danielsLegalStatus = daniel.age match {
35 | case Person(status) => s"Daniel's legal drinking status is $status"
36 | }
37 |
38 | // boolean patterns
39 | object even {
40 | def unapply(arg: Int): Boolean = arg % 2 == 0
41 | }
42 |
43 | object singleDigit {
44 | def unapply(arg: Int): Boolean = arg > -10 && arg < 10
45 | }
46 |
47 | val n: Int = 43
48 | val mathProperty = n match {
49 | case even() => "an even number"
50 | case singleDigit() => "a one digit number"
51 | case _ => "no special property"
52 | }
53 |
54 | // infix patterns
55 | infix case class Or[A, B](a: A, b: B)
56 | val anEither = Or(2, "two")
57 | val humanDescriptionEither = anEither match {
58 | case number Or string => s"$number is written as $string"
59 | }
60 |
61 | val aList = List(1,2,3)
62 | val listPM = aList match {
63 | case 1 :: rest => "a list starting with 1"
64 | case _ => "some uninteresting list"
65 | }
66 |
67 | // decomposing sequences
68 | val vararg = aList match {
69 | case List(1, _*) => "list starting with 1"
70 | case _ => "some other list"
71 | }
72 |
73 | abstract class MyList[A] {
74 | def head: A = throw new NoSuchElementException
75 | def tail: MyList[A] = throw new NoSuchElementException
76 | }
77 | case class Empty[A]() extends MyList[A]
78 | case class Cons[A](override val head: A, override val tail: MyList[A]) extends MyList[A]
79 |
80 | object MyList {
81 | def unapplySeq[A](list: MyList[A]): Option[Seq[A]] =
82 | if (list == Empty()) Some(Seq.empty)
83 | else unapplySeq(list.tail).map(restOfSequence => list.head +: restOfSequence)
84 | }
85 |
86 | val myList: MyList[Int] = Cons(1, Cons(2, Cons(3, Empty())))
87 | val varargCustom = myList match {
88 | case MyList(1, _*) => "list starting with 1"
89 | case _ => "some other list"
90 | }
91 |
92 | // custom return type for unapply
93 | abstract class Wrapper[T] {
94 | def isEmpty: Boolean
95 | def get: T
96 | }
97 |
98 | object PersonWrapper {
99 | def unapply(person: Person): Wrapper[String] = new Wrapper[String] {
100 | override def isEmpty = false
101 | override def get = person.name
102 | }
103 | }
104 |
105 | val weirdPersonPM = daniel match {
106 | case PersonWrapper(name) => s"Hey my name is $name"
107 | }
108 |
109 | def main(args: Array[String]): Unit = {
110 | println(danielPM)
111 | println(danielsLegalStatus)
112 | println(mathProperty)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part1as/DarkSugars.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part1as
2 |
3 | import scala.util.Try
4 |
5 | object DarkSugars {
6 |
7 | // 1 - sugar for methods with one argument
8 | def singleArgMethod(arg: Int): Int = arg + 1
9 |
10 | val aMethodCall = singleArgMethod({
11 | // long code
12 | 42
13 | })
14 |
15 | val aMethodCall_v2 = singleArgMethod {
16 | // long code
17 | 42
18 | }
19 |
20 | // example: Try, Future
21 | val aTryInstance = Try {
22 | throw new RuntimeException
23 | }
24 |
25 | // with hofs
26 | val anIncrementedList = List(1,2,3).map { x =>
27 | // code block
28 | x + 1
29 | }
30 |
31 | // 2 - single abstract method pattern (since Scala 2.12)
32 | trait Action {
33 | // can also have other implmented fields/methods here
34 | def act(x: Int): Int
35 | }
36 |
37 | val anAction = new Action {
38 | override def act(x: Int) = x + 1
39 | }
40 |
41 | val anotherAction: Action = (x: Int) => x + 1 // new Action { def act(x: Int) = x + 1 }
42 |
43 | // example: Runnable
44 | val aThread = new Thread(new Runnable {
45 | override def run(): Unit = println("Hi, Scala, from another thread")
46 | })
47 |
48 | val aSweeterThread = new Thread(() => println("Hi, Scala"))
49 |
50 | // 3 - methods ending in a : are RIGHT-ASSOCIATIVE
51 | val aList = List(1,2,3)
52 | val aPrependedList = 0 :: aList // aList.::(0)
53 | val aBigList = 0 :: 1 :: 2 :: List(3,4) // List(3,4).::(2).::(1).::(0)
54 |
55 | class MyStream[T] {
56 | infix def -->:(value: T): MyStream[T] = this // impl not important
57 | }
58 |
59 | val myStream = 1 -->: 2 -->: 3 -->: 4 -->: new MyStream[Int]
60 |
61 | // 4 - multi-word identifiers
62 | class Talker(name: String) {
63 | infix def `and then said`(gossip: String) = println(s"$name said $gossip")
64 | }
65 |
66 | val daniel = new Talker("Daniel")
67 | val danielsStatement = daniel `and then said` "I love Scala"
68 |
69 | // example: HTTP libraries
70 | object `Content-Type` {
71 | val `application/json` = "application/JSON"
72 | }
73 |
74 | // 5 - infix types
75 | import scala.annotation.targetName
76 | @targetName("Arrow") // for more readable bytecode + Java interop
77 | infix class -->[A, B]
78 | val compositeType: Int --> String = new -->[Int, String]
79 |
80 | // 6 - update()
81 | val anArray = Array(1,2,3,4)
82 | anArray.update(2, 45)
83 | anArray(2) = 45 // same
84 |
85 | // 7 - mutable fields
86 | class Mutable {
87 | private var internalMember: Int = 0
88 | def member = internalMember // "getter"
89 | def member_=(value: Int): Unit =
90 | internalMember = value // "setter"
91 | }
92 |
93 | val aMutableContainer = new Mutable
94 | aMutableContainer.member = 42 // aMutableContainer.member_=(42)
95 |
96 | // 8 - variable arguments (varargs)
97 | def methodWithVarargs(args: Int*) = {
98 | // return the number of arguments supplied
99 | args.length
100 | }
101 |
102 | val callWithZeroArgs = methodWithVarargs()
103 | val callWithOneArgs = methodWithVarargs(78)
104 | val callWithTwoArgs = methodWithVarargs(12, 34)
105 |
106 | val aCollection = List(1,2,3,4)
107 | val callWithDynamicArgs = methodWithVarargs(aCollection*)
108 |
109 | def main(args: Array[String]): Unit = {
110 |
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part1as/Recap.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part1as
2 |
3 | import scala.annotation.tailrec
4 |
5 | object Recap {
6 |
7 | // values, types, expressions
8 | val aCondition = false // vals are constants
9 | val anIfExpression = if (aCondition) 42 else 55 // expressions evaluate to a value
10 |
11 | val aCodeBlock = {
12 | if (aCondition) 54
13 | 78
14 | }
15 |
16 | // types: Int, String, Double, Boolean, Char, ...
17 | // Unit = () == "void" in other languages
18 | val theUnit = println("Hello, Scala")
19 |
20 | // functions
21 | def aFunction(x: Int): Int = x + 1
22 |
23 | // recursion: stack & tail
24 | @tailrec def factorial(n: Int, acc: Int): Int =
25 | if (n <= 0) acc
26 | else factorial(n - 1, n * acc)
27 |
28 | val fact10 = factorial(10, 1)
29 |
30 | // object oriented programming
31 | class Animal
32 | class Dog extends Animal
33 | val aDog: Animal = new Dog
34 |
35 | trait Carnivore {
36 | infix def eat(a: Animal): Unit
37 | }
38 |
39 | class Crocodile extends Animal with Carnivore {
40 | override infix def eat(a: Animal): Unit = println("I'm a croc, I eat everything")
41 | }
42 |
43 | // method notation
44 | val aCroc = new Crocodile
45 | aCroc.eat(aDog)
46 | aCroc eat aDog // "operator"/infix position
47 |
48 | // anonymous classes
49 | val aCarnivore = new Carnivore {
50 | override infix def eat(a: Animal): Unit = println("I'm a carnivore")
51 | }
52 |
53 | // generics
54 | abstract class LList[A] {
55 | // type A is known inside the implementation
56 | }
57 | // singletons and companions
58 | object LList // companion object, used for instance-independent ("static") fields/methods
59 |
60 | // case classes
61 | case class Person(name: String, age: Int)
62 |
63 | // enums
64 | enum BasicColors {
65 | case RED, GREEN, BLUE
66 | }
67 |
68 | // exceptions and try/catch/finally
69 | def throwSomeException(): Int =
70 | throw new RuntimeException
71 |
72 | val aPotentialFailure = try {
73 | // code that might fail
74 | throwSomeException()
75 | } catch {
76 | case e: Exception => "I caught an exception"
77 | } finally {
78 | // closing resources
79 | println("some important logs")
80 | }
81 |
82 | // functional programming
83 | val incrementer = new Function1[Int, Int] {
84 | override def apply(x: Int) = x + 1
85 | }
86 |
87 | val two = incrementer(1)
88 |
89 | // lambdas
90 | val anonymousIncrementer = (x: Int) => x + 1
91 | // hofs = higher-order functions
92 | val anIncrementerList = List(1,2,3).map(anonymousIncrementer) // [2,3,4]
93 | // map, flatMap, filter
94 |
95 | // for-comprehensions
96 | val pairs = for {
97 | number <- List(1,2,3)
98 | char <- List('a', 'b')
99 | } yield s"$number-$char"
100 |
101 | // Scala collections: Seqs, Arrays, Lists, Vectors, Maps, Tupes, Sets
102 |
103 | // options, try
104 | val anOption: Option[Int] = Option(42)
105 |
106 | // pattern matching
107 | val x = 2
108 | val order = x match {
109 | case 1 => "first"
110 | case 2 => "second"
111 | case _ => "not important"
112 | }
113 |
114 | val bob = Person("Bob", 22)
115 | val greeting = bob match {
116 | case Person(n, _) => s"Hi, my name is $n"
117 | }
118 |
119 | // braceless syntax
120 | val pairs_v2 =
121 | for
122 | number <- List(1,2,3)
123 | char <- List('a', 'b')
124 | yield s"$number-$char"
125 | // same for if, match, while
126 |
127 | // indentation tokens
128 | class BracelessAnimal:
129 | def eat(): Unit =
130 | println("I'm doing something")
131 | println("I'm eating")
132 | end eat
133 | end BracelessAnimal
134 |
135 | def main(args: Array[String]): Unit = {
136 |
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part2afp/CurryingPAFs.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part2afp
2 |
3 | object CurryingPAFs {
4 |
5 | // currying
6 | val superAdder: Int => Int => Int =
7 | x => y => x + y
8 |
9 | val add3: Int => Int = superAdder(3) // y => 3 + y
10 | val eight = add3(5) // 8
11 | val eight_v2 = superAdder(3)(5)
12 |
13 | // curried methods
14 | def curriedAdder(x: Int)(y: Int): Int =
15 | x + y
16 |
17 | // methods != function values
18 |
19 | // converting methods to functions = eta-expansion
20 | val add4 = curriedAdder(4)
21 | val nine = add4(5) // 9
22 |
23 | def increment(x: Int): Int = x + 1
24 | val aList = List(1,2,3)
25 | val anIncrementedList = aList.map(increment) // eta-expansion
26 |
27 | // underscores are powerful: allow you to decide the shapes of lambdas obtained from methods
28 | def concatenator(a: String, b: String, c: String): String = a + b + c
29 | val insertName = concatenator(
30 | "Hello, my name is ",
31 | _: String,
32 | ", I'm going to show you a nice Scala trick."
33 | ) // x => concatenator("...", x, "...")
34 |
35 | val danielsGreeting = insertName("Daniel") // concatenator("...", "Daniel", "...")
36 | val fillInTheBlanks = concatenator(_: String, "Daniel", _: String) // (x, y) => concatenator(x, "Daniel", y)
37 | val danielsGreeting_v2 = fillInTheBlanks("Hi,", "how are you?")
38 |
39 | /**
40 | * Exercises
41 | */
42 | val simpleAddFunction = (x: Int, y: Int) => x + y
43 | def simpleAddMethod(x: Int, y: Int) = x + y
44 | def curriedMethod(x: Int)(y: Int) = x + y
45 |
46 | // 1 - obtain an add7 function: x => x + 7 out of these 3 definitions
47 | val add7 = (x: Int) => simpleAddFunction(x, 7)
48 | val add7_v2 = (x: Int) => simpleAddMethod(x, 7)
49 | val add7_v3 = (x: Int) => curriedMethod(7)(x)
50 | val add7_v4 = curriedMethod(7)
51 | val add7_v5 = simpleAddMethod(7, _)
52 | val add7_v6 = simpleAddMethod(_, 7)
53 | val add7_v7 = simpleAddFunction.curried(7)
54 |
55 | // 2 - process a list of numbers and return their string representations under different formats
56 | // step 1: create a curried formatting method with a formatting string and a value
57 | def curriedFormatter(fmt: String)(number: Double): String = fmt.format(number)
58 | // step 2: process a list of numbers with various formats
59 | val piWith2Dec = "%8.6f".format(Math.PI) // 3.14
60 | val someDecimals = List(Math.PI, Math.E, 1, 9.8, 1.3e-12)
61 |
62 | // methods vs functions + by-name vs 0-lambdas
63 | def byName(n: => Int) = n + 1
64 | def byLambda(f: () => Int) = f() + 1
65 |
66 | def method: Int = 42
67 | def parenMethod(): Int = 42
68 |
69 | byName(23) // ok
70 | byName(method) // 43. eta-expanded? NO - method is INVOKED here
71 | byName(parenMethod()) // 43
72 | // byName(parenMethod) // not ok
73 | byName((() => 42)()) // ok
74 | // byName(() => 42) // not ok
75 |
76 | // byLambda(23) // not ok
77 | // byLambda(method) // eta-expansion is NOT possible
78 | byLambda(parenMethod) // eta-expansion is done
79 | byLambda(() => 42)
80 | byLambda(() => parenMethod()) // ok
81 |
82 | def main(args: Array[String]): Unit = {
83 | println(someDecimals.map(curriedFormatter("%4.2f")))
84 | println(someDecimals.map(curriedFormatter("%8.6f")))
85 | println(someDecimals.map(curriedFormatter("%16.14f")))
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part2afp/FunctionalCollections.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part2afp
2 |
3 | object FunctionalCollections {
4 |
5 | // sets are functions A => Boolean
6 | val aSet: Set[String] = Set("I", "love", "Scala")
7 | val setContainsScala = aSet("Scala") // true
8 |
9 | // Seq extends PartialFunction[Int, A]
10 | val aSeq: Seq[Int] = Seq(1,2,3,4)
11 | val anElement = aSeq(2) // 3
12 | // val aNonExistingElement = aSeq(100) // throws an OOBException
13 |
14 | // Map[K,V] "extends" PartialFunction[K, V]
15 | val aPhonebook: Map[String, Int] = Map(
16 | "Alice" -> 123456,
17 | "Bob" -> 987654
18 | )
19 | val alicesPhone = aPhonebook("Alice")
20 | // val danielsPhone = aPhonebook("Daniel") // throws a NoSuchElementException
21 |
22 | def main(args: Array[String]): Unit = {
23 |
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part2afp/LazyEvaluation.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part2afp
2 |
3 | object LazyEvaluation {
4 |
5 | lazy val x: Int = {
6 | println("Hello")
7 | 42
8 | }
9 |
10 | // lazy DELAYS the evaluation of a value until the first use
11 | // evaluation occurs ONCE
12 |
13 | /*
14 | Example 1: call by need
15 | */
16 | def byNameMethod(n: => Int): Int =
17 | n + n + n + 1
18 |
19 | def retrieveMagicValue() = {
20 | println("waiting...")
21 | Thread.sleep(1000)
22 | 42
23 | }
24 |
25 | def demoByName(): Unit = {
26 | println(byNameMethod(retrieveMagicValue()))
27 | // retrieveMagicValue() + retrieveMagicValue() + retrieveMagicValue() + 1
28 | }
29 |
30 | // call by need = call by name + lazy values
31 | def byNeedMethod(n: => Int): Int = {
32 | lazy val lazyN = n // memoization
33 | lazyN + lazyN + lazyN + 1
34 | }
35 |
36 | def demoByNeed(): Unit = {
37 | println(byNeedMethod(retrieveMagicValue()))
38 | }
39 |
40 | /*
41 | Example 2: withFilter
42 | */
43 | def lessThan30(i: Int): Boolean = {
44 | println(s"$i is less than 30?")
45 | i < 30
46 | }
47 |
48 | def greaterThan20(i: Int): Boolean = {
49 | println(s"$i is greater than 20?")
50 | i > 20
51 | }
52 |
53 | val numbers = List(1, 25, 40, 5, 23)
54 |
55 | def demoFilter(): Unit = {
56 | val lt30 = numbers.filter(lessThan30)
57 | val gt20 = lt30.filter(greaterThan20)
58 | println(gt20)
59 | }
60 |
61 | def demoWithFilter(): Unit = {
62 | val lt30 = numbers.withFilter(lessThan30)
63 | val gt20 = lt30.withFilter(greaterThan20)
64 | println(gt20.map(identity))
65 | }
66 |
67 | def demoForComprehension(): Unit = {
68 | val forComp = for {
69 | n <- numbers if lessThan30(n) && greaterThan20(n)
70 | } yield n
71 | println(forComp)
72 | }
73 |
74 | def main(args: Array[String]): Unit = {
75 | demoForComprehension()
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part2afp/Monads.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part2afp
2 |
3 | import scala.annotation.targetName
4 |
5 | object Monads {
6 |
7 | def listStory(): Unit = {
8 | val aList = List(1,2,3)
9 | val listMultiply = for {
10 | x <- List(1,2,3)
11 | y <- List(4,5,6)
12 | } yield x * y
13 | // for comprehensions = chains of map + flatMap
14 | val listMultiply_v2 = List(1,2,3).flatMap(x => List(4,5,6).map(y => x * y))
15 |
16 | val f = (x: Int) => List(x, x + 1)
17 | val g = (x: Int) => List(x, 2 * x)
18 | val pure = (x: Int) => List(x) // same as the list "constructor"
19 |
20 | // prop 1: left identity
21 | val leftIdentity = pure(42).flatMap(f) == f(42) // for every x, for every f
22 |
23 | // prop 2: right identity
24 | val rightIdentity = aList.flatMap(pure) == aList // for every list
25 |
26 | // prop 3: associativity
27 | /*
28 | [1,2,3].flatMap(x => [x, x+1]) = [1,2,2,3,3,4]
29 | [1,2,2,3,3,4].flatMap(x => [x, 2*x]) = [1,2, 2,4, 2,4, 3,6, 3,6, 4,8]
30 | [1,2,3].flatMap(f).flatMap(g) = [1,2, 2,4, 2,4, 3,6, 3,6, 4,8]
31 |
32 | [1,2,2,4] = f(1).flatMap(g)
33 | [2,4,3,6] = f(2).flatMap(g)
34 | [3,6,4,8] = f(3).flatMap(g)
35 | [1,2, 2,4, 2,4, 3,6, 3,6, 4,8] = f(1).flatMap(g) ++ f(2).flatMap(g) ++ f(3).flatMap(g)
36 | [1,2,3].flatMap(x => f(x).flatMap(g))
37 | */
38 | val associativity = aList.flatMap(f).flatMap(g) == aList.flatMap(x => f(x).flatMap(g))
39 | }
40 |
41 | def optionStory(): Unit = {
42 | val anOption = Option(42)
43 | val optionString = for {
44 | lang <- Option("Scala")
45 | ver <- Option(3)
46 | } yield s"$lang-$ver"
47 | // identical
48 | val optionString_v2 = Option("Scala").flatMap(lang => Option(3).map(ver => s"$lang-$ver"))
49 |
50 | val f = (x: Int) => Option(x + 1)
51 | val g = (x: Int) => Option(2 * x)
52 | val pure = (x: Int) => Option(x) // same as Option "constructor"
53 |
54 | // prop 1: left-identity
55 | val leftIdentity = pure(42).flatMap(f) == f(42) // for any x, for any f
56 |
57 | // prop 2: right-identity
58 | val rightIdentity = anOption.flatMap(pure) == anOption // for any Option
59 |
60 | // prop 3: associativity
61 | /*
62 | anOption.flatMap(f).flatMap(g) = Option(42).flatMap(x => Option(x + 1)).flatMap(x => Option(2 * x)
63 | = Option(43).flatMap(x => Option(2 * x)
64 | = Option(86)
65 |
66 | anOption.flatMap(x => f(x).flatMap(g)) = Option(42).flatMap(x => Option(x + 1).flatMap(y => 2 * y)))
67 | = Option(42).flatMap(x => 2 * x + 2)
68 | = Option(86)
69 | */
70 | val associativity = anOption.flatMap(f).flatMap(g) == anOption.flatMap(x => f(x).flatMap(g)) // for any option, f and g
71 | }
72 |
73 | // MONADS = chain dependent computations
74 |
75 | // exercise: IS THIS A MONAD?
76 | // answer: IT IS A MONAD!
77 | // interpretation: ANY computation that might perform side effects
78 | case class IO[A](unsafeRun: () => A) {
79 | def map[B](f: A => B): IO[B] =
80 | IO(() => f(unsafeRun()))
81 |
82 | def flatMap[B](f: A => IO[B]): IO[B] =
83 | IO(() => f(unsafeRun()).unsafeRun())
84 | }
85 |
86 | object IO {
87 | @targetName("pure")
88 | def apply[A](value: => A): IO[A] =
89 | new IO(() => value)
90 | }
91 |
92 | def possiblyMonadStory(): Unit = {
93 | val aPossiblyMonad = IO(42)
94 | val f = (x: Int) => IO(x + 1)
95 | val g = (x: Int) => IO(2 * x)
96 | val pure = (x: Int) => IO(x)
97 |
98 | // prop 1: left-identity
99 | val leftIdentity = pure(42).flatMap(f) == f(42)
100 |
101 | // prop 2: right-identity
102 | val rightIdentity = aPossiblyMonad.flatMap(pure) == aPossiblyMonad
103 |
104 | // prop 3: associativity
105 | val associativity = aPossiblyMonad.flatMap(f).flatMap(g) == aPossiblyMonad.flatMap(x => f(x).flatMap(g))
106 |
107 | println(leftIdentity)
108 | println(rightIdentity)
109 | println(associativity)
110 | println(IO(3) == IO(3))
111 | // ^^ false negative.
112 |
113 | // real tests: values produced + side effect ordering
114 | val leftIdentity_v2 = pure(42).flatMap(f).unsafeRun() == f(42).unsafeRun()
115 | val rightIdentity_v2 = aPossiblyMonad.flatMap(pure).unsafeRun() == aPossiblyMonad.unsafeRun()
116 | val associativity_v2 = aPossiblyMonad.flatMap(f).flatMap(g).unsafeRun() == aPossiblyMonad.flatMap(x => f(x).flatMap(g)).unsafeRun()
117 |
118 | println(leftIdentity_v2)
119 | println(rightIdentity_v2)
120 | println(associativity_v2)
121 |
122 | val fs = (x: Int) => IO {
123 | println("incrementing")
124 | x + 1
125 | }
126 |
127 | val gs = (x: Int) => IO {
128 | println("doubling")
129 | x * 2
130 | }
131 |
132 | val associativity_v3 = aPossiblyMonad.flatMap(fs).flatMap(gs).unsafeRun() == aPossiblyMonad.flatMap(x => fs(x).flatMap(gs)).unsafeRun()
133 | }
134 |
135 | def possiblyMonadExample(): Unit = {
136 | val aPossiblyMonad = IO {
137 | println("printing my first possibly monad")
138 | // do some computations
139 | 42
140 | }
141 |
142 | val anotherPM = IO {
143 | println("my second PM")
144 | "Scala"
145 | }
146 |
147 | val aForComprehension = for { // computations are DESCRIBED, not EXECUTED
148 | num <- aPossiblyMonad
149 | lang <- anotherPM
150 | } yield s"$num-$lang"
151 | }
152 |
153 | def main(args: Array[String]): Unit = {
154 | possiblyMonadStory()
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part2afp/PartialFunctions.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part2afp
2 |
3 | object PartialFunctions {
4 |
5 | val aFunction: Int => Int = x => x + 1
6 |
7 | val aFussyFunction = (x: Int) =>
8 | if (x == 1) 42
9 | else if (x == 2) 56
10 | else if (x == 5) 999
11 | else throw new RuntimeException("no suitable cases possible")
12 |
13 | val aFussyFunction_v2 = (x: Int) => x match {
14 | case 1 => 42
15 | case 2 => 56
16 | case 5 => 999
17 | }
18 |
19 | // partial function
20 | val aPartialFunction: PartialFunction[Int, Int] = { // x => x match { ... }
21 | case 1 => 42
22 | case 2 => 56
23 | case 5 => 999
24 | }
25 |
26 | // utilities on PFs
27 | val canCallOn37 = aPartialFunction.isDefinedAt(37)
28 | val liftedPF = aPartialFunction.lift // Int => Option[Int]
29 |
30 | val anotherPF: PartialFunction[Int, Int] = {
31 | case 45 => 86
32 | }
33 | val pfChain = aPartialFunction.orElse[Int, Int](anotherPF)
34 |
35 | // HOFs accept PFs as arguments
36 | val aList = List(1,2,3,4)
37 | val aChangedList = aList.map(x => x match {
38 | case 1 => 4
39 | case 2 => 3
40 | case 3 => 45
41 | case 4 => 67
42 | case _ => 0
43 | })
44 |
45 | val aChangedList_v2 = aList.map({ // possible because PartialFunction[A, B] extends Function1[A, B]
46 | case 1 => 4
47 | case 2 => 3
48 | case 3 => 45
49 | case 4 => 67
50 | case _ => 0
51 | })
52 |
53 | val aChangedList_v3 = aList.map {
54 | case 1 => 4
55 | case 2 => 3
56 | case 3 => 45
57 | case 4 => 67
58 | case _ => 0
59 | }
60 |
61 | case class Person(name: String, age: Int)
62 | val someKids = List(
63 | Person("Alice", 3),
64 | Person("Bobbie", 5),
65 | Person("Jane", 4)
66 | )
67 |
68 | val kidsGrowingUp = someKids.map {
69 | case Person(name, age) => Person(name, age + 1)
70 | }
71 |
72 |
73 | def main(args: Array[String]): Unit = {
74 | println(aPartialFunction(2))
75 | // println(aPartialFunction(33)) // throws MatchError
76 | println(liftedPF(5)) // Some(999)
77 | println(liftedPF(37)) // None
78 | println(pfChain(45))
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part3async/Futures.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part3async
2 |
3 | import java.util.concurrent.Executors
4 | import scala.concurrent.{Await, ExecutionContext, Future, Promise}
5 | import scala.util.{Failure, Random, Success, Try}
6 | import scala.concurrent.duration.*
7 |
8 | object Futures {
9 |
10 | def calculateMeaningOfLife(): Int = {
11 | // simulate long compute
12 | Thread.sleep(1000)
13 | 42
14 | }
15 |
16 | // thread pool (Java-specific)
17 | val executor = Executors.newFixedThreadPool(4)
18 | // thread pool (Scala-specific)
19 | given executionContext: ExecutionContext = ExecutionContext.fromExecutorService(executor)
20 |
21 | // a future = an async computation that will finish at some point
22 | val aFuture: Future[Int] = Future(calculateMeaningOfLife()) // given executionContext will be passed here
23 |
24 | // Option[Try[Int]], because
25 | // - we don't know if we have a value
26 | // - if we do, that can be a failed computation
27 | val futureInstantResult: Option[Try[Int]] = aFuture.value // inspect the value of the future RIGHT NOW
28 |
29 | // callbacks
30 | aFuture.onComplete {
31 | case Success(value) => println(s"I've completed with the meaning of life: $value")
32 | case Failure(ex) => println(s"My async computation failed: $ex")
33 | } // on SOME other thread
34 |
35 | /*
36 | Functional Programming on Futures
37 | Motivation: onComplete is a hassle
38 | */
39 | case class Profile(id: String, name: String) {
40 | def sendMessage(anotherProfile: Profile, message: String) =
41 | println(s"${this.name} sending message to ${anotherProfile.name}: $message")
42 | }
43 |
44 | object SocialNetwork {
45 | // "database"
46 | val names = Map(
47 | "rtjvm.id.1-daniel" -> "Daniel",
48 | "rtjvm.id.2-jane" -> "Jane",
49 | "rtjvm.id.3-mark" -> "Mark",
50 | )
51 |
52 | // friends "database"
53 | val friends = Map(
54 | "rtjvm.id.2-jane" -> "rtjvm.id.3-mark"
55 | )
56 |
57 | val random = new Random()
58 |
59 | // "API"
60 | def fetchProfile(id: String): Future[Profile] = Future {
61 | // fetch something from the database
62 | Thread.sleep(random.nextInt(300)) // simulate the time delay
63 | Profile(id, names(id))
64 | }
65 |
66 | def fetchBestFriend(profile: Profile): Future[Profile] = Future {
67 | Thread.sleep(random.nextInt(400))
68 | val bestFriendId = friends(profile.id)
69 | Profile(bestFriendId, names(bestFriendId))
70 | }
71 | }
72 |
73 | // problem: sending a message to my best friend
74 | def sendMessageToBestFriend(accountId: String, message: String): Unit = {
75 | // 1 - call fetchProfile
76 | // 2 - call fetchBestFriend
77 | // 3 - call profile.sendMessage(bestFriend)
78 | val profileFuture = SocialNetwork.fetchProfile(accountId)
79 | profileFuture.onComplete {
80 | case Success(profile) => // "code block"
81 | val friendProfileFuture = SocialNetwork.fetchBestFriend(profile)
82 | friendProfileFuture.onComplete {
83 | case Success(friendProfile) => profile.sendMessage(friendProfile, message)
84 | case Failure(e) => e.printStackTrace()
85 | }
86 | case Failure(ex) => ex.printStackTrace()
87 | }
88 | } // onComplete is such a pain - callback hell!
89 |
90 | def sendMessageToBestFriend_v2(accountId: String, message: String): Unit = {
91 | val profileFuture = SocialNetwork.fetchProfile(accountId)
92 | val action = profileFuture.flatMap { profile => // Future[Unit]
93 | SocialNetwork.fetchBestFriend(profile).map { bestFriend => // Future[Unit]
94 | profile.sendMessage(bestFriend, message) // unit
95 | }
96 | }
97 | }
98 |
99 | def sendMessageToBestFriend_v3(accountId: String, message: String): Unit =
100 | for {
101 | profile <- SocialNetwork.fetchProfile(accountId)
102 | bestFriend <- SocialNetwork.fetchBestFriend(profile)
103 | } yield profile.sendMessage(bestFriend, message) // identical to v2
104 |
105 | val janeProfileFuture = SocialNetwork.fetchProfile("rtjvm.id.2-jane")
106 | val janeFuture: Future[String] = janeProfileFuture.map(profile => profile.name) // map transforms value contained inside, ASYNCHRONOUSLY
107 | val janesBestFriend: Future[Profile] = janeProfileFuture.flatMap(profile => SocialNetwork.fetchBestFriend(profile))
108 | val janesBestFriendFilter: Future[Profile] = janesBestFriend.filter(profile => profile.name.startsWith("Z"))
109 |
110 | // fallbacks
111 | val profileNoMatterWhat: Future[Profile] = SocialNetwork.fetchProfile("unknown id").recover {
112 | case e: Throwable => Profile("rtjvm.id.0-dummy", "Forever alone")
113 | }
114 |
115 | val aFetchedProfileNoMatterWhat: Future[Profile] = SocialNetwork.fetchProfile("unknown id").recoverWith {
116 | case e: Throwable => SocialNetwork.fetchProfile("rtjvm.id.0-dummy")
117 | }
118 |
119 | val fallBackProfile: Future[Profile] = SocialNetwork.fetchProfile("unknown id").fallbackTo(SocialNetwork.fetchProfile("rtjvm.id.0-dummy"))
120 |
121 | /*
122 | Blocking the calling thread for future completion
123 | Example: a transaction that must go through.
124 | */
125 | case class User(name: String)
126 | case class Transaction(sender: String, receiver: String, amount: Double, status: String)
127 |
128 | object BankingApp {
129 | // "APIs"
130 | def fetchUser(name: String): Future[User] = Future {
131 | // simulate some DB fetching
132 | Thread.sleep(500)
133 | User(name)
134 | }
135 |
136 | def createTransaction(user: User, merchantName: String, amount: Double): Future[Transaction] = Future {
137 | // simulate payment
138 | Thread.sleep(1000)
139 | Transaction(user.name, merchantName, amount, "SUCCESS")
140 | }
141 |
142 | // "external API"
143 | def purchase(username: String, item: String, merchantName: String, price: Double): String = {
144 | /*
145 | 1. fetch user
146 | 2. create transaction
147 | 3. WAIT for the txn to finish
148 | */
149 | val transactionStatusFuture: Future[String] = for {
150 | user <- fetchUser(username)
151 | transaction <- createTransaction(user, merchantName, price)
152 | } yield transaction.status
153 |
154 | // blocking call - not recommended unless in VERY STRICT circumstances
155 | Await.result(transactionStatusFuture, 2.seconds) // throws TimeoutException if the future doesn't finish within 2s
156 | }
157 | }
158 |
159 | /*
160 | Promises: a technique for controlling the completion of Futures
161 | */
162 | def demoPromises(): Unit = {
163 | val promise = Promise[Int]()
164 | val futureInside: Future[Int] = promise.future
165 |
166 | // thread 1 - "consumer": monitor the future for completion
167 | futureInside.onComplete {
168 | case Success(value) => println(s"[consumer] I've just been completed with $value")
169 | case Failure(ex) => ex.printStackTrace()
170 | }
171 |
172 | // thread 2 - "producer"
173 | val producerThread = new Thread(() => {
174 | println("[producer] Crunching numbers...")
175 | Thread.sleep(1000)
176 | // "fulfil" the promise
177 | promise.success(42)
178 | println("[producer] I'm done.")
179 | })
180 |
181 | producerThread.start()
182 | }
183 |
184 | /**
185 | Exercises
186 | 1) fulfil a future IMMEDIATELY with a value
187 | 2) in sequence: make sure the first Future has been completed before returning the second
188 | 3) first(fa, fb) => new Future with the value of the first Future to complete
189 | 4) last(fa, fb) => new Future with the value of the LAST Future to complete
190 | 5) retry an action returning a Future until a predicate holds true
191 | */
192 |
193 | // 1
194 | def completeImmediately[A](value: A): Future[A] = Future(value) // async completion as soon as possible
195 | def completeImmediately_v2[A](value: A): Future[A] = Future.successful(value) // synchronous completion
196 |
197 | // 2
198 | def inSequence[A, B](first: Future[A], second: Future[B]): Future[B] =
199 | first.flatMap(_ => second)
200 |
201 | // 3
202 | def first[A](f1: Future[A], f2: Future[A]): Future[A] = {
203 | val promise = Promise[A]()
204 | f1.onComplete(result1 => promise.tryComplete(result1))
205 | f2.onComplete(result2 => promise.tryComplete(result2))
206 |
207 | promise.future
208 | }
209 |
210 | // 4
211 | def last[A](f1: Future[A], f2: Future[A]): Future[A] = {
212 | val bothPromise = Promise[A]()
213 | val lastPromise = Promise[A]()
214 |
215 | def checkAndComplete(result: Try[A]): Unit =
216 | if (!bothPromise.tryComplete(result))
217 | lastPromise.complete(result)
218 |
219 | f1.onComplete(checkAndComplete)
220 | f2.onComplete(checkAndComplete)
221 |
222 | lastPromise.future
223 | }
224 |
225 | def testFirstLast(): Unit = {
226 | lazy val fast = Future {
227 | Thread.sleep(100)
228 | 1
229 | }
230 | lazy val slow = Future {
231 | Thread.sleep(200)
232 | 2
233 | }
234 | first(fast, slow).foreach(result => println(s"FIRST: $result"))
235 | last(fast, slow).foreach(result => println(s"LAST: $result"))
236 | }
237 |
238 | // 5
239 | def retryUntil[A](action: () => Future[A], predicate: A => Boolean): Future[A] =
240 | action()
241 | .filter(predicate)
242 | .recoverWith {
243 | case _ => retryUntil(action, predicate)
244 | }
245 |
246 | def testRetries(): Unit = {
247 | val random = new Random()
248 | val action = () => Future {
249 | Thread.sleep(100)
250 | val nextValue = random.nextInt(100)
251 | println(s"Generated $nextValue")
252 | nextValue
253 | }
254 |
255 | val predicate = (x: Int) => x < 10
256 |
257 | retryUntil(action, predicate).foreach(finalResult => println(s"Settled at $finalResult"))
258 | }
259 |
260 | def main(args: Array[String]): Unit = {
261 | testRetries()
262 | Thread.sleep(2000)
263 | executor.shutdown()
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part3async/JVMConcurrencyIntro.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part3async
2 |
3 | import java.util.concurrent.Executors
4 |
5 | object JVMConcurrencyIntro {
6 |
7 | def basicThreads(): Unit = {
8 | val runnable = new Runnable {
9 | override def run(): Unit = {
10 | println("waiting...")
11 | Thread.sleep(2000)
12 | println("running on some thread")
13 | }
14 | }
15 |
16 | // threads on the JVM
17 | val aThread = new Thread(runnable)
18 | aThread.start() // will run the runnable on some JVM thread
19 | // JVM thread == OS thread (soon to change via Project Loom)
20 | aThread.join() // block until thread finishes
21 | }
22 |
23 | // order of operations is NOT guaranteed
24 | // different runs = different results!
25 | def orderOfExecution(): Unit = {
26 | val threadHello = new Thread(() => (1 to 100).foreach(_ => println("hello")))
27 | val threadGoodbye = new Thread(() => (1 to 100).foreach(_ => println("goodbye")))
28 | threadHello.start()
29 | threadGoodbye.start()
30 | }
31 |
32 | // executors
33 | def demoExecutors(): Unit = {
34 | val threadPool = Executors.newFixedThreadPool(4)
35 | // submit a computation
36 | threadPool.execute(() => println("something in the thread pool"))
37 |
38 | threadPool.execute { () =>
39 | Thread.sleep(1000)
40 | println("done after one second")
41 | }
42 |
43 | threadPool.execute { () =>
44 | Thread.sleep(1000)
45 | println("almost done")
46 | Thread.sleep(1000)
47 | println("done after 2 seconds")
48 | }
49 |
50 | threadPool.shutdown()
51 | // threadPool.execute(() => println("this should NOT apeear")) // should throw an exception in the calling thread
52 | }
53 |
54 | def main(args: Array[String]): Unit = {
55 | demoExecutors()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part3async/JVMConcurrencyProblems.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part3async
2 |
3 | object JVMConcurrencyProblems {
4 |
5 | def runInParallel(): Unit = {
6 | var x = 0
7 |
8 | val thread1 = new Thread(() => {
9 | x = 1
10 | })
11 |
12 | val thread2 = new Thread(() => {
13 | x = 2
14 | })
15 |
16 | thread1.start()
17 | thread2.start()
18 | println(x) // race condition
19 | }
20 |
21 | case class BankAccount(var amount: Int)
22 |
23 | def buy(bankAccount: BankAccount, thing: String, price: Int): Unit = {
24 | /*
25 | involves 3 steps:
26 | - read old value
27 | - compute result
28 | - write new value
29 | */
30 | bankAccount.amount -= price
31 | }
32 |
33 | def buySafe(bankAccount: BankAccount, thing: String, price: Int): Unit = {
34 | bankAccount.synchronized { // does not allow multiple threads to run the critical section AT THE SAME TIME
35 | bankAccount.amount -= price // critical section
36 | }
37 | }
38 |
39 | /*
40 | Example race condition:
41 | thread1 (shoes)
42 | - reads amount 50000
43 | - compute result 50000 - 3000 = 47000
44 | thread2 (iPhone)
45 | - reads amount 50000
46 | - compute result 50000 - 4000 = 46000
47 | thread1 (shoes)
48 | - write amount 47000
49 | thread2 (iPhone)
50 | - write amount 46000
51 | */
52 | def demoBankingProblem(): Unit = {
53 | (1 to 10000).foreach { _ =>
54 | val account = new BankAccount(50000)
55 | val thread1 = new Thread(() => buy(account, "shoes", 3000))
56 | val thread2 = new Thread(() => buy(account, "iPhone", 4000))
57 | thread1.start()
58 | thread2.start()
59 | thread1.join()
60 | thread2.join()
61 | if (account.amount != 43000) println(s"AHA! I've just broken the bank: ${account.amount}")
62 | }
63 | }
64 |
65 | /**
66 | Exercises
67 | 1 - create "inception threads"
68 | thread 1
69 | -> thread 2
70 | -> thread 3
71 | ....
72 | each thread prints "hello from thread $i"
73 | Print all messages IN REVERSE ORDER
74 |
75 | 2 - What's the max/min value of x?
76 | 3 - "sleep fallacy": what's the value of message?
77 | */
78 | // 1 - inception threads
79 | def inceptionThreads(maxThreads: Int, i: Int = 1): Thread =
80 | new Thread(() => {
81 | if (i < maxThreads) {
82 | val newThread = inceptionThreads(maxThreads, i + 1)
83 | newThread.start()
84 | newThread.join()
85 | }
86 | println(s"Hello from thread $i")
87 | })
88 |
89 | // 2
90 | /*
91 | max value = 100 - each thread increases x by 1
92 | min value = 1
93 | all threads read x = 0 at the same time
94 | all threads (in parallel) compute 0 + 1 = 1
95 | all threads try to write x = 1
96 | */
97 | def minMaxX(): Unit = {
98 | var x = 0
99 | val threads = (1 to 100).map(_ => new Thread(() => x += 1))
100 | threads.foreach(_.start())
101 | }
102 |
103 | // 3
104 | /*
105 | almost always, message = "Scala is awesome"
106 | is it guaranteed? NO
107 | Obnoxious situation (possible):
108 |
109 | main thread:
110 | message = "Scala sucks"
111 | awesomeThread.start()
112 | sleep(1001) - yields execution
113 | awesome thread:
114 | sleep(1000) - yields execution
115 | OS gives the CPU to some important thread, takes > 2s
116 | OS gives the CPU back to the main thread
117 | main thread:
118 | println(message) // "Scala sucks"
119 | awesome thread:
120 | message = "Scala is awesome"
121 | */
122 | def demoSleepFallacy(): Unit = {
123 | var message = ""
124 | val awesomeThread = new Thread(() => {
125 | Thread.sleep(1000)
126 | message = "Scala is awesome"
127 | })
128 |
129 | message = "Scala sucks"
130 | awesomeThread.start()
131 | Thread.sleep(1001)
132 | // solution: join the worker thread
133 | awesomeThread.join()
134 | println(message)
135 | }
136 |
137 | def main(args: Array[String]): Unit = {
138 | demoSleepFallacy()
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part3async/JVMThreadCommunication.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part3async
2 |
3 | import scala.collection.mutable
4 | import scala.util.Random
5 |
6 | object JVMThreadCommunication {
7 | def main(args: Array[String]): Unit = {
8 | ProdConsV4.start(4, 2, 5)
9 | }
10 | }
11 |
12 | // example: the producer-consumer problem
13 | class SimpleContainer {
14 | private var value: Int = 0
15 |
16 | def isEmpty: Boolean =
17 | value == 0
18 |
19 | def set(newValue: Int): Unit =
20 | value = newValue
21 |
22 | def get: Int = {
23 | val result = value
24 | value = 0
25 | result
26 | }
27 | }
28 |
29 | // one producer, one consumer, busy waiting
30 | object ProdConsV1 {
31 | def start(): Unit = {
32 | val container = new SimpleContainer
33 |
34 | val consumer = new Thread(() => {
35 | println("[consumer] waiting...")
36 | // busy waiting
37 | while (container.isEmpty) {
38 | println("[consumer] waiting for a value..")
39 | }
40 |
41 | println(s"[consumer] I have consumed a value: ${container.get}")
42 | })
43 |
44 | val producer = new Thread(() => {
45 | println("[producer] computing...")
46 | Thread.sleep(500)
47 | val value = 42
48 | println(s"[producer] I am producing, after LONG work, the value $value")
49 | container.set(value)
50 | })
51 |
52 | consumer.start()
53 | producer.start()
54 | }
55 | }
56 |
57 | // wait + notify
58 | object ProdConsV2 {
59 | def start(): Unit = {
60 | val container = new SimpleContainer
61 |
62 | val consumer = new Thread(() => {
63 | println("[consumer] waiting...")
64 |
65 | container.synchronized { // block all other threads trying to "lock" this object
66 | // thread-safe code
67 | if (container.isEmpty)
68 | container.wait() // release the lock + suspend the thread
69 | // reacquire the lock here
70 | // continue execution
71 | println(s"[consumer] I have consumed a value: ${container.get}")
72 | }
73 | })
74 |
75 | val producer = new Thread(() => {
76 | println("[producer] computing...")
77 | Thread.sleep(500)
78 | val value = 42
79 |
80 | container.synchronized {
81 | println(s"[producer] I am producing, after LONG work, the value $value")
82 | container.set(value)
83 | container.notify() // awaken ONE suspended thread on this object
84 | } // release the lock
85 | })
86 |
87 | consumer.start()
88 | producer.start()
89 | }
90 | }
91 |
92 | // insert a larger container
93 | // producer -> [ _ _ _ ] -> consumer
94 | object ProdConsV3 {
95 | def start(containerCapacity: Int): Unit = {
96 | val buffer: mutable.Queue[Int] = new mutable.Queue[Int]
97 |
98 | val consumer = new Thread(() => {
99 | val random = new Random(System.nanoTime())
100 |
101 | while (true) {
102 | buffer.synchronized {
103 | if (buffer.isEmpty) {
104 | println("[consumer] buffer empty, waiting...")
105 | buffer.wait()
106 | }
107 |
108 | // buffer must not be empty
109 | val x = buffer.dequeue()
110 | println(s"[consumer] I've just consumed $x")
111 |
112 | // producer, gimmme more elements!
113 | buffer.notify() // wake up the producer if it's asleep
114 | }
115 |
116 | Thread.sleep(random.nextInt(500))
117 | }
118 | })
119 |
120 | val producer = new Thread(() => {
121 | val random = new Random(System.nanoTime())
122 | var counter = 0
123 |
124 | while (true) {
125 | buffer.synchronized {
126 | if (buffer.size == containerCapacity) {
127 | println("[producer] buffer full, waiting...")
128 | buffer.wait()
129 | }
130 |
131 | // buffer is not empty
132 | val newElement = counter
133 | counter += 1
134 | println(s"[producer] I'm producing $newElement")
135 | buffer.enqueue(newElement)
136 |
137 | // consumer, don't be lazy!
138 | buffer.notify() // wakes up the consumer (if it's asleep)
139 | }
140 |
141 | Thread.sleep(random.nextInt(500))
142 | }
143 | })
144 |
145 | consumer.start()
146 | producer.start()
147 | }
148 | }
149 |
150 | /*
151 | large container, multiple producers/consumers
152 | producer1 -> [ _ _ _ ] -> consumer1
153 | producer2 ---^ +---> consumer2
154 | */
155 | object ProdConsV4 {
156 | class Consumer(id: Int, buffer: mutable.Queue[Int]) extends Thread {
157 | override def run(): Unit = {
158 | val random = new Random(System.nanoTime())
159 |
160 | while (true) {
161 | buffer.synchronized {
162 | /*
163 | we need to constantly check if the buffer is empty - otherwise, crashing scenario:
164 | one producer, two consumers
165 | producer produces 1 value in the buffer
166 | both consumers are waiting
167 | producer calls notify, awakens one consumer
168 | consumer dequeues, calls notify, awakens the other consumer
169 | the other consumer awakens, tries dequeuing, CRASH
170 | */
171 | while (buffer.isEmpty) {
172 | println(s"[consumer $id] buffer empty, waiting...")
173 | buffer.wait()
174 | }
175 |
176 | // buffer is non-empty
177 | val newValue = buffer.dequeue()
178 | println(s"[consumer $id] consumed $newValue")
179 |
180 | /*
181 | We need to use notifyAll. Otherwise, deadlock scenario:
182 | Scenario: 2 producers, one consumer, capacity = 1
183 | producer1 produces a value, then waits
184 | producer2 sees buffer full, waits
185 | consumer consumes value, notifies one producer (producer1)
186 | consumer sees buffer empty, wait
187 | producer1 produces a value, calls notify - signal goes to producer2
188 | producer1 sees buffer full, waits
189 | producer2 sees buffer full, waits
190 | DEADLOCK
191 | */
192 | buffer.notifyAll() // signal all the waiting threads on the buffer
193 | }
194 |
195 | Thread.sleep(random.nextInt(500))
196 | }
197 | }
198 | }
199 |
200 | class Producer(id: Int, buffer: mutable.Queue[Int], capacity: Int) extends Thread {
201 | override def run(): Unit = {
202 | val random = new Random(System.nanoTime())
203 | var currentCount = 0
204 |
205 | while (true) {
206 | buffer.synchronized {
207 | while (buffer.size == capacity) { // buffer full
208 | println(s"[producer $id] buffer is full, waiting...")
209 | buffer.wait()
210 | }
211 |
212 | // there is space in the buffer
213 | println(s"[producer $id] producing $currentCount")
214 | buffer.enqueue(currentCount)
215 |
216 | // wake up everyone (similar/symmetrical deadlocking scenario: see producer code)
217 | buffer.notifyAll()
218 |
219 | // upcoming produced value
220 | currentCount += 1
221 | }
222 |
223 | Thread.sleep(random.nextInt(500))
224 | }
225 | }
226 | }
227 |
228 | def start(nProducers: Int, nConsumers: Int, containerCapacity: Int): Unit = {
229 | val buffer: mutable.Queue[Int] = new mutable.Queue[Int]
230 | val producers = (1 to nProducers).map(id => new Producer(id, buffer, containerCapacity))
231 | val consumers = (1 to nConsumers).map(id => new Consumer(id, buffer))
232 |
233 | producers.foreach(_.start())
234 | consumers.foreach(_.start())
235 | }
236 | }
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part3async/ParallelCollections.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part3async
2 |
3 | import scala.collection.parallel.*
4 | import scala.collection.parallel.CollectionConverters.*
5 | import scala.collection.parallel.immutable.ParVector
6 |
7 | object ParallelCollections {
8 |
9 | val aList = (1 to 1000000).toList
10 | val anIncrementedList = aList.map(_ + 1)
11 | val parList: ParSeq[Int] = aList.par
12 | val aParallelizedIncrementedList = parList.map(_ + 1) // map, flatMap, filter, foreach, reduce, fold
13 | /*
14 | Applicable for
15 | - Seq
16 | - Vector
17 | - Arrays
18 | - Maps
19 | - Sets
20 |
21 | Use-case: faster processing
22 | */
23 |
24 | // parallel collection build explicitly
25 | val aParVecror = ParVector[Int](1,2,3,4,5,6)
26 |
27 | def measure[A](expression: => A): Long = {
28 | val time = System.currentTimeMillis()
29 | expression // forcing evaluation
30 | System.currentTimeMillis() - time
31 | }
32 |
33 | def compareListTransformation(): Unit = {
34 | val list = (1 to 30000000).toList
35 | println("list creation done")
36 |
37 | val serialTime = measure(list.map(_ + 1))
38 | println(s"serial time: $serialTime")
39 |
40 | val parallelTime = measure(list.par.map(_ + 1))
41 | println(s"parallel time: $parallelTime")
42 | }
43 |
44 | def demoUndefinedOrder(): Unit = {
45 | val list = (1 to 1000).toList
46 | val reduction = list.reduce(_ - _) // usually bad idea to use non-associative operators
47 | // [1,2,3].reduce(_ - _) = 1 - 2 - 3 = -4
48 | // [1,2,3].reduce(_ - _) = 1 - (2 - 3) = 2
49 |
50 | val parallelReduction = list.par.reduce(_ - _) // order of operation is undefined, returns different results
51 |
52 | println(s"Sequential reduction: $reduction")
53 | println(s"Parallel reduction: $parallelReduction")
54 | }
55 |
56 | // for associative ops, result is deterministic
57 | def demoDefinedOrder(): Unit = {
58 | val strings = "I love parallel collections but I must be careful".split(" ").toList
59 | val concatenation = strings.reduce(_ + " " + _)
60 | val parallelConcatenation = strings.par.reduce(_ + " " + _)
61 |
62 | println(s"Sequential concatenation: $concatenation")
63 | println(s"Parallel concatenation: $parallelConcatenation")
64 | }
65 |
66 | // be careful with imperative programming on parallel collections
67 | def demoRaceConditions(): Unit = {
68 | var sum = 0
69 | (1 to 1000).toList.par.foreach(elem => sum += elem)
70 | println(sum)
71 | }
72 |
73 | def main(args: Array[String]): Unit = {
74 | demoRaceConditions()
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part4context/ContextFunctions.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part4context
2 |
3 | import scala.concurrent.{ExecutionContext, Future}
4 |
5 | object ContextFunctions {
6 |
7 | val aList = List(2,1,3,4)
8 | val sortedList = aList.sorted
9 |
10 | // defs can take using clauses
11 | def methodWithoutContextArguments(nonContextArg: Int)(nonContextArg2: String): String = ???
12 | def methodWithContextArguments(nonContextArg: Int)(using nonContextArg2: String): String = ???
13 |
14 | // eta-expansion
15 | val functionWithoutContextArguments = methodWithoutContextArguments
16 | // val func2 = methodWithContextArguments // doesn't work
17 |
18 | // context function
19 | val functionWithContextArguments: Int => String ?=> String = methodWithContextArguments
20 |
21 | /*
22 | Use cases:
23 | - convert methods with using clauses to function values
24 | - HOF with function values taking given instances as arguments
25 | - requiring given instances at CALL SITE, not at DEFINITION
26 | */
27 | // execution context here
28 | // val incrementAsync: Int => Future[Int] = x => Future(x + 1) // doesn't work without a given EC in scope
29 |
30 | val incrementAsync: ExecutionContext ?=> Int => Future[Int] = x => Future(x + 1)
31 |
32 | def main(args: Array[String]): Unit = {
33 |
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part4context/ExtensionMethods.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part4context
2 |
3 | object ExtensionMethods {
4 |
5 | case class Person(name: String) {
6 | def greet: String = s"Hi, my name is $name, nice to meet you!"
7 | }
8 |
9 | extension (string: String)
10 | def greetAsPerson: String = Person(string).greet
11 |
12 | val danielGreeting = "Daniel".greetAsPerson
13 |
14 | // generic extension methods
15 | extension [A](list: List[A])
16 | def ends: (A, A) = (list.head, list.last)
17 |
18 | val aList = List(1,2,3,4)
19 | val firstLast = aList.ends
20 |
21 | // reason: make APIs very expressive
22 | // reason 2: enhance CERTAIN types with new capabilities
23 | // => super-powerful code
24 | trait Semigroup[A] {
25 | def combine(x: A, y: A): A
26 | }
27 |
28 | extension [A](list: List[A])
29 | def combineAll(using combinator: Semigroup[A]): A =
30 | list.reduce(combinator.combine)
31 |
32 | given intCombinator: Semigroup[Int] with
33 | override def combine(x: Int, y: Int) = x + y
34 |
35 | val firstSum = aList.combineAll // works, sum is 10
36 | val someStrings = List("I", "love", "Scala")
37 | // val stringsSum = someStrings.combineAll // does not compile - no given Combinator[String] in scope
38 |
39 | // grouping extensions
40 | object GroupedExtensions {
41 | extension [A](list: List[A]) {
42 | def ends: (A, A) = (list.head, list.last)
43 | def combineAll(using combinator: Semigroup[A]): A =
44 | list.reduce(combinator.combine)
45 | }
46 | }
47 |
48 | // call extension methods directly
49 | val firstLast_v2 = ends(aList) // same as aList.ends
50 |
51 | /**
52 | * Exercises
53 | * 1. Add an isPrime method to the Int type.
54 | * You should be able to write 7.isPrime
55 | * 2. Add extensions to Tree:
56 | * - map(f: A => B): Tree[B]
57 | * - forall(predicate: A => Boolean): Boolean
58 | * - sum => sum of all elements of the tree
59 | */
60 |
61 | // 1
62 | extension (number: Int)
63 | def isPrime: Boolean = {
64 | def isPrimeAux(potentialDivisor: Int): Boolean =
65 | if (potentialDivisor > number / 2) true
66 | else if (number % potentialDivisor == 0) false
67 | else isPrimeAux(potentialDivisor + 1)
68 |
69 | assert(number >= 0)
70 | if (number == 0 || number == 1) false
71 | else isPrimeAux(2)
72 | }
73 |
74 | // 2
75 | // "library code" = cannot change
76 | sealed abstract class Tree[A]
77 | case class Leaf[A](value: A) extends Tree[A]
78 | case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
79 |
80 | extension [A](tree: Tree[A]) {
81 | def map[B](f: A => B): Tree[B] = tree match {
82 | case Leaf(value) => Leaf(f(value))
83 | case Branch(left, right) => Branch(left.map(f), right.map(f))
84 | }
85 |
86 | def forall(predicate: A => Boolean): Boolean = tree match {
87 | case Leaf(value) => predicate(value)
88 | case Branch(left, right) => left.forall(predicate) && right.forall(predicate)
89 | }
90 |
91 | def combineAll(using combinator: Semigroup[A]): A = tree match {
92 | case Leaf(value) => value
93 | case Branch(left, right) => combinator.combine(left.combineAll, right.combineAll)
94 | }
95 | }
96 |
97 | extension (tree: Tree[Int]) {
98 | def sum: Int = tree match {
99 | case Leaf(value) => value
100 | case Branch(left, right) => left.sum + right.sum
101 | }
102 | }
103 |
104 | def main(args: Array[String]): Unit = {
105 | val aTree: Tree[Int] = Branch(Branch(Leaf(3), Leaf(1)), Leaf(10))
106 | println(aTree.map(_ + 1))
107 | println(aTree.forall(_ % 2 == 0)) // false
108 | println(aTree.sum) // 14
109 | println(aTree.combineAll) // 14
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part4context/Givens.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part4context
2 |
3 | object Givens {
4 |
5 | // list sorting
6 | val aList = List(4,2,3,1)
7 | val anOrderedList = aList.sorted//(descendingOrdering)
8 |
9 | given descendingOrdering: Ordering[Int] = Ordering.fromLessThan(_ > _)
10 | val anInverseOrderedList = aList.sorted(descendingOrdering)
11 |
12 | // custom sorting
13 | case class Person(name: String, age: Int)
14 | val people = List(Person("Alice", 29), Person("Sarah", 34), Person("Jim", 23))
15 |
16 | given personOrdering: Ordering[Person] = new Ordering[Person] {
17 | override def compare(x: Person, y: Person): Int =
18 | x.name.compareTo(y.name)
19 | }
20 |
21 | val sortedPeople = people.sorted//(personOrdering) <-- automatically passed by the compiler
22 |
23 | object PersonAltSyntax {
24 | given personOrdering: Ordering[Person] with {
25 | override def compare(x: Person, y: Person): Int =
26 | x.name.compareTo(y.name)
27 | }
28 | }
29 |
30 | // using clauses
31 | trait Combinator[A] {
32 | def combine(x: A, y: A): A
33 | }
34 |
35 | def combineAll[A](list: List[A])(using combinator: Combinator[A]): A =
36 | list.reduce(combinator.combine)
37 |
38 | /*
39 | Desire
40 | combineAll(List(1,2,3,4))
41 | combineAll(people)
42 | */
43 |
44 | given intCombinator: Combinator[Int] with {
45 | override def combine(x: Int, y: Int) = x + y
46 | }
47 |
48 | val firstSum = combineAll(List(1,2,3,4))//(intCombinator) <-- passed automatically
49 | // val combineAllPeople = combineAll(people) // does not compile - no Combinator[Person] in scope
50 |
51 | // context bound
52 | def combineInGroupsOf3[A](list: List[A])(using Combinator[A]): List[A] =
53 | list.grouped(3).map(group => combineAll(group)/* given Combinator[A] passed by the compiler */).toList
54 |
55 | def combineInGroupsOf3_v2[A : Combinator](list: List[A]): List[A] = // A : Combinator => there is a given Combinator[A] in scope
56 | list.grouped(3).map(group => combineAll(group)/* given Combinator[A] passed by the compiler */).toList
57 |
58 | // synthesize new given instance based on existing ones
59 | given listOrdering(using intOrdering: Ordering[Int]): Ordering[List[Int]] with {
60 | override def compare(x: List[Int], y: List[Int]) =
61 | x.sum - y.sum
62 | }
63 |
64 | val listOfLists = List(List(1,2), List(1,1), List(3,4,5))
65 | val nestedListsOrdered = listOfLists.sorted
66 |
67 | // ... with generics
68 | given listOrderingBasedOnCombinator[A](using ord: Ordering[A])(using combinator: Combinator[A]): Ordering[List[A]] with {
69 | override def compare(x: List[A], y: List[A]) =
70 | ord.compare(combineAll(x), combineAll(y))
71 | }
72 |
73 | // pass a regular value instead of a given
74 | val myCombinator = new Combinator[Int] {
75 | override def combine(x: Int, y: Int) = x * y
76 | }
77 | val listProduct = combineAll(List(1,2,3,4))(using myCombinator)
78 |
79 | /**
80 | * Exercises:
81 | * 1 - create a given for ordering Option[A] if you can order A
82 | * 2 - create a summoning method that fetches the given value of your particular
83 | */
84 | given optionOrdering[A : Ordering]: Ordering[Option[A]] with {
85 | override def compare(x: Option[A], y: Option[A]) = (x, y) match {
86 | case (None, None) => 0
87 | case (None, _) => -1
88 | case (_, None) => 1
89 | case (Some(a), Some(b)) => fetchGivenValue[Ordering[A]].compare(a, b) // or use summon
90 | }
91 | }
92 |
93 | def fetchGivenValue[A](using theValue: A): A = theValue
94 |
95 | def main(args: Array[String]): Unit = {
96 | println(anOrderedList) // [1,2,3,4]
97 | println(anInverseOrderedList) // [4,3,2,1]
98 | println(List(Option(1), Option.empty[Int], Option(3), Option(-1000)).sorted)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part4context/ImplicitConversions.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part4context
2 |
3 | // special import
4 | import scala.language.implicitConversions
5 |
6 | object ImplicitConversions {
7 |
8 | case class Person(name: String) {
9 | def greet(): String = s"Hi, I'm $name, how are you?"
10 | }
11 |
12 | val daniel = Person("Daniel")
13 | val danielSaysHi = daniel.greet()
14 |
15 | // special conversion instance
16 | given string2Person: Conversion[String, Person] with
17 | override def apply(x: String) = Person(x)
18 |
19 | val danielSaysHi_v2 = "Daniel".greet() // Person("Daniel").greet(), automatically by the compiler
20 |
21 | def processPerson(person: Person): String =
22 | if (person.name.startsWith("J")) "OK"
23 | else "NOT OK"
24 |
25 | val isJaneOk = processPerson("Jane") // ok - compiler rewrites to processPerson(Person("Jane"))
26 |
27 | /*
28 | Use-cases for implicit conversions
29 | - auto-box types
30 | - use multiple (often unrelated) types with the same meaning in your code, interchangeably
31 | */
32 |
33 | def main(args: Array[String]): Unit = {
34 |
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part4context/Implicits.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part4context
2 |
3 | object Implicits {
4 |
5 | // the ability to pass arguments automatically (implicitly) by the compiler
6 | trait Semigroup[A] {
7 | def combine(x: A, y: A): A
8 | }
9 |
10 | def combineAll[A](list: List[A])(implicit semigroup: Semigroup[A]): A =
11 | list.reduce(semigroup.combine)
12 |
13 | implicit val intSemigroup: Semigroup[Int] = new Semigroup[Int] {
14 | override def combine(x: Int, y: Int) = x + y
15 | }
16 |
17 | val sumOf10 = combineAll((1 to 10).toList)
18 |
19 | // implicit arg -> using clause
20 | // implicit val -> given declaration
21 |
22 | // implicit class -> extension methods
23 | implicit class MyRichInteger(number: Int) {
24 | // extension methods here
25 | def isEven = number % 2 == 0
26 | }
27 |
28 | val questionOfMyLife = 23.isEven // new MyRichInteger(23).isEven
29 |
30 | // implicit conversion
31 | case class Person(name: String) {
32 | def greet(): String = s"Hi, my name is $name."
33 | }
34 |
35 | // implicit conversion - SUPER DANGEROUS
36 | implicit def string2Person(x: String): Person = Person(x)
37 | val danielSaysHi = "Daniel".greet() // string2Person("Daniel").greet()
38 |
39 | // implicit def => synthesize NEW implicit values
40 | implicit def semigroupOfOption[A](implicit semigroup: Semigroup[A]): Semigroup[Option[A]] = new Semigroup[Option[A]] {
41 | override def combine(x: Option[A], y: Option[A]) = for {
42 | valueX <- x
43 | valueY <- y
44 | } yield semigroup.combine(valueX, valueY)
45 | }
46 |
47 | /*
48 | Equivalent:
49 | given semigroupOfOption[A](using semigroup: Semigroup[A]): Semigroup[Option[A]] with ...
50 | */
51 |
52 | // organizing implicits == organizing contextual abstractions (same principles)
53 | // import yourPackage.* // also imports implicits
54 |
55 | /*
56 | Why implicits will be phased out:
57 | - the implicit keyword has many different meanings
58 | - conversions are easy to abuse
59 | - implicits are very hard to track down while debugging (givens also not trivial, but they are explicitly imported)
60 | */
61 |
62 | /*
63 | The right way of doing contextual abstractions in Scala 3:
64 | - given/using clauses
65 | - extension methods
66 | - explicitly declared implicit conversions
67 | */
68 |
69 | def main(args: Array[String]): Unit = {
70 | println(sumOf10)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part4context/OrganizingCAs.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part4context
2 |
3 | object OrganizingCAs {
4 |
5 | val aList = List(2,3,1,4)
6 | val anOrderedList = aList.sorted
7 |
8 | // compiler fetches givens/EMs
9 | // 1 - local scope
10 | given reverseOrdering: Ordering[Int] with
11 | override def compare(x: Int, y: Int) = y - x
12 |
13 | // 2 - imported scope
14 | case class Person(name: String, age: Int)
15 | val persons = List(
16 | Person("Steve", 30),
17 | Person("Amy", 22),
18 | Person("John", 67)
19 | )
20 |
21 | object PersonGivens {
22 | given ageOrdering: Ordering[Person] with
23 | override def compare(x: Person, y: Person) = y.age - x.age
24 |
25 | extension (p: Person)
26 | def greet(): String = s"Heya, I'm ${p.name}, I'm so glad to meet you!"
27 | }
28 |
29 | // a - import explicitly
30 | // import PersonGivens.ageOrdering
31 |
32 | // b - import a given for a particular type
33 | import PersonGivens.{given Ordering[Person]}
34 |
35 | // c - import all givens
36 | // import PersonGivens.given
37 |
38 | // warning: import PersonGivens.* does NOT also import given instances!
39 |
40 | // 3 - companions of all types involved in method signature
41 | /*
42 | - Ordering
43 | - List
44 | - Person
45 | */
46 | // def sorted[B >: A](using ord: Ordering[B]): List[B]
47 |
48 | object Person {
49 | given byNameOrdering: Ordering[Person] with
50 | override def compare(x: Person, y: Person) =
51 | x.name.compareTo(y.name)
52 |
53 | extension (p: Person)
54 | def greet(): String = s"Hello, I'm ${p.name}."
55 | }
56 |
57 | val sortedPersons = persons.sorted
58 |
59 | /*
60 | Good practice tips:
61 | 1) When you have a "default" given (only ONE that makes sense) add it in the companion object of the type.
62 | 2) When you have MANY possible givens, but ONE that is dominant (used most), add that in the companion and the rest in other objects.
63 | 3) Whem you have MANY possible givens and NO ONE is dominant, add them in separate objects and import them explicitly.
64 | */
65 |
66 | // Same principles apply to extension methods as well.
67 |
68 | /**
69 | * Exercises. Create given instances for Ordering[Purchase]
70 | * - ordering by total price, descending = 50% of code base
71 | * - ordering by unit count, descending = 25% of code base
72 | * - ordering by unit price, ascending = 25% of code base
73 | */
74 | case class Purchase(nUnits: Int, unitPrice: Double)
75 |
76 | object Purchase {
77 | given totalPriceOrdering: Ordering[Purchase] with
78 | override def compare(x: Purchase, y: Purchase) = {
79 | val xTotalPrice = x.nUnits * x.unitPrice
80 | val yTotalPrice = y.nUnits * y.unitPrice
81 |
82 | if (xTotalPrice == yTotalPrice) 0
83 | else if (xTotalPrice < yTotalPrice) -1
84 | else 1
85 | }
86 | }
87 |
88 | object UnitCountOrdering {
89 | given unitCountOrdering: Ordering[Purchase] = Ordering.fromLessThan((x, y) => y.nUnits > x.nUnits)
90 | }
91 |
92 | object UnitPriceOrdering {
93 | given unitPriceOrdering: Ordering[Purchase] = Ordering.fromLessThan((x, y) => x.unitPrice < y.unitPrice)
94 | }
95 |
96 |
97 | def main(args: Array[String]): Unit = {
98 | println(anOrderedList)
99 | println(sortedPersons)
100 | import PersonGivens.* // includes extension methods
101 | println(Person("Daniel", 99).greet())
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part4context/TypeClasses.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part4context
2 |
3 | object TypeClasses {
4 |
5 | /*
6 | Small library to serialize some data to a standard format (HTML)
7 | */
8 |
9 | // V1: the OO way
10 | trait HTMLWritable {
11 | def toHtml: String
12 | }
13 |
14 | case class User(name: String, age: Int, email: String) extends HTMLWritable {
15 | override def toHtml = s"
$name ($age yo)
"
16 | }
17 |
18 | val bob = User("Bob", 43, "bob@rockthejvm.com")
19 | val bob2Html = bob.toHtml
20 | // same for other data structures that we want to serialize
21 |
22 | /*
23 | Drawbacks:
24 | - only available for the types WE write
25 | - can only provide ONE implementation
26 | */
27 |
28 | // V2: pattern matching
29 | object HTMLSerializerPM {
30 | def serializeToHtml(value: Any): String = value match {
31 | case User(name, age, email) => s"
$name ($age yo)
"
32 | case _ => throw new IllegalArgumentException("data structure not supported")
33 | }
34 | }
35 |
36 | /*
37 | Drawbacks:
38 | - lost type safety
39 | - need to modify a SINGLE piece of code every time
40 | - still ONE implementation
41 | */
42 |
43 | // V3 - type class
44 | // part 1 - type class definition
45 | trait HTMLSerializer[T] {
46 | def serialize(value: T): String
47 | }
48 |
49 | // part 2 - type class instances for the supported types
50 | given userSerializer: HTMLSerializer[User] with {
51 | override def serialize(value: User) = {
52 | val User(name, age, email) = value
53 | s"
$name ($age yo)
"
54 | }
55 | }
56 |
57 | val bob2Html_v2 = userSerializer.serialize(bob)
58 |
59 | /*
60 | Benefits:
61 | - can define serializers for other types OUTSIDE the "library"
62 | - multiple serializers for the same type, pick whichever you want
63 | */
64 | import java.util.Date
65 | given dateSerializer: HTMLSerializer[Date] with {
66 | override def serialize(date: Date) = s"
${date.toString()}
"
67 | }
68 |
69 | object SomeOtherSerializerFunctionality { // organize givens properly
70 | given partialUserSerializer: HTMLSerializer[User] with {
71 | override def serialize(user: User) = s"
${user.name}
"
72 | }
73 | }
74 |
75 | // part 3 - using the type class (user-facing API)
76 | object HTMLSerializer {
77 | def serialize[T](value: T)(using serializer: HTMLSerializer[T]): String =
78 | serializer.serialize(value)
79 |
80 | def apply[T](using serializer: HTMLSerializer[T]): HTMLSerializer[T] = serializer
81 | }
82 |
83 | val bob2Html_v3 = HTMLSerializer.serialize(bob)
84 | val bob2Html_v4 = HTMLSerializer[User].serialize(bob)
85 |
86 | // part 4
87 | object HTMLSyntax {
88 | extension [T](value: T)
89 | def toHTML(using serializer: HTMLSerializer[T]): String = serializer.serialize(value)
90 | }
91 |
92 | import HTMLSyntax.*
93 | val bob2Html_v5 = bob.toHTML
94 |
95 | /*
96 | Cool!
97 | - extend functionality to new types that we want to support
98 | - flexibility to add TC instances in a different place than the definition of the TC
99 | - choose implementations (by importing the right givens)
100 | - super expressive! (via extension methods)
101 | */
102 |
103 | def main(args: Array[String]): Unit = {
104 | println(bob2Html)
105 | println(bob2Html_v2)
106 | println(bob2Html_v3)
107 | println(bob2Html_v4)
108 | println(bob2Html_v5)
109 | }
110 | }
111 |
112 | // addendum: your recipe for the type class pattern
113 | object TypeClassTemplate {
114 | // 1 - type class definition
115 | trait MyTypeClass[T] {
116 | def action(value: T): String // can have multiple methods; adapt signatures to your needs
117 | }
118 | // 2 - type class instances
119 | given intInstance: MyTypeClass[Int] with
120 | override def action(value: Int) = value.toString
121 | // same for other types you want to support
122 |
123 | // 3 - user-facing API
124 | object MyTypeClass {
125 | // often similar to what the type class definition offers
126 | def action[T](value: T)(using instance: MyTypeClass[T]): String = instance.action(value)
127 | // often expose a method to retrieve the current given instance for a type (similar to summon)
128 | def apply[T](using instance: MyTypeClass[T]): MyTypeClass[T] = instance
129 | }
130 |
131 | // 4 - expressiveness through extension methods
132 | object MyTypeClassSyntax {
133 | extension [T](value: T)
134 | def action(using instance: MyTypeClass[T]): String =
135 | instance.action(value)
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part5ts/AdvancedInheritance.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part5ts
2 |
3 | object AdvancedInheritance {
4 |
5 | // 1 - composite types can be used on their own
6 | trait Writer[T] {
7 | def write(value: T): Unit
8 | }
9 |
10 | trait Stream[T] {
11 | def foreach(f: T => Unit): Unit
12 | }
13 |
14 | trait Closeable {
15 | def close(status: Int): Unit
16 | }
17 |
18 | // class MyDataStream extends Writer[String] with Stream[String] with Closeable { ... }
19 |
20 | def processStream[T](stream: Writer[T] with Stream[T] with Closeable): Unit = {
21 | stream.foreach(println)
22 | stream.close(0)
23 | }
24 |
25 | // 2 - diamond problem
26 |
27 | trait Animal { def name: String }
28 | trait Lion extends Animal { override def name = "Lion" }
29 | trait Tiger extends Animal { override def name = "Tiger" }
30 | class Liger extends Lion with Tiger
31 |
32 | def demoLiger(): Unit = {
33 | val liger = new Liger
34 | println(liger.name)
35 | }
36 |
37 | /*
38 | Pseudo-definition:
39 |
40 | class Liger extends Animal
41 | with { override def name = "Lion" }
42 | with { override def name = "Tiger" }
43 |
44 | Last override always gets picked.
45 | */
46 |
47 | // 3 - the super problem
48 |
49 | trait Cold { // cold colors
50 | def print() = println("cold")
51 | }
52 |
53 | trait Green extends Cold {
54 | override def print(): Unit = {
55 | println("green")
56 | super.print()
57 | }
58 | }
59 |
60 | trait Blue extends Cold {
61 | override def print(): Unit = {
62 | println("blue")
63 | super.print()
64 | }
65 | }
66 |
67 | class Red {
68 | def print() = println("red")
69 | }
70 |
71 | class White extends Red with Green with Blue {
72 | override def print(): Unit = {
73 | println("white")
74 | super.print()
75 | }
76 | }
77 |
78 | /*
79 | Expected result
80 | - white
81 | - red
82 |
83 | Actual result
84 | - white
85 | - blue
86 | - green
87 | - cold
88 | NO RED!!!
89 | See the slide explanation on why this happens.
90 | */
91 |
92 | def demoColorInheritance(): Unit = {
93 | val white = new White
94 | white.print()
95 | }
96 |
97 | def main(args: Array[String]): Unit = {
98 | demoColorInheritance()
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part5ts/FBoundedPolymorphism.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part5ts
2 |
3 | object FBoundedPolymorphism {
4 |
5 | object Problem {
6 | trait Animal {
7 | def breed: List[Animal]
8 | }
9 |
10 | class Cat extends Animal {
11 | override def breed: List[Animal] = List(new Cat, new Dog) // <-- problem!!
12 | }
13 |
14 | class Dog extends Animal {
15 | override def breed: List[Animal] = List(new Dog, new Dog, new Dog)
16 | }
17 |
18 | // losing type safety
19 | }
20 |
21 | object NaiveSolution {
22 | trait Animal {
23 | def breed: List[Animal]
24 | }
25 |
26 | class Cat extends Animal {
27 | override def breed: List[Cat] = List(new Cat, new Cat)
28 | }
29 |
30 | class Dog extends Animal {
31 | override def breed: List[Dog] = List(new Dog, new Dog, new Dog)
32 | }
33 |
34 | // I have to write the proper type signatures
35 | // problem: want the compiler to help
36 | }
37 |
38 | object FBP {
39 | trait Animal[A <: Animal[A]] { // recursive type, F-bounded polymorphism
40 | def breed: List[Animal[A]]
41 | }
42 |
43 | class Cat extends Animal[Cat] {
44 | override def breed: List[Animal[Cat]] = List(new Cat, new Cat)
45 | }
46 |
47 | class Dog extends Animal[Dog] {
48 | override def breed = List(new Dog, new Dog, new Dog)
49 | }
50 |
51 | // mess up FBP
52 | class Crocodile extends Animal[Dog] {
53 | override def breed = ??? // list of dogs
54 | }
55 | }
56 |
57 | // example: some ORM libraries
58 | trait Entity[E <: Entity[E]]
59 | // example: Java sorting library
60 | class Person extends Comparable[Person] { // FPB
61 | override def compareTo(o: Person) = ???
62 | }
63 |
64 | // FBP + self types
65 | object FBPSelf {
66 | trait Animal[A <: Animal[A]] { self: A =>
67 | def breed: List[Animal[A]]
68 | }
69 |
70 | class Cat extends Animal[Cat] { // Cat == Animal[Cat]
71 | override def breed: List[Animal[Cat]] = List(new Cat, new Cat)
72 | }
73 |
74 | class Dog extends Animal[Dog] {
75 | override def breed = List(new Dog, new Dog, new Dog)
76 | }
77 |
78 | // class Crocodile extends Animal[Dog] { // not ok, I must also extend Dog
79 | // override def breed = ??? // list of dogs
80 | // }
81 |
82 | // I can go one level deeper
83 | trait Fish extends Animal[Fish]
84 | class Cod extends Fish {
85 | override def breed = List(new Cod, new Cod)
86 | }
87 |
88 | class Shark extends Fish {
89 | override def breed: List[Animal[Fish]] = List(new Cod)
90 | }
91 |
92 | // solution level 2
93 | trait FishL2[A <: FishL2[A]] extends Animal[FishL2[A]] { self: A => }
94 | class Tuna extends FishL2[Tuna] {
95 | override def breed = List(new Tuna)
96 | }
97 | // not ok
98 | // class Swordfish extends FishL2[Swordfish] {
99 | // override def breed = List(new Tuna)
100 | // }
101 | }
102 |
103 | def main(args: Array[String]): Unit = {
104 |
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part5ts/HigherKindedTypes.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part5ts
2 |
3 | import scala.util.Try
4 |
5 | object HigherKindedTypes {
6 |
7 | class HigherKindedType[F[_]] // hkt
8 | class HigherKindedType2[F[_], G[_], A]
9 |
10 | val hkExample = new HigherKindedType[List]
11 | val hkExample2 = new HigherKindedType2[List, Option, String]
12 | // can use hkts for methods as well
13 |
14 | // why: abstract libraries, e.g. Cats
15 | // example: Functor
16 | val aList = List(1,2,3)
17 | val anOption = Option(2)
18 | val aTry = Try(42)
19 |
20 | val anIncrementedList = aList.map(_ + 1) // List(2,3,4)
21 | val anIncrementedOption = anOption.map(_ + 1) // Some(3)
22 | val anIncrementedTry = aTry.map(_ + 1) // Success(43)
23 |
24 | // "duplicated" APIs
25 | def do10xList(list: List[Int]): List[Int] = list.map(_ * 10)
26 | def do10xOption(option: Option[Int]): Option[Int] = option.map(_ * 10)
27 | def do10xTry(theTry: Try[Int]): Try[Int] = theTry.map(_ * 10)
28 |
29 | // DRY principle
30 | // step 1: TC definition
31 | trait Functor[F[_]] {
32 | def map[A, B](fa: F[A])(f: A => B): F[B]
33 | }
34 |
35 | // step 2: TC instances
36 | given listFunctor: Functor[List] with
37 | override def map[A, B](list: List[A])(f: A => B): List[B] = list.map(f)
38 |
39 | // step 3: "user-facing" API
40 | def do10x[F[_]](container: F[Int])(using functor: Functor[F]): F[Int] =
41 | functor.map(container)(_ * 10)
42 |
43 | // if you create TC instances for Option and Try, the do10x method will work on Option and Try too
44 |
45 | // step 4: extension methods
46 | extension [F[_], A](container: F[A])(using functor: Functor[F])
47 | def map[B](f: A => B): F[B] = functor.map(container)(f)
48 |
49 | def do10x_v2[F[_] : Functor](container: F[Int]): F[Int] =
50 | container.map(_ * 10) // map is an extension method
51 |
52 | /**
53 | * Exercise: implement a new type class on the same structure as Functor.
54 | * In the general API, must use for-comprehensions.
55 | */
56 | def combineList[A, B](listA: List[A], listB: List[B]): List[(A, B)] =
57 | for {
58 | a <- listA
59 | b <- listB
60 | } yield (a, b)
61 |
62 | def combineOption[A, B](optionA: Option[A], optionB: Option[B]): Option[(A, B)] =
63 | for {
64 | a <- optionA
65 | b <- optionB
66 | } yield (a, b)
67 |
68 | def combineTry[A, B](tryA: Try[A], tryB: Try[B]): Try[(A, B)] =
69 | for {
70 | a <- tryA
71 | b <- tryB
72 | } yield (a, b)
73 |
74 |
75 | /*
76 | def combine[F[_]: SomeTC, A, B](fa: F[A], fb: F[B]): F[(A,B)] =
77 | for {
78 | a <- fa
79 | b <- fb
80 | } yield (a, b)
81 | */
82 |
83 | // 1 - TC definition
84 | trait Monad[F[_]] extends Functor[F] {
85 | def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
86 | }
87 |
88 | // 2 - TC instance(s)
89 | given magicList: Monad[List] with {
90 | override def map[A, B](list: List[A])(f: A => B) = list.map(f)
91 | override def flatMap[A, B](list: List[A])(f: A => List[B]) = list.flatMap(f)
92 | }
93 |
94 | // 3 - "user-facing" API
95 | def combine[F[_], A, B](fa: F[A], fb: F[B])(using magic: Monad[F]): F[(A, B)] =
96 | magic.flatMap(fa)(a => magic.map(fb)(b => (a,b)))
97 | // listA.map(a => listB.map(b => (a,b))
98 |
99 | extension [F[_], A](container: F[A])(using magic: Monad[F])
100 | def flatMap[B](f: A => F[B]): F[B] = magic.flatMap(container)(f)
101 |
102 | def combine_v2[F[_] : Monad, A, B](fa: F[A], fb: F[B]): F[(A, B)] =
103 | for {
104 | a <- fa
105 | b <- fb
106 | } yield (a, b)
107 |
108 | def main(args: Array[String]): Unit = {
109 | println(do10x(List(1,2,3)))
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part5ts/LiteralUnionIntersectionTypes.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part5ts
2 |
3 | import java.io.File
4 |
5 | object LiteralUnionIntersectionTypes {
6 |
7 | // 1 - literal types
8 | val aNumber = 3
9 | val three: 3 = 3
10 |
11 | def passNumber(n: Int) = println(n)
12 | passNumber(45) // ok
13 | passNumber(three) // ok, 3 <: Int
14 |
15 | def passStrict(n: 3) = println(n)
16 | passStrict(three) // ok
17 | // passStrict(45) // not ok, Int <: 3
18 |
19 | // available for double, boolean, strings
20 | val pi: 3.14 = 3.14
21 | val truth: true = true
22 | val favLang: "Scala" = "Scala"
23 |
24 | // literal types can be used as type arguments (just like any other types)
25 | def doSomethingWithYourLife(meaning: Option[42]) = meaning.foreach(println)
26 |
27 | // 2 - union types
28 | val truthor42: Boolean | Int = 43
29 |
30 | def ambivalentMethod(arg: String | Int): String = arg match {
31 | case _: String => "a string"
32 | case _: Int => "a number"
33 | } // PM complete
34 |
35 | val aNumberDescription = ambivalentMethod(56) // ok
36 | val aStringDescription = ambivalentMethod("Scala") // ok
37 |
38 | // type inference chooses a LCA of the two types instead of the String | Int
39 | val stringOrInt = if (43 > 0) "a string" else 45
40 | val stringOrInt_v2: String | Int = if (43 > 0) "a string" else 45 // ok
41 |
42 | // union types + nulls
43 | type Maybe[T] = T | Null // not null
44 | def handleMaybe(someValue: Maybe[String]): Int =
45 | if (someValue != null) someValue.length // flow typing
46 | else 0
47 |
48 | type ErrorOr[T] = T | "error"
49 | // def handleResource(arg: ErrorOr[Int]): Unit =
50 | // if (arg != "error") println(arg + 1) // flow typing doesn't work
51 | // else println("Error!")
52 |
53 | // 3 - intersection types
54 | class Animal
55 | trait Carnivore
56 | class Crocodile extends Animal with Carnivore
57 | val carnivoreAnimal: Animal & Carnivore = new Crocodile
58 |
59 | trait Gadget {
60 | def use(): Unit
61 | }
62 |
63 | trait Camera extends Gadget {
64 | def takePicture() = println("smile!")
65 | override def use() = println("snap")
66 | }
67 |
68 | trait Phone extends Gadget {
69 | def makePhoneCall() = println("calling...")
70 | override def use() = println("ring")
71 | }
72 |
73 | def useSmartDevice(sp: Camera & Phone): Unit = {
74 | sp.takePicture()
75 | sp.makePhoneCall()
76 | sp.use() // which use() is being called? can't tell
77 | }
78 |
79 | class SmartPhone extends Phone with Camera // diamond problem
80 | class CameraWithPhone extends Camera with Phone
81 |
82 | // intersection types + covariance
83 | trait HostConfig
84 | trait HostController {
85 | def get: Option[HostConfig]
86 | }
87 |
88 | trait PortConfig
89 | trait PortController {
90 | def get: Option[PortConfig]
91 | }
92 |
93 | def getConfigs(controller: HostController & PortController): Option[HostConfig & PortConfig] = controller.get
94 | // compiles
95 |
96 | def main(args: Array[String]): Unit = {
97 | useSmartDevice(new SmartPhone) // "snap"
98 | useSmartDevice(new CameraWithPhone) // "ring"
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part5ts/OpaqueTypes.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part5ts
2 |
3 | object OpaqueTypes {
4 |
5 | object SocialNetwork {
6 | // usually applied inside tooling/libraries managing data types
7 | // domain = all the type definitions for your business use case
8 | opaque type Name = String
9 |
10 | // API entry point #1 - companion objects, useful for factory methods
11 | object Name {
12 | def apply(str: String): Name = str
13 | }
14 |
15 | // API entry point #2 - extension methods, give you control of the methods YOU want to expose
16 | extension (name: Name)
17 | def length: Int = name.length // use String API
18 |
19 | // inside, Name <-> String
20 | def addFriend(person1: Name, person2: Name): Boolean =
21 | person1.length == person2.length // use the entire String API
22 | }
23 |
24 | // outside SocialNetwork, Name and String are NOT related
25 | import SocialNetwork.*
26 | // val name: Name = "Daniel" // will not compile, Name != String
27 |
28 | // why opaque types: when you don't need (or want) to offer access to the entire String API for the Name type
29 |
30 | object Graphics {
31 | opaque type Color = Int // in hex
32 | opaque type ColorFilter <: Color = Int
33 |
34 | val Red: Color = 0xFF000000
35 | val Green: Color = 0x00FF0000
36 | val Blue: Color = 0x0000FF00
37 | val halfTransparency: ColorFilter = 0x88 // 50%
38 | }
39 |
40 | import Graphics.*
41 | case class OverlayFilter(c: Color)
42 | val fadeLayer = OverlayFilter(halfTransparency) // ColorFilter <: Color
43 |
44 | // how can we create instances of opaque types + how to access their APIs
45 | // 1 - companion objects
46 | val aName = Name("Daniel") // ok
47 | // 2 - extension methods
48 | val nameLength = aName.length // ok because of the extension method
49 |
50 | def main(args: Array[String]): Unit = {
51 |
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part5ts/PathDependentTypes.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part5ts
2 |
3 | object PathDependentTypes {
4 |
5 | class Outer {
6 | class Inner
7 | object InnerObject
8 | type InnerType
9 |
10 | def process(arg: Inner) = println(arg)
11 | def processGeneral(arg: Outer#Inner) = println(arg)
12 | }
13 |
14 | val outer = new Outer
15 | val inner = new outer.Inner // outer.Inner is a separate TYPE = path-dependent type
16 |
17 | val outerA = new Outer
18 | val outerB = new Outer
19 | // val inner2: outerA.Inner = new outerB.Inner // path-dependent types are DIFFERENT
20 | val innerA = new outerA.Inner
21 | val innerB = new outerB.Inner
22 |
23 | // outerA.process(innerB) // same type mismatch
24 | outer.process(inner) // ok
25 |
26 | // parent-type: Outer#Inner
27 | outerA.processGeneral(innerA) // ok
28 | outerA.processGeneral(innerB) // ok, outerB.Inner <: Outer#Inner
29 |
30 | /*
31 | Why:
32 | - type-checking/type inference, e.g. Akka Streams: Flow[Int, Int, NotUsed]#Repr
33 | - type-level programming
34 | */
35 |
36 | // methods with dependent types: return a different COMPILE-TIME type depending on the argument
37 | // no need for generics
38 | trait Record {
39 | type Key
40 | def defaultValue: Key
41 | }
42 |
43 | class StringRecord extends Record {
44 | override type Key = String
45 | override def defaultValue = ""
46 | }
47 |
48 | class IntRecord extends Record {
49 | override type Key = Int
50 | override def defaultValue = 0
51 | }
52 |
53 | // user-facing API
54 | def getDefaultIdentifier(record: Record): record.Key = record.defaultValue
55 |
56 | val aString: String = getDefaultIdentifier(new StringRecord) // a string
57 | val anInt: Int = getDefaultIdentifier(new IntRecord) // an int, ok
58 |
59 | // functions with dependent types
60 | val getIdentifierFunc: Record => Record#Key = getDefaultIdentifier // eta-expansion
61 |
62 |
63 | def main(args: Array[String]): Unit = {
64 |
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part5ts/SelfTypes.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part5ts
2 |
3 | object SelfTypes {
4 |
5 | trait Instrumentalist {
6 | def play(): Unit
7 | }
8 |
9 | trait Singer { self: Instrumentalist => // self-type: whoever implements Singer MUST also implement Instrumentalist
10 | // ^^ name can be anything, usually called "self"
11 | // DO NOT confuse this with a lambda
12 |
13 | // rest of your API
14 | def sing(): Unit
15 | }
16 |
17 | class LeadSinger extends Singer with Instrumentalist { // ok
18 | override def sing(): Unit = ???
19 | override def play(): Unit = ???
20 | }
21 |
22 | // not ok, because I've not extended Instrumentalist
23 | // class Vocalist extends Singer {
24 | //
25 | // }
26 |
27 | val jamesHetfield = new Singer with Instrumentalist { // ok
28 | override def sing(): Unit = ???
29 | override def play(): Unit = ???
30 | }
31 |
32 | class Guitarist extends Instrumentalist {
33 | override def play(): Unit = println("some guitar solo")
34 | }
35 |
36 | val ericClapton = new Guitarist with Singer { // ok - extending Guitarist <: Instrumentalist
37 | override def sing(): Unit = println("layla")
38 | }
39 |
40 | // self-types vs inheritance
41 | class A
42 | class B extends A // B "is an" A
43 |
44 | trait T
45 | trait S { self: T => } // S "requires a" T
46 |
47 | // self-types for DI = "cake pattern"
48 |
49 | // normal DI
50 | abstract class Component {
51 | // main general API
52 | }
53 | class ComponentA extends Component
54 | class ComponentB extends Component
55 | class DependentComponent(val component: Component)
56 |
57 | // cake pattern
58 | trait ComponentLayer1 {
59 | // API
60 | def actionLayer1(x: Int): String
61 | }
62 | trait ComponentLayer2 { self: ComponentLayer1 =>
63 | // some other API
64 | def actionLayer2(x: String): Int
65 | }
66 | trait Application { self: ComponentLayer1 with ComponentLayer2 =>
67 | // your main API
68 | }
69 |
70 | // example: a photo taking application API in the style of Instagram
71 | // layer 1 - small components
72 | trait Picture extends ComponentLayer1
73 | trait Stats extends ComponentLayer1
74 |
75 | // layer 2 - compose
76 | trait ProfilePage extends ComponentLayer2 with Picture
77 | trait Analytics extends ComponentLayer2 with Stats
78 |
79 | // layer 3 - application
80 | trait AnalyticsApp extends Application with Analytics
81 | // dependencies are specified in layers, like baking a cake
82 | // when you put the pieces together, you can pick a possible implementation from each layer
83 |
84 | // self-types: preserve the "this" instance
85 | class SingerWithInnerClass { self => // self-type with no type requirement, self == this
86 | class Voice {
87 | def sing() = this.toString // this == the voice, use "self" to refer to the outer instance
88 | }
89 | }
90 |
91 | // cyclical inheritance does NOT work
92 | // class X extends Y
93 | // class Y extends X
94 |
95 | // cyclical dependencies
96 | trait X { self: Y => }
97 | trait Y { self: X => }
98 | trait Z extends X with Y // all good
99 |
100 | def main(args: Array[String]): Unit = {
101 |
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part5ts/StructuralTypes.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part5ts
2 |
3 | import reflect.Selectable.reflectiveSelectable
4 |
5 | object StructuralTypes {
6 |
7 | type SoundMaker = { // structural type
8 | def makeSound(): Unit
9 | }
10 |
11 | class Dog {
12 | def makeSound(): Unit = println("bark!")
13 | }
14 |
15 | class Car {
16 | def makeSound(): Unit = println("vroom!")
17 | }
18 |
19 | val dog: SoundMaker = new Dog // ok
20 | val car: SoundMaker = new Car
21 | // compile-time duck typing
22 |
23 | // type refinements
24 | abstract class Animal {
25 | def eat(): String
26 | }
27 |
28 | type WalkingAnimal = Animal { // refined type
29 | def walk(): Int
30 | }
31 |
32 | // why: creating type-safe APIs for existing types following the same structure, but no connection to each other
33 | type JavaCloseable = java.io.Closeable
34 | class CustomCloseable {
35 | def close(): Unit = println("ok ok I'm closing")
36 | def closeSilently(): Unit = println("not making a sound, I promise")
37 | }
38 |
39 | // def closeResource(closeable: JavaCloseable | CustomCloseable): Unit =
40 | // closeable.close() // not ok
41 |
42 | // solution: structural type
43 | type UnifiedCloseable = {
44 | def close(): Unit
45 | }
46 |
47 | // unified API: any type which conforms to the structure of UnifiedCloseable is a legitimate argument
48 | def closeResource(closeable: UnifiedCloseable): Unit = closeable.close()
49 | val jCloseable = new JavaCloseable {
50 | override def close(): Unit = println("closing Java resource")
51 | }
52 | val cCloseable = new CustomCloseable
53 |
54 | // same unified API, with the structural type inline
55 | def closeResource_v2(closeable: { def close(): Unit }): Unit = closeable.close()
56 | // | |
57 | // | this is a type: |
58 | // | structrural type |
59 |
60 | def main(args: Array[String]): Unit = {
61 | dog.makeSound() // through reflection (slow)
62 | car.makeSound()
63 |
64 | closeResource(jCloseable)
65 | closeResource(cCloseable)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part5ts/TypeMembers.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part5ts
2 |
3 | object TypeMembers {
4 |
5 | class Animal
6 | class Dog extends Animal
7 | class Cat extends Animal
8 |
9 | class AnimalCollection {
10 | // val, var, def, class, trait, object
11 | type AnimalType // abstract type member
12 | type BoundedAnimal <: Animal // abstract type member with a type bound
13 | type SuperBoundedAnimal >: Dog <: Animal
14 | type AnimalAlias = Cat // type alias
15 | type NestedOption = List[Option[Option[Int]]] // often used to alias complex/nested types
16 | }
17 |
18 | // using type members
19 | val ac = new AnimalCollection
20 | val anAnimal: ac.AnimalType = ???
21 |
22 | // val cat: ac.BoundedAnimal = new Cat // BoundedAnimal might be Dog
23 | val aDog: ac.SuperBoundedAnimal = new Dog // ok, Dog <: SuperBoundedAnimal
24 | val aCat: ac.AnimalAlias = new Cat // ok, Cat == AnimalAlias
25 |
26 | // establish relationships between types
27 | // alternative to generics
28 | class LList[T] {
29 | def add(element: T): LList[T] = ???
30 | }
31 |
32 | class MyList {
33 | type T
34 | def add(element: T): MyList = ???
35 | }
36 |
37 | // .type
38 | type CatType = aCat.type
39 | val newCat: CatType = aCat
40 |
41 | def main(args: Array[String]): Unit = {
42 |
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part5ts/Variance.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part5ts
2 |
3 | import com.rockthejvm.part1as.Recap.LList
4 |
5 | object Variance {
6 |
7 | class Animal
8 | class Dog(name: String) extends Animal
9 |
10 | // Variance question for List: if Dog extends Animal, then should a List[Dog] "extend" List[Animal]?
11 |
12 | // for List, YES - List is COVARIANT
13 | val lassie = new Dog("Lassie")
14 | val hachi = new Dog("Hachi")
15 | val laika = new Dog("Laika")
16 |
17 | val anAnimal: Animal = lassie // ok, Dog <: Animal
18 | val myDogs: List[Animal] = List(lassie, hachi, laika) // ok - List is COVARIANT: a list of dogs is a list of animals
19 |
20 | // define covariant types
21 | class MyList[+A] // MyList is COVARIANT in A
22 | val aListOfAnimals: MyList[Animal] = new MyList[Dog]
23 |
24 | // if NO, then the type is INVARIANT
25 | trait Semigroup[A] { // no marker = INVARIANT
26 | def combine(x: A, y: A): A
27 | }
28 |
29 | // java generics
30 | // val aJavaList: java.util.ArrayList[Animal] = new java.util.ArrayList[Dog] // type mismatch: java generics are all INVARIANT
31 |
32 | // Hell NO - CONTRAVARIANCE
33 | // if Dog <: Animal, then Vet[Animal] <: Vet[Dog]
34 | trait Vet[-A] { // contravariant in A
35 | def heal(animal: A): Boolean
36 | }
37 |
38 | val myVet: Vet[Dog] = new Vet[Animal] {
39 | override def heal(animal: Animal) = {
40 | println("Hey there, you're all good...")
41 | true
42 | }
43 | }
44 | // if the vet can treat any animal, she/he can treat my dog too
45 | val healLaika = myVet.heal(laika) // ok
46 |
47 | /*
48 | Rule of thumb:
49 | - if your type PRODUCES or RETRIEVES a value (e.g. a list), then it should be COVARIANT
50 | - if your type ACTS ON or CONSUMES a value (e.g. a vet), then it should be CONTRAVARIANT
51 | - otherwise, INVARIANT
52 | */
53 |
54 | /**
55 | * Exercises
56 | */
57 | // 1 - which types should be invariant, covariant, contravariant
58 | class RandomGenerator[+A] // produces values: Covariant
59 | class MyOption[+A] // similar to Option[A]
60 | class JSONSerializer[-A] // consumes values and turns them into strings: Contravariant
61 | trait MyFunction[-A, +B] // similar to Function1[A, B]
62 |
63 | // 2 - add variance modifiers to this "library"
64 | abstract class LList[+A] {
65 | def head: A
66 | def tail: LList[A]
67 | }
68 |
69 | case object EmptyList extends LList[Nothing] {
70 | override def head = throw new NoSuchElementException
71 | override def tail = throw new NoSuchElementException
72 | }
73 |
74 | case class Cons[+A](override val head: A, override val tail: LList[A]) extends LList[A]
75 |
76 | val aList: LList[Int] = EmptyList // fine
77 | val anotherList: LList[String] = EmptyList // also fine
78 | // Nothing <: A, then LList[Nothing] <: LList[A]
79 |
80 | def main(args: Array[String]): Unit = {
81 |
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/part5ts/VariancePositions.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.part5ts
2 |
3 | object VariancePositions {
4 |
5 | class Animal
6 | class Dog extends Animal
7 | class Cat extends Animal
8 | class Crocodile extends Animal
9 |
10 | /**
11 | * 1 - type bounds
12 | */
13 |
14 | class Cage[A <: Animal] // A must be a subtype of Animal
15 | val aRealCage = new Cage[Dog] // ok, Dog <: Animal
16 | // val aCage = new Cage[String] // not ok, String is not a subtype of animal
17 |
18 | class WeirdContainer[A >: Animal] // A must be a supertype of Animal
19 |
20 | /**
21 | * 2 - variance positions
22 | */
23 |
24 | // types of val fields are in COVARIANT position
25 | // class Vet[-T](val favoriteAnimal: T)
26 |
27 | /*
28 | val garfield = new Cat
29 | val theVet: Vet[Animal] = new Vet[Animal](garfield)
30 | val aDogVet: Vet[Dog] = theVet // possible, theVet is Vet[Animal]
31 | val aDog: Dog = aDogVet.favoriteAnimal // must be a Dog - type conflict!
32 | */
33 |
34 | // types of var fields are in COVARIANT position
35 | // (same reason)
36 |
37 | // types of var fields are in CONTRAVARIANT position
38 | // class MutableOption[+T](var contents: T)
39 |
40 | /*
41 | val maybeAnimal: MutableOption[Animal] = new MutableOption[Dog](new Dog)
42 | maybeAnimal.contents = new Cat // type conflict!
43 | */
44 |
45 | // types of method arguments are in CONTRAVARIANT position
46 | // class MyList[+T] {
47 | // def add(element: T): MyList[T] = ???
48 | // }
49 |
50 | class Vet[-T] {
51 | def heal(animal: T): Boolean = true
52 | }
53 |
54 | /*
55 | val animals: MyList[Animal] = new MyList[Cat]
56 | val biggerListOfAnimals = animals.add(new Dog) // type conflict!
57 | */
58 |
59 | // method return types are in COVARIANT position
60 | // abstract class Vet2[-T] {
61 | // def rescueAnimal(): T
62 | // }
63 |
64 | /*
65 | val vet: Vet2[Animal] = new Vet2[Animal] {
66 | override def rescueAnimal(): Animal = new Cat
67 | }
68 | val lassiesVet: Vet2[Dog] = vet // Vet2[Animal]
69 | val rescueDog: Dog = lassiesVet.rescueAnimal() // must return a Dog, returns a Cat - type conflict!
70 | */
71 |
72 | /**
73 | * 3 - solving variance positions problems
74 | */
75 | abstract class LList[+A] {
76 | def add[B >: A](element: B): LList[B] // widen the type
77 | }
78 | // val animals: List[Cat] = list of cats
79 | // val newAnimals: List[Animal] = animals.add(new Dog)
80 |
81 | class Vehicle
82 | class Car extends Vehicle
83 | class Supercar extends Car
84 | class RepairShop[-A <: Vehicle] {
85 | def repair[B <: A](vehicle: B): B = vehicle // narrowing the type
86 | }
87 |
88 | val myRepairShop: RepairShop[Car] = new RepairShop[Vehicle]
89 | val myBeatupVW = new Car
90 | val freshCar = myRepairShop.repair(myBeatupVW) // works, returns a car
91 | val damagedFerrari = new Supercar
92 | val freshFerrari = myRepairShop.repair(damagedFerrari) // works, returns a Supercar
93 |
94 | def main(args: Array[String]): Unit = {
95 |
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/playground/Playground.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.playground
2 |
3 | object Playground {
4 | def main(args: Array[String]): Unit = {
5 | println("Up and running! Looking forward to getting advanced with Scala!")
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/practice/FunctionalSet.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.practice
2 |
3 | import scala.annotation.tailrec
4 |
5 | abstract class FSet[A] extends (A => Boolean) {
6 | // main api
7 | def contains(elem: A): Boolean
8 | def apply(elem: A): Boolean = contains(elem)
9 |
10 | infix def +(elem: A): FSet[A]
11 | infix def ++(anotherSet: FSet[A]): FSet[A]
12 |
13 | // "classics"
14 | def map[B](f: A => B): FSet[B]
15 | def flatMap[B](f: A => FSet[B]): FSet[B]
16 | def filter(predicate: A => Boolean): FSet[A]
17 | def foreach(f: A => Unit): Unit
18 |
19 | // utilities
20 | infix def -(elem: A): FSet[A]
21 | infix def --(anotherSet: FSet[A]): FSet[A]
22 | infix def &(anotherSet: FSet[A]): FSet[A]
23 |
24 | // "negation" == all the elements of type A EXCEPT the elements in this set
25 | def unary_! : FSet[A] = new PBSet(!contains(_))
26 | }
27 |
28 | // example { x in N | x % 2 == 0 }
29 | // property-based set
30 | class PBSet[A](property: A => Boolean) extends FSet[A] {
31 | // main api
32 | def contains(elem: A): Boolean = property(elem)
33 |
34 | infix def +(elem: A): FSet[A] =
35 | new PBSet(x => x == elem || property(x))
36 | infix def ++(anotherSet: FSet[A]): FSet[A] =
37 | new PBSet(x => property(x) || anotherSet(x))
38 |
39 | // "classics"
40 | def map[B](f: A => B): FSet[B] =
41 | politelyFail()
42 | def flatMap[B](f: A => FSet[B]): FSet[B] =
43 | politelyFail()
44 | def filter(predicate: A => Boolean): FSet[A] =
45 | new PBSet(x => property(x) && predicate(x))
46 | def foreach(f: A => Unit): Unit =
47 | politelyFail()
48 |
49 | // utilities
50 | infix def -(elem: A): FSet[A] =
51 | filter(x => x != elem)
52 | infix def --(anotherSet: FSet[A]): FSet[A] =
53 | filter(!anotherSet)
54 | infix def &(anotherSet: FSet[A]): FSet[A] =
55 | filter(anotherSet)
56 |
57 | // extra utilities (internal)
58 | private def politelyFail() = throw new RuntimeException("I don't know if this set is iterable...")
59 | }
60 |
61 | case class Empty[A]() extends FSet[A] { // PBSet(x => false)
62 | override def contains(elem: A) = false
63 | infix def +(elem: A): FSet[A] = Cons(elem, this)
64 | infix def ++(anotherSet: FSet[A]): FSet[A] = anotherSet
65 |
66 | // "classics"
67 | def map[B](f: A => B): FSet[B] = Empty()
68 | def flatMap[B](f: A => FSet[B]): FSet[B] = Empty()
69 | def filter(predicate: A => Boolean): FSet[A] = this
70 | def foreach(f: A => Unit): Unit = ()
71 |
72 | // utilities
73 | infix def -(elem: A): FSet[A] = this
74 | infix def --(anotherSet: FSet[A]): FSet[A] = this
75 | infix def &(anotherSet: FSet[A]): FSet[A] = this
76 |
77 | }
78 |
79 | case class Cons[A](head: A, tail: FSet[A]) extends FSet[A] {
80 | override def contains(elem: A) = elem == head || tail.contains(elem)
81 | infix def +(elem: A): FSet[A] =
82 | if (contains(elem)) this
83 | else Cons(elem, this)
84 |
85 | infix def ++(anotherSet: FSet[A]): FSet[A] = tail ++ anotherSet + head
86 |
87 | // "classics"
88 | def map[B](f: A => B): FSet[B] = tail.map(f) + f(head)
89 | def flatMap[B](f: A => FSet[B]): FSet[B] = tail.flatMap(f) ++ f(head)
90 | def filter(predicate: A => Boolean): FSet[A] = {
91 | val filteredTail = tail.filter(predicate)
92 | if predicate(head) then filteredTail + head
93 | else filteredTail
94 | }
95 |
96 | def foreach(f: A => Unit): Unit = {
97 | f(head)
98 | tail.foreach(f)
99 | }
100 |
101 | // utilities
102 | infix def -(elem: A): FSet[A] =
103 | if (head == elem) tail
104 | else tail - elem + head
105 |
106 | infix def --(anotherSet: FSet[A]): FSet[A] = filter(!anotherSet)
107 | infix def &(anotherSet: FSet[A]): FSet[A] = filter(anotherSet) // intersection = filtering
108 | }
109 |
110 | object FSet {
111 | def apply[A](values: A*): FSet[A] = {
112 | @tailrec
113 | def buildSet(valuesSeq: Seq[A], acc: FSet[A]): FSet[A] =
114 | if (valuesSeq.isEmpty) acc
115 | else buildSet(valuesSeq.tail, acc + valuesSeq.head)
116 |
117 | buildSet(values, Empty())
118 | }
119 | }
120 |
121 | object FunctionalSetPlayground {
122 |
123 | def main(args: Array[String]): Unit = {
124 |
125 | val first5 = FSet(1,2,3,4,5)
126 | val someNumbers = FSet(4,5,6,7,8)
127 | println(first5.contains(5)) // true
128 | println(first5(6)) // false
129 | println((first5 + 10).contains(10)) // true
130 | println(first5.map(_ * 2).contains(10)) // true
131 | println(first5.map(_ % 2).contains(1)) // true
132 | println(first5.flatMap(x => FSet(x, x + 1)).contains(7)) // false
133 |
134 | println((first5 - 3).contains(3)) // false
135 | println((first5 -- someNumbers).contains(4)) // false
136 | println((first5 & someNumbers).contains(4)) // true
137 |
138 | val naturals = new PBSet[Int](_ => true)
139 | println(naturals.contains(5237548)) // true
140 | println(!naturals.contains(0)) // false
141 | println((!naturals + 1 + 2 + 3).contains(3)) // true
142 | // println(!naturals.map(_ + 1)) // throws - map/flatMap/foreach will not work
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/practice/JSONSerialization.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.practice
2 |
3 | import java.util.Date
4 |
5 | object JSONSerialization {
6 |
7 | /*
8 | Users, posts, feeds
9 | Serialize to JSON
10 | */
11 |
12 | case class User(name: String, age: Int, email: String)
13 | case class Post(content: String, createdAt: Date)
14 | case class Feed(user: User, posts: List[Post])
15 |
16 | /*
17 | 1 - intermediate data: numbers, strings, lists, objects
18 | 2 - type class to convert data to intermediate data
19 | 3 - serialize to JSON
20 | */
21 |
22 | sealed trait JSONValue {
23 | def stringify: String
24 | }
25 |
26 | final case class JSONString(value: String) extends JSONValue {
27 | override def stringify = "\"" + value + "\""
28 | }
29 |
30 | final case class JSONNumber(value: Int) extends JSONValue {
31 | override def stringify = value.toString
32 | }
33 |
34 | final case class JSONArray(values: List[JSONValue]) extends JSONValue {
35 | override def stringify = values.map(_.stringify).mkString("[", ",", "]")
36 | }
37 |
38 | final case class JSONObject(values: Map[String, JSONValue]) extends JSONValue {
39 | override def stringify = values
40 | .map {
41 | case (key, value) => "\"" + key + "\":" + value.stringify
42 | }
43 | .mkString("{", ",", "}")
44 | }
45 |
46 | /*
47 | {
48 | "name": "John",
49 | "age": 22,
50 | "friends": [...],
51 | "latestPost" : { ... }
52 | }
53 | */
54 |
55 | val data = JSONObject(Map(
56 | "user" -> JSONString("Daniel"),
57 | "posts" -> JSONArray(List(
58 | JSONString("Scala is awesome!"),
59 | JSONNumber(42)
60 | ))
61 | ))
62 |
63 | // part 2 - type class pattern
64 | // 1 - TC definition
65 | trait JSONConverter[T] {
66 | def convert(value: T): JSONValue
67 | }
68 |
69 | // 2 - TC instances for String, Int, Date, User, Post, Feed
70 | given stringConverter: JSONConverter[String] with
71 | override def convert(value: String) = JSONString(value)
72 |
73 | given intConverter: JSONConverter[Int] with
74 | override def convert(value: Int) = JSONNumber(value)
75 |
76 | given dateConverter: JSONConverter[Date] with
77 | override def convert(value: Date) = JSONString(value.toString)
78 |
79 | given userConverter: JSONConverter[User] with
80 | override def convert(user: User) = JSONObject(Map(
81 | "name" -> JSONConverter[String].convert(user.name),
82 | "age" -> JSONConverter[Int].convert(user.age),
83 | "email" -> JSONConverter[String].convert(user.email)
84 | ))
85 |
86 | given postConverter: JSONConverter[Post] with
87 | override def convert(post: Post) = JSONObject(Map(
88 | "content" -> JSONConverter[String].convert(post.content),
89 | "createdAt" -> JSONConverter[String].convert(post.createdAt.toString)
90 | ))
91 |
92 | given feedConverter: JSONConverter[Feed] with
93 | override def convert(feed: Feed) = JSONObject(Map(
94 | "user" -> JSONConverter[User].convert(feed.user),
95 | "posts" -> JSONArray(feed.posts.map(post => JSONConverter[Post].convert(post)))
96 | ))
97 |
98 | // 3 - user-facing API
99 | object JSONConverter {
100 | def convert[T](value: T)(using converter: JSONConverter[T]): JSONValue =
101 | converter.convert(value)
102 |
103 | def apply[T](using instance: JSONConverter[T]): JSONConverter[T] = instance
104 | }
105 |
106 | // example
107 | val now = new Date(System.currentTimeMillis())
108 | val john = User("John", 34, "john@rockthejvm.com")
109 | val feed = Feed(john, List(
110 | Post("Hello, I'm learning type classes", now),
111 | Post("Look at this cute puppy!", now),
112 | ))
113 |
114 | // 4 - extension methods
115 | object JSONSyntax {
116 | extension [T](value: T) {
117 | def toIntermediate(using converter: JSONConverter[T]): JSONValue =
118 | converter.convert(value)
119 |
120 | def toJSON(using converter: JSONConverter[T]): String =
121 | toIntermediate.stringify
122 | }
123 | }
124 |
125 | def main(args: Array[String]): Unit = {
126 | import JSONSyntax.*
127 |
128 | println(feed.toJSON)
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/main/scala/com/rockthejvm/practice/LzList.scala:
--------------------------------------------------------------------------------
1 | package com.rockthejvm.practice
2 |
3 | import scala.annotation.tailrec
4 |
5 | // Write a lazily evaluated, potentially INFINITE linked list
6 |
7 | abstract class LzList[A] {
8 | def isEmpty: Boolean
9 | def head: A
10 | def tail: LzList[A]
11 |
12 | // utilities
13 | def #::(element: A): LzList[A] // prepending
14 | infix def ++(another: => LzList[A]): LzList[A]
15 |
16 | // classics
17 | def foreach(f: A => Unit): Unit
18 | def map[B](f: A => B): LzList[B]
19 | def flatMap[B](f: A => LzList[B]): LzList[B]
20 | def filter(predicate: A => Boolean): LzList[A]
21 | def withFilter(predicate: A => Boolean): LzList[A] = filter(predicate)
22 |
23 | def take(n: Int): LzList[A] // takes the first n elements from this lazy list
24 | def takeAsList(n: Int): List[A] =
25 | take(n).toList
26 |
27 | def toList: List[A] = {
28 | @tailrec
29 | def toListAux(remaining: LzList[A], acc: List[A]): List[A] =
30 | if (remaining.isEmpty) acc.reverse
31 | else toListAux(remaining.tail, remaining.head :: acc)
32 |
33 | toListAux(this, List())
34 | }
35 | }
36 |
37 | case class LzEmpty[A]() extends LzList[A] {
38 | def isEmpty: Boolean = true
39 | def head: A = throw new NoSuchElementException
40 | def tail: LzList[A] = throw new NoSuchElementException
41 |
42 | // utilities
43 | def #::(element: A): LzList[A] = new LzCons(element, this)
44 | infix def ++(another: => LzList[A]): LzList[A] = another
45 |
46 | // classics
47 | def foreach(f: A => Unit): Unit = ()
48 | def map[B](f: A => B): LzList[B] = LzEmpty()
49 | def flatMap[B](f: A => LzList[B]): LzList[B] = LzEmpty()
50 | def filter(predicate: A => Boolean): LzList[A] = this
51 |
52 | def take(n: Int): LzList[A] =
53 | if (n == 0) this
54 | else throw new RuntimeException(s"Cannot take $n elements from an empty lazy list.")
55 | }
56 |
57 | class LzCons[A](hd: => A, tl: => LzList[A]) extends LzList[A] {
58 | def isEmpty: Boolean = false
59 |
60 | // hint: use call by need
61 | override lazy val head: A = hd
62 | override lazy val tail: LzList[A] = tl
63 |
64 | // utilities
65 | def #::(element: A): LzList[A] =
66 | new LzCons(element, this)
67 |
68 | infix def ++(another: => LzList[A]): LzList[A] =
69 | new LzCons(head, tail ++ another)
70 |
71 | // classics
72 | def foreach(f: A => Unit): Unit = {
73 | def foreachTailrec(lzList: LzList[A]): Unit =
74 | if (lzList.isEmpty) ()
75 | else {
76 | f(lzList.head)
77 | foreachTailrec(lzList.tail)
78 | }
79 |
80 | foreachTailrec(this)
81 | }
82 |
83 | def map[B](f: A => B): LzList[B] =
84 | new LzCons(f(head), tail.map(f))
85 |
86 | def flatMap[B](f: A => LzList[B]): LzList[B] =
87 | f(head) ++ tail.flatMap(f) // preserves lazy evaluation
88 |
89 | def filter(predicate: A => Boolean): LzList[A] =
90 | if (predicate(head)) new LzCons(head, tail.filter(predicate)) // preserves lazy eval
91 | else tail.filter(predicate)
92 |
93 | def take(n: Int): LzList[A] = {
94 | if (n <= 0) LzEmpty()
95 | else if (n == 1) new LzCons(head, LzEmpty())
96 | else new LzCons(head, tail.take(n - 1)) // preserves lazy eval
97 | }
98 | }
99 |
100 | object LzList {
101 | def empty[A]: LzList[A] = LzEmpty()
102 |
103 | def generate[A](start: A)(generator: A => A): LzList[A] =
104 | new LzCons(start, LzList.generate(generator(start))(generator))
105 |
106 | def from[A](list: List[A]): LzList[A] = list.reverse.foldLeft(LzList.empty) { (currentLzList, newElement) =>
107 | new LzCons(newElement, currentLzList)
108 | }
109 |
110 | def apply[A](values: A*) = LzList.from(values.toList)
111 |
112 | /**
113 | Exercises:
114 | 1. Lazy list of Fibonacci numbers
115 | 1,2,3,5,8,13,21,34 ...
116 | 2. Infinite list of prime numbers
117 | - filter with isPrime
118 | - Eratosthenes' sieve
119 | [2,3,4,5,6,7,8,9,10,11,12,13,14,15,...
120 | [2,3,5,7,9,11,13,15,17,...
121 | [2,3,5,7,11,13,17,19,23,25,29,...
122 | [2,3,5,7,11,13,17,19,23,29,...
123 |
124 | sieve([2,3,4,5,6...]) =
125 | 2 #:: sieve([3,4,5,6...].filter(_ % 2 != 0))
126 | 2 #:: sieve([3,5,7,9,...])
127 | 2 #:: 3 #:: sieve([5,7,9,11,...].filter(_ % 3 != 0))
128 | ... ad infinitum.
129 | */
130 | def fibonacci: LzList[BigInt] = {
131 | def fibo(first: BigInt, second: BigInt): LzList[BigInt] =
132 | new LzCons[BigInt](first, fibo(second, first + second))
133 |
134 | fibo(1, 2)
135 | }
136 |
137 | def eratosthenes: LzList[Int] = {
138 | def isPrime(n: Int) = {
139 | def isPrimeTailrec(potentialDivisor: Int): Boolean = {
140 | if (potentialDivisor < 2) true
141 | else if (n % potentialDivisor == 0) false
142 | else isPrimeTailrec(potentialDivisor - 1)
143 | }
144 |
145 | isPrimeTailrec(n / 2)
146 | }
147 |
148 | def sieve(numbers: LzList[Int]): LzList[Int] = {
149 | if (numbers.isEmpty) numbers
150 | else if (!isPrime(numbers.head)) sieve(numbers.tail)
151 | else new LzCons[Int](numbers.head, sieve(numbers.tail.filter(_ % numbers.head != 0)))
152 | }
153 |
154 | val naturalsFrom2 = LzList.generate(2)(_ + 1)
155 | sieve(naturalsFrom2)
156 | }
157 | }
158 |
159 | object LzListPlayground {
160 | def main(args: Array[String]): Unit = {
161 | val naturals = LzList.generate(1)(n => n + 1) // INFINITE list of natural numbers
162 | println(naturals.head) // 1
163 | println(naturals.tail.head) // 2
164 | println(naturals.tail.tail.head) // 3
165 |
166 | val first50k = naturals.take(50000)
167 | val first50kList = first50k.toList
168 |
169 | // classics
170 | println(naturals.map(_ * 2).takeAsList(100))
171 | println(naturals.flatMap(x => LzList(x, x + 1)).takeAsList(100))
172 | println(naturals.filter(_ < 10).takeAsList(9))
173 | // println(naturals.filter(_ < 10).takeAsList(10)) // crash with SO or infinite recursion
174 |
175 | val combinationsLazy: LzList[String] = for {
176 | number <- LzList(1,2,3)
177 | string <- LzList("black", "white")
178 | } yield s"$number-$string"
179 | println(combinationsLazy.toList)
180 |
181 | // fibonacci
182 | val fibos = LzList.fibonacci
183 | println(fibos.takeAsList(100))
184 |
185 | // primes
186 | val primes = LzList.eratosthenes
187 | println(primes.takeAsList(100))
188 | }
189 | }
190 |
--------------------------------------------------------------------------------