Skip to content

makiftutuncu/kodluyoruz-scala

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kodluyoruz Scala

Mehmet Akif TĂĽtĂĽncĂĽ

1. Functional Programming

2. Scala Basics

2.1. Introduction

  • Statically typed with a powerful type system
  • Multiparadigm, best of both worlds, OOP + FP
  • Runs on JVM, Java interop is possible
  • Modern, concise, cool

2.2. Hello World

object Application {
  def main(args: Array[String]): Unit = {
    println("Hello world!")
  }
}

2.3. Values and Types

// A variable of type `String`
var variableMessage: String = "test"

// Can be reassigned
variableMessage = "test 2"

// No initial value (e.g. null), _ is used as a placeholder on definitions
var variable2: String = _

// A value (final variable)
val message: String = "hello"

// Cannot do this because val cannot be reassigned
message = "test"

// Cannot do because a value it won't be assigned in the future, needs to be assigned now
val value: String = _

// Type can be inferred, (mostly) no need to provide it explicitly in the definition

val number    = 42         // Type is `Int`
val isEnabled = false      // Type is `Boolean`
val timestamp = 123456789L // Type is `Long`
val separator = '-'        // Type is `Char`
val average   = 123.45     // Type is `Double`

// A method taking 3 parameters and returning `String`
// `z` has a default value (like overloading)
def explain(x: String, y: Int, z: Boolean = false): String = {
  // String interpolation
  s"X = $x, Y = $y, Z = $z"
}

println(explain("hello", 5, true))

// Named arguments, notice `z` is not provided so default value will be used
println(explain(x = "hello", 5))

// Since arguments can be named, order may change
println(explain(y = 42, z = true, x = "test"))

// Method return types can also be inferred
// If method body is a single expression, {} are not needed
def add(a: Int, b: Int) = a + b

// Methods can have multiple parameter groups
def someWeirdMethod(x: String, y: Int)(z: Boolean): Char = ???

// They can also have 0 parameters
def hello = "hello"

Types

2.4. Conditions and Loops

val score = 42

// if/else if/else block as an expression (produces value)
val state =
  if (score < 33) {
    "Low"
  } else if (score < 66) {
    "Average"
  } else {
    "High"
  }

// match expression (similar but more powerful than switch, more on that later)
val message =
  state match {
    case "Low"     => "Try again"
    case "Average" => "OK"
    case _         => "Congratulations" // default branch
  }

// while loop with a variable that changes loop condition
var i = 1
while (i <= 10) {
  println(i)
  i += 1
}

// Iterating over [1, 5] range (index based for loop)
for (i <- 1 to 5) println(i)

// Iterating over [6, 11) range
for (j <- 6 until 11) {
  println(j)
}

// Multi dimentional, stepped and conditioned for
for {
  i <- 1 to 20 by 2           // i = 1, 3, 5, ...
  j <- i to (i * 2) if j < 14 // j won't be >= 14, loop will terminate
} {
  print("[" + i + ", " + j + "] ")
}

// for also can be an expression via `yield`, otherwise for returns `Unit`
val doubles = (for (i <- 1 to 3) yield i * 2).toList // List(2, 4, 6)

2.5. Standard Library and Data Structures

2.5.1. Functions

// `Int => Int` is the type of the function, sugar for `Function1[Int, Int]`
// `number` is inferred to be of `Int` in the lambda
// `double` is called a function literal
val double: Int => Int = number => number * 2

// Name can be omitted in simole cases
val half: Int => Int = _ / 2

// `(Int, Int) => String` is inferred
val describe = { (number1: Int, number2: Int) =>
  val doubled = double(number1) // invocation is like method call
  val halved  = half(number2)
  
  // Triple quoted String literal
  // Great for multiline, great for using quotations without escaping
  // Can define margin and strip it later for easier formatting
  s"""
     |For "$number1":
     |  Double: $doubled
     |  Half  : $halved
     |""".stripMargin
}

for (i <- 1 to 5) {
  println(describe(i, i))
}

2.5.2. Array, List, Set, Map

// Array construction, `Array[Int]` is inferred
val numbers = Array(1, 2, 3)

// Access an index
println(numbers(1))

// Update an index
numbers(0) = 2
numbers.update(2, 5)

// Classic foreach-style loop
for (i <- numbers) {
  println(i)
}

// Functional way of side-effecting loop
// Here, `foreach` takes a `Int => Unit`
// Using the value, not producing result, hence the side effect
numbers.foreach { i =>
  println(i)
}
// List construction, `List[Int]` is inferred
val scores = List(37, 83)

// `head :: tail` style list construction, ends with `Nil` (empty List)
val names = "Mehmet" :: "Akif" :: Nil

// Prepending via `+:` and getting a new list, List is immutable
val moreNames = "Ali" +: names

// Appending via `:+`, addition happens where `+` is
val evenMoreNames = moreNames :+ "Veli"

// Apply some operations, like a pipeline and get new values
val someUpperCaseNames = evenMoreNames.filter(_.length > 3).map(_.toUpperCase)

// Access via application (`apply` method)
println(someUpperCaseNames(0))

// Pattern matching against a List
someUpperCaseNames match {
  case "MEHMET" :: nextName :: _ =>
    // Matching first element to a literal
    // Matching second to a named value, `nextName` is inferred as `String`
    // Ignoring tail via `_`
    println(s"First name was MEHMET and the next is $nextName")
  
  case _ =>
    println("Pattern did not match")
}

// Folding starts with a value, uses that value and current item while iterating over
// For a `List[A]`, signature is `foldLeft(b: B)(f: A => B): B`
val string =
  (1 to 5).toList
          // List[Int]
          .zipWithIndex
          // List[(Int, Int)]
          .map { case (number, index) => s"$index: $number" }
          // List[String]
          .foldLeft("") { case (result, value) => result + s"$value, " }
          // String
// Set creation, `Set[String]` is inferred
val initialWords = Set("hello", "world")

val newWords = "Damn! World failed."
  .replaceAll("[^\\w]", " ") // Replace anything not a letter with space
  .split(" ")                // Split from spaces into an Array
  .filterNot(_.isEmpty)      // Remove empty Strings
  .map(_.toLowerCase)        // Lowercase words
  .toSet                     // Convert the Array to a Set

// Set union, won't add "world" again
val allWords = initialWords ++ newWords

if (allWords.contains("damn")) {
  println("Watch your language!")
}
// Map creation, `Map[Char, Int]` is inferred
val romanNumerals = Map(
  'I' -> 1, // Syntax sugar for `Tuple2`
  'V' -> 5,
  'X' -> 10,
  'L' -> 50,
  'C' -> 100,
  'D' -> 500,
  'M' -> 1000
)

def convert(romanNumeralType: Char): Int =
  if (!romanNumerals.contains(romanNumeralType)) {
    -1
  } else {
    // Access directly by key (via `apply`)
    // Use with caution. If key doesn't exist, it will throw an exception.
    romanNumerals(romanNumeralType)
  }

println(s"X: ${convert('X')}")
println(s"A: ${convert('A')}")

2.5.3. Option

// `Option[Int]` is inferred as return type
def divide(a: Int, b: Int) = if (b == 0) None else Some(a / b)

val division1 = divide(4, 2) // Some(2)
val division2 = divide(5, 0) // None

// `Option.apply` is smart against nulls, `Some` is not smart (`Some(null)` is possible)
val maybeUserName = Option(SomeJavaClass.getNullableUserName)

// Can be pattern matched
division2 match {
  case Some(result) => println(result)
  case None         => println("Cannot divide by 0")
}

// For comprehension, will print and be `Some(15)`
val calculation1 =
  for {
    res1 <- divide(9, 1)
    res2 <- divide(8, 2)
    res3 <- divide(7, 3)
  } yield {
    println("Yielding result")
    res1 + res2 + res3
  }

// Won't print and be `None`
val calculation2 =
  for {
    res1 <- divide(6, 4)
    res2 <- divide(5, 0)
  } yield {
    println("Results: $res1, $res2")
    res1 + res2
  }

// This is what happens behind the scenes of `calculation1`
val calculation3 =
  divide(9, 1).flatMap { res1 =>    // Values are flatmapped
    divide(8, 2).flatMap { res2 =>
      divide(7, 3).map { res3 =>    // Last value is mapped
        println("Yielding result")
        res1 + res2 + res3
      }
    }
  }

val result1 = calculation1.get // 15
val result2 = calculation2.get // NoSuchElementException: None.get

val result3 = calculation2.getOrElse(0) // 0

val message =
  calculation2.orElse(calculation1)     // Provide alternative `Option[Int]`
              .filter(_ > 10)           // Will be `None` if predicate fails against value
              .fold("No result") { i => // `def fold(default: B)(use: A => B): B`
    s"Result = $i"
}

2.5.4. Either

val errorOrValue1: Either[String, Double] = Left("no value")
val errorOrValue2: Either[Exception, Int] = Right(42)

val error1 = errorOrValue1.swap.toOption // Some("no value") as `Option[String]`
val error2 = errorOrValue2.left.toOption // None             as `Option[Exception]`

// `Either` is right-biased, if Left, default value will be returned
val value1 = errorOrValue1.getOrElse(0) // 0 as `Double`
val value2 = errorOrValue2.getOrElse(0) // 42 as `Int`

// Can be pattern matched
errorOrValue1 match {
  case Left(error)  => println(s"Error: $error")
  case Right(value) => println(s"Value: $value")
}

// Need to indicate types of both sides for type inference to infer correct types
// Otherwise `Right(3)` would be inferred as `Right[Any, Int]`
// Result will be `Right("hello 3")` as `Either[String, String]`
val maybeMessage =
  for {
    number  <- Right[String, Int](3)
    message <- Right[String, String]("hello")
  } yield s"$message $number"

// `Left(false)` as `Either[Boolean, Int]`
val maybeMultiplication =
  for {
    a <- Left[Boolean, Double](false)
    b <- Right[Boolean, Int](5)
  } yield a * b

2.5.5. Try

import scala.util.{Try, Success, Failure} // multiple imports from same package

def firstUnsafe(list: List[Int]): Int = list.head

val first1 = firstUnsafe(List.empty) // NoSuchElementException: head of empty list

val first2 =
  try {
    firstUnsafe(List.empty)
  } catch {
    // catch part is also a pattern matching
    case e: IllegalArgumentException => println(e); -1 // Semicolon if you really need
    case _: NoSuchElementException   => -2
    case e =>
      e.printStackTrace()
      -3
  }

// `Try.apply` is the functional way of wrapping things in try-catch
def first(list: List[Int]): Try[Int] = Try { list.head }

val first3 = first(List.empty)    // Failure(...)
val first4 = first(List(1, 2, 3)) // Success(1)

println(first3.get)           // Will throw the caught exception
println(first3.getOrElse(-1)) // -1

println(first4.get) // 1

// Recover with a value if given partial function matches
// Success(-1)
val first5 = first3.recover {
  case e if e.getMessage.contains("empty") =>
    -1
}

// Recover with another Try if given partial function matches
// Failure(Exception(NoSuchElementException(...)))
val first6 = first3.recoverWith {
  case e: IllegalArgumentException => Success(-1)
  case e                           => Failure(new Exception(e))
}

2.6. Structures

2.6.1. Class

// Primary constructor in the definition, concise syntax
class Player(val name: String, var score: Int, isNoob: Boolean = true) {
  // `override` is a keyword, not an annotation
  override def equals(obj: Any): Boolean =
    obj match {
      // Matches on the type, not the value itself, value is bound to name `that`
      case that: Player =>
        // `==` is `equals` so Strings are also compared using `==`
        this.name == that.name && this.score == that.score
      
      case _ => false
    }
  
  override def hashCode(): Int =
    17 * name.hashCode() * score
  
  def copy(newName: String = name, newScore: Int = score): Player =
    new Player(newName, newScore)
  
  override def toString(): String =
    s"Player($name, $score)"
}

// Instance creation via `new`
val player = new Player("Akif", 0)

// `score` is a field, therefore can be accessed (and be modified since it's var)
player.score += 10

// This won't work here (outside the class)
// `isNoob` is only a constructor parameter, not a field
println(player.isNoob)

println(player.copy(newName = "Mehmet Akif"))
2.6.1.1. Case Class
// Immutable, therefore `val` fields by default
// Generates all the boilerplate for you
case class Player(name: String, score: Int = 0)

// No need for `new` because generated `apply` is called as `Player.apply(...)`
val player = Player("Akif")

// Generated `copy` to get a copied instance with certain things modified
val playerWithHigherScore = player.copy(score = player.score + 10)

// Generated `toString`
println(playerWithHigherScore)

// Also `equals`, `hashCode` and more are generated

2.6.2. Object

// Out-of-the-box singleton
object Utilities {
  val pi = 3.141592653589793
  
  def areaOfCircle(radius: Double): Double = pi * radius * radius
}
2.6.2.1. Companion Object
class Cake(val layers: Int) {
  val description: String = s"$layers layered cake"
  
  def celebrate(): Unit = Cake.celebrateWith(this)
}

// Companion object to `Cake` using same name, otherwise a regular object
object Cake {
  // Acts as a constructor, can be overloaded
  def apply(layers: Int): Cake = new Cake(layers)
  
  def celebrateWith(cake: Cake): Unit =
    println(s"I'm having a ${cake.description} for celebration.")
}

val fruitCake     = new Cake(2) // Regular instance creation
val chocolateCake = Cake(3)     // Short for `Cake.apply(3)`

// Instance method
chocolateCake.celebrate()

// Accessed statically on the object
Cake.celebrateWith(fruitCake)

2.6.3. Abstract Class and Trait

// Can have primary constructor like a regular class
// Can be extended only once via `extends`
abstract class Pide() {
  def eat(): Unit
}

// Interface on steroids, cannot have a constructor (yet)
trait Meaty {
  val meat: String
}

trait Cheesy {
  val cheese: String
}

// First extend via `extends`, then mix-in via `with`
case class CheesyMeatPide(override val meat: String,
                          override val cheese: String) extends Pide() with Meaty with Cheesy {
  val name: String = s"$meat pide with $cheese cheese"
  
  override def eat(): Unit = println(s"Eating $name")
}

object VegetarianPide extends Pide() with Cheesy {
  override val cheese: String = "white"
  
  override def eat(): Unit = println(s"I don't eat meat so I'm eating $cheese cheese pide")
}

CheesyMeatPide("ground beef", "white cheddar").eat()

VegetarianPide.eat()

2.6.4. Enumerations and Algebraic Data Types (ADT)

// Standard Scala enumeration (not the recommended way)
// It will contain an inner `Value` type.
object Color extends Enumeration {
  // This weird thing generates `Value` instances
  // for these values during compilation
  val Red, Green, Blue = Value
}

println(Color.values)            // Color.ValueSet(Red, Green, Blue)
println(Color.Blue.id)           // 2
println(Color.withName("Green")) // Green

object RGB extends Enumeration {
  // You can extend `Value` to add more fields to each item
  case class CustomVal(override val id: Int, code: Char) extends Value

  // We instantiate them ourselves in this case.
  val Red   = CustomVal(0, 'r')
  val Green = CustomVal(1, 'g')
  val Blue  = CustomVal(2, 'b')
  
  override def values: RGB.ValueSet = RGB.ValueSet(Red, Green, Blue)
}

println(RGB.Blue.code) // b

def describe(color: RGB.CustomVal): String =
  color match {
    case RGB.Red  => "red"
    case RGB.Blue => "blue"
  }

// This will fail with `MatchError` in runtime!
println(describe(RGB.Green))
// Sealed means `Color` cannot be extended outside this file.
// In other words, all possible subtypes of this type is known.
// It will also help compiler in exhaustivity checks of pattern matches.
sealed trait Color

object Color {
  // Regular inheritance, need single instances so they are `object`s
  // We want to benefit from generated methods so we also use `case`
  case object Red   extends Color
  case object Green extends Color
  case object Blue  extends Color

  // This is optional but it's a good idea to have it avaliable
  val values: List[Color] = List(Red, Green, Blue)
}

// Custom type for enum items
sealed abstract class Auth(val anonymous: Boolean)

object Auth {
  // Can have multiple instances since it's a regular class
  case class  UserPass(user: String, pass: String) extends Auth(false)
  case class  Token(token: String)                 extends Auth(false)
  case object Guest                                extends Auth(true)
}

def getSecret(credentials: Auth): Either[String, Int] =
  credentials match {
    // Match to a pattern and add conditions if needed
    case Auth.UserPass("admin", pass) if pass != "@dM1n" =>
      Left("Incorrect username/password")

    case Auth.Token("1234abc") =>
      Left("Expired token")

    // Can match against type and give name to value of that type
    case Auth.Guest =>
    	Left("Need authorization")

    // Can destruct and match against fields (and ignore some)
    // Also give a name the matched pattern via `@`
    // Here `a` is of `Auth.UserPass` type, not just `Auth`
    case a @ Auth.UserPass(user, _) =>
      println(s"Accessing secret as $user")
      Right(42)

    // Can match against type and not care about name of the value
    case _: Auth.Token =>
      println(s"Accessing secret as an API with token")
      Right(42)
    
    // If this pattern match did not cover all cases (not exhaustive),
    // compiler will warn because `Auth` is a sealed type.
    // In such a case, add `case _ =>` or handle all possible cases.
  }

// Left(Need authorization)
println(getSecret(Auth.Guest))

// Left(Expired token)
println(getSecret(Auth.Token("1234abc")))

// Right(42)
println(getSecret(Auth.Token("test")))

// Right(Incorrect username/password)
println(getSecret(Auth.UserPass("admin", "admin")))

// Right(42)
println(getSecret(Auth.UserPass("admin", "@dM1n")))

3. More on Scala

3.1. Generics

// `I` is called a type parameter of `Model`
trait Model[I] {
  val id: I
}

abstract class Message[M] {
  val content: M
}

case class TextMessage(override val id: Int,
                       override val content: String) extends Message[String]
                                                        with Model[Int]

case object UnitModel extends Model[Unit] {
  override val id: Unit = ()
}

// `M` has a constraint, `M` must be a subtype of `Model[I]`
// Subtype relation is denoted with `<:`
// There is also its counterpart supertype constaint `>:`
def idOf[M <: Model[Int]](model: M): Int = model.id

val message = TextMessage(1, "Hello")

// Method type parameter is inferred from parameter
// Explicit version: `idOf[TextMessage](message)`
println(idOf(message)) // 1

// Will not work because `UnitModel` is not a `Model[Int]`
idOf(UnitModel)

3.2. Higher Order Functions, Currying, Partially Applied Functions

// Example from: https://docs.scala-lang.org/tour/higher-order-functions.html

// Method returns a function
def urlBuilder(secure: Boolean, domain: String): (String, String) => String = {
  val schema = if (secure) "https://" else "http://"
  
  // Function to be returned
  (path: String, query: String) => s"$schema$domain/$path?$query"
}

// Function literal of type `(String, String) => String`
// (path: String, query: String) => s"https://google.com/$path?$query"
val getUrl = urlBuilder(secure = true, "google.com")

// https://google.com/search?q=scala
val url = getUrl("search", "q=scala")
def convert(int: Int): String = int.toString

// Becomes a function literal of type `Int => String` when argument is not given
// Also called partially applied function
val converter = convert _

// `fold` has 2 parameter groups, it's also called a curried function
// Think of each parameter group as the input of next parameter group
//
// So type of `fold` is
// `(List[A], B) => ((B, A) => B) => B`
//
// When first parameter group is given it becomes
// `((B, A) => B) => B`
//
// When second parameter group is given it becomes
// `B`
def fold[A, B](list: List[A], initial: B)(operation: (B, A) => B): B = {
  var result: B = initial
  for (a <- list) {
    result = operation(result, a)
  }
  result
}

// A function literal
val appender: (String, Int) => String = (s, i) => s + i.toString

// Second parameter group of `fold` is not given
// So it becomes a function literal, requiring the value of first parameter group as an input
val looper: ((String, Int) => String) => String = fold[Int, String]((1 to 5).toList, "")

// Apply the missing parameter (which is another function)
println(looper(appender)) // "12345"

// Notice second parameter group is written with {} as it is a function
// Type parameters of `fold` are both inferred to be `Int` here
//
// fold((1 to 5).toList, 0)((result, int) => result + int)
//
// fold((1 to 5).toList, 0)(_ + _)
println(
  fold((1 to 5).toList, 0) { (result, int) =>
    result + int
  }
) // 15

3.3. More on Collections

val list = (1 to 100 by 2).toList // List(1, 3, 5, ..., 97, 99)

list.head                  // 1
List.empty[Int].head       // throws exception

list.headOption            // Some(1)
List.empty[Int].headOption // None

list.last                  // 99
List.empty[Int].last       // throws exception

list.lastOption            // Some(99)
List.empty[Int].lastOption // None

list.tail            // List(3, 5, ..., 97, 99)
List(1).tail         // List()
List.empty[Int].tail // List()

list.mkString                            // 135...9799
list.mkString("-")                       // 1-3-5...-97-99
list.mkString("[", ", ", "]")            // [1, 3, ..., 97, 99]
List.empty[Int].mkString("[", ", ", "]") // []

list.isEmpty  // false
list.nonEmpty // true

list.contains(3) // true
list.contains(4) // false

list.exists(i => i * 2 == 46)            // true
List.empty[Int].exists(i => i * 2 == 46) // false

list.forall(_ > 0)             // true
list.forall(_ > 10)            // false
List.empty[Int].forall(_ > 10) // true

list.find(_ * 2 == 46) // Some(23)
list.find(_ * 2 == 3)  // None

list.filter(_ > 90) // List(91, 93, 95, 97, 99)

list.filterNot(_ > 10) // List(1, 3, 5, 7, 9)

// On a `List[A]`, `map` takes `A => B` and returns `List[B]`
val listOfList: List[List[Int]] = list.map(i => List(i - 1, i)) // List(List(0, 1), List(2, 3), ...)

listOfList.flatten // List(0, 1, 2, 3, ..., 99)

// On a `List[A]`, `flatMap` takes `A => List[B]` and returns `List[B]`
// flatMap = map + flatten
list.flatMap(i => List(i - 1, i)) // List(0, 1, 2, 3, ..., 99)

// Same as `reduce`
// On a `List[A]`, `reduce` takes `(A, A) => A` and returns `A`
list.reduceLeft((i, j) => i + j)  // 2500
List.empty[Int].reduceLeft(_ + _) // throws an exception

list.reduceLeftOption(_ + _)            // Some(2500)
List.empty[Int].reduceLeftOption(_ + _) // None

// Same as `fold`
list.foldLeft("")((acc, i) => acc + i)            // "1357...9799"
List.empty[Int].foldLeft("")((acc, i) => acc + i) // ""

list.foldRight("")((i, acc) => acc + i)           // "9997...531"
List.empty[Int].foldLeft("")((acc, i) => acc + i) // ""

// Same as `list.filter(_ > 90).map(_ % 10)`
list.collect {
  case i if i > 90 =>
    i % 10
}
// List(1, 3, 5, 7, 9)

// Same as `list.find(_ > 90).map(_ % 10)`
list.collectFirst {
  case i if i > 90 =>
    i % 10
}
// Some(1)

list.collectFirst {
  case i if i > 100 =>
    i % 10
}
// None

list.foreach(i => println(i))

list.drop(3)      // List(7, 9, 11, ..., 97, 99)
list.dropRight(3) // List(1, 3, 5, ..., 91, 93)

list.take(3)      // List(1, 3, 5)
list.takeRight(3) // List(95, 97, 99)

list.takeWhile(_ < 10) // List(1, 3, 5, 7)

// On a `List[Int]`, `partition` takes `Int => Boolean` and returns `(List[Int], List[Int])`
list.partition(_ < 50) // (List(1, 3, ..., 47, 49), List(51, 53, ..., 97, 99))

list.span(_ < 50) // (List(1, 3, 5, 7, 9, 11, ..., 47, 49), List(51, 53, ..., 97, 99))

list.reverse // List(99, 97, ..., 3, 1)

list.slice(3, 5)            // List(7, 9)
List.empty[Int].slice(3 ,5) // List()

3.4. Recursion

To understand recursion, you must first understand recursion.

https://www.google.com/search?q=recursion

def fib(n: Int): Int =
  if (n < 0) {
    // Base case where recursion needs to stop (no recursive call)
    0
  } else if (n == 1) {
    // Base case where recursion needs to stop (no recursive call)
    1
  } else {
    // Recursive case where function calls itself
    // i.e. represents itself with a combination of smaller problems
    fib(n - 1) + fib(n - 2)
  }

/*
fib(4) =
fib(3) + fib(2) =
fib(2) + fib(1) + fib(2) =
fib(1) + fib(0) + fib(2) =
1 + 0 + fib(2) =
1 + fib(2) =
1 + fib(1) + fib(0) =
1 + 1 + 0 =
2
*/
import scala.annotation.tailrec

// Regular recursion
def recSum(list: List[Int]): Int =
  list match {
    case Nil =>
      0

    case head :: tail =>
      // recursive call is not in tail position
      head + recSum(tail)
  }

def tailRecSum(list: List[Int]): Int = {
  // Method inside method
  // Annotated with `@tailrec` so compiler will optimize this into a regular loop
  @tailrec
  def step(l: List[Int], result: Int): Int =
    l match {
      case Nil =>
        result

      case head :: tail =>
        // recursive call is in tail position (only thing is the recursive call)
        step(tail, result + head)
    }

  // Start stepping with entire list and 0 as sum
  step(list, 0)
}

3.5. Implicits

class Logger {
  def log(value: Any): Unit = println(value)
}

val logger = new Logger

def add(a: Int, b: Int, logger: Logger): Int = {
  logger.log("a = " + a)
  logger.log("b = " + b)
  val result = a + b
  logger.log("result = " + result)
  result
}

add(2, 3, logger)
class Logger {
  def log(value: Any): Unit = println(value)
}

// Marking the value as implicit
// so any request to an implicit `Logger` can access this
implicit val logger = new Logger

// Moved `logger` into separate parameter group
// and marked it as `implicit` so it is searched in current scope.
//
// If there is no `Logger` instance marked as implicit in current scope,
// it won't compile.
def add(a: Int, b: Int)(implicit logger: Logger): Int = {
  logger.log("a = " + a)
  logger.log("b = " + b)
  val result = a + b
  logger.log("result = " + result)
  result
}

// Not passing logger explicitly, it is being passed implicitly.
// Since implicit parameter is in a separate parameter group,
// we can call the method as if it doesn't have a second parameter group.
add(2, 3)

add(4, 5)(anotherLogger) // can still be provided explicitly

// `implicitly` method captures implicit instance of given type
implicitly[Logger].log("test")
trait Show[A] {
  def show(a: A): String
}

// new Show[Int] { override def show(a: Int): String = a.toString }
// { a: Int => a.toString }
// { _.toString }
implicit val intShow: Show[Int] = _.toString

def show1[A](a: A)(implicit s: Show[A]): String = s.show(a)

println(show1(42)) // 42

// Will not compile because there is no implicit `Show[String]` in scope
println(show1("42"))

// `A: Show` is called a context bound.
// It means there should be an implicit `Show[A]` in the context (scope) .
def show2[A: Show](a: A): String = implicitly[Show[A]].show(a)

3.6. Laziness

// This will not be evaluated until it is referenced.
lazy val x: String = {
  println("Initializing x")
  "hello"
}

val y: String = "world"

println(y)
// world

println(x)
// Initializing x
// hello

println(x)
// hello
sealed abstract class LogLevel(val id: Int)
object LogLevel {
  case object Debug extends LogLevel(1)
  case object Warn  extends LogLevel(2)
  case object Error extends LogLevel(3)
}

class Logger(val level: LogLevel) {
  // Writing `=> A` instead of `A` makes the parameter lazy.
  // Unless `value` is referenced, it will not be evaluated.
  // Think of it like a function taking no parameters
  // so it doesn't produce the value until it is called.
  //
  // In this case, it will only be evaluated if it is actually going to be logged
  def debug(value: => Any): Unit = if (enabled(LogLevel.Debug)) { println(value) }
  
  def warn(value: => Any): Unit = if (enabled(LogLevel.Warn)) { println(value) }
  
  def error(value: => Any): Unit = if (enabled(LogLevel.Error)) { println(value) }
  
  private def enabled(l: LogLevel): Boolean = l.id >= level.id
}

val errorLogger = new Logger(LogLevel.Error)

def getValue(): String = {
  println("Getting value")
  "test"
}

// Prints nothing because `errorLogger` won't log at debug level
// Therefore argument `getValue()` won't be evaluated (because it is marked as lazy)
errorLogger.debug(getValue())

// Getting value
// test
errorLogger.error(getValue())

3.7 Future and Concurrency

// TODO

3.8. Typeclasses

// TODO

About

Content of Scala Lecture on Kodluyoruz

Topics

Resources

License

Stars

Watchers

Forks

Languages