├── .gitignore ├── .idea ├── .gitignore ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── misc.xml ├── modules.xml ├── modules │ ├── scala-3-advanced-build.iml │ └── scala-3-advanced.iml ├── sbt.xml ├── scala_compiler.xml ├── scala_settings.xml └── vcs.xml ├── README.md ├── build.sbt ├── project └── build.properties └── src └── main └── scala └── com └── rockthejvm ├── part1as ├── AdvancedPatternMatching.scala ├── DarkSugars.scala └── Recap.scala ├── part2afp ├── CurryingPAFs.scala ├── FunctionalCollections.scala ├── LazyEvaluation.scala ├── Monads.scala └── PartialFunctions.scala ├── part3async ├── Futures.scala ├── JVMConcurrencyIntro.scala ├── JVMConcurrencyProblems.scala ├── JVMThreadCommunication.scala └── ParallelCollections.scala ├── part4context ├── ContextFunctions.scala ├── ExtensionMethods.scala ├── Givens.scala ├── ImplicitConversions.scala ├── Implicits.scala ├── OrganizingCAs.scala └── TypeClasses.scala ├── part5ts ├── AdvancedInheritance.scala ├── FBoundedPolymorphism.scala ├── HigherKindedTypes.scala ├── LiteralUnionIntersectionTypes.scala ├── OpaqueTypes.scala ├── PathDependentTypes.scala ├── SelfTypes.scala ├── StructuralTypes.scala ├── TypeMembers.scala ├── Variance.scala └── VariancePositions.scala ├── playground └── Playground.scala └── practice ├── FunctionalSet.scala ├── JSONSerialization.scala └── LzList.scala /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/scala,sbt,intellij,java 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=scala,sbt,intellij,java 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/artifacts 37 | # .idea/compiler.xml 38 | # .idea/jarRepositories.xml 39 | # .idea/modules.xml 40 | # .idea/*.iml 41 | # .idea/modules 42 | # *.iml 43 | # *.ipr 44 | 45 | # CMake 46 | cmake-build-*/ 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # IntelliJ 55 | out/ 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Cursive Clojure plugin 64 | .idea/replstate.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | 72 | # Editor-based Rest Client 73 | .idea/httpRequests 74 | 75 | # Android studio 3.1+ serialized cache file 76 | .idea/caches/build_file_checksums.ser 77 | 78 | ### Intellij Patch ### 79 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 80 | 81 | # *.iml 82 | # modules.xml 83 | # .idea/misc.xml 84 | # *.ipr 85 | 86 | # Sonarlint plugin 87 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 88 | .idea/**/sonarlint/ 89 | 90 | # SonarQube Plugin 91 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 92 | .idea/**/sonarIssues.xml 93 | 94 | # Markdown Navigator plugin 95 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 96 | .idea/**/markdown-navigator.xml 97 | .idea/**/markdown-navigator-enh.xml 98 | .idea/**/markdown-navigator/ 99 | 100 | # Cache file creation bug 101 | # See https://youtrack.jetbrains.com/issue/JBR-2257 102 | .idea/$CACHE_FILE$ 103 | 104 | # CodeStream plugin 105 | # https://plugins.jetbrains.com/plugin/12206-codestream 106 | .idea/codestream.xml 107 | 108 | ### Java ### 109 | # Compiled class file 110 | *.class 111 | 112 | # Log file 113 | *.log 114 | 115 | # BlueJ files 116 | *.ctxt 117 | 118 | # Mobile Tools for Java (J2ME) 119 | .mtj.tmp/ 120 | 121 | # Package Files # 122 | *.jar 123 | *.war 124 | *.nar 125 | *.ear 126 | *.zip 127 | *.tar.gz 128 | *.rar 129 | 130 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 131 | hs_err_pid* 132 | 133 | ### SBT ### 134 | # Simple Build Tool 135 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 136 | 137 | dist/* 138 | target/ 139 | lib_managed/ 140 | src_managed/ 141 | project/boot/ 142 | project/plugins/project/ 143 | .history 144 | .cache 145 | .lib/ 146 | .bsp 147 | 148 | ### Scala ### 149 | 150 | # End of https://www.toptal.com/developers/gitignore/api/scala,sbt,intellij,java 151 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | scala-3-advanced -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules/scala-3-advanced-build.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 116 | -------------------------------------------------------------------------------- /.idea/modules/scala-3-advanced.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/sbt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/scala_compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/scala_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | --------------------------------------------------------------------------------