Skip to content

Instantly share code, notes, and snippets.

@ssledz
Last active April 29, 2020 10:50
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save ssledz/c1c1ae9b9401d8c2455ae01d9045b2d5 to your computer and use it in GitHub Desktop.
Save ssledz/c1c1ae9b9401d8c2455ae01d9045b2d5 to your computer and use it in GitHub Desktop.

Useful resources

Evaluation Rules

  • Call by value - evaluates the function arguments before calling the function
  • Call by name - evaluates the function first, and then evaluates the arguments if need be
def square(x: Double)    // call by value
def square(x: => Double) // call by name

Currying

Converting a function with multiple arguments into a function with a single argument that returns another function.

def f(a: Int, b: Int): Int = a + b // uncurried version (type is (Int, Int) => Int)
def f(a: Int)(b: Int): Int = a + b // curried version (type is Int => Int => Int)
def inc = f(1)_                    // (type is Int => Int)

Operators

The precedence of an operator is determined by its first character, with the following in decreasing order of precedence:

(all other special characters)
*/ %
+-
:
=!
<>
&
ˆ
|
(all letters)
(all assignment operators)
((a + b) ^? (c ?^ d)) less ((a ==> b) | c)

The associativity of an operator is determined by its last character

  • Right-associative if ending with :
  • Left-associative otherwise
1 + 2         // Scala invokes (1).+(2)
1 :: 2 :: Nil // Scala invokes (Nil).::(2).::(1)

Notation

  • infix
  • prefix (olny with + , - , ! , and ~)
  • postfix
1 + 2    // infix
-1       // prefix - Scala invokes (1).unary_-
1 toLong // postfix

Polymorphism

Principal forms of polymorphism:

  • subtyping
  • generics

Type Parameters

  • upper bound - S <: T means: S is a subtype of T (S narrows T, T is an upper bound of S)
  • lower bound - S >: T means: S is a supertype of T (S widens T, T is a lower bound of S)
  • mixed bound - S >: T <: U means: S is a supertype of T and a subtype of U (S widens T and narrows U)
class Foo[A,B] {
  def fun1[C <: A](arg: C): C = arg
  def fun2[C >: B](arg: C): C = arg
  def fun3[C >: B <: A](arg: C): C = arg
}
val foo = new Foo[Seq[Int], List[Int]]
foo.fun1(1 to 5)                                // Range is a subtype of Seq
foo.fun2(Iterable(1, 2, 3))                     // Iterable is a supertype of List
foo.fun3(List(1,2,3))                           // List is a subtype od Seq and a supertype of List

// foo.fun3(Iterable(1, 2, 3))                  // Won't compile because Iterable is not a subtype 
                                                // of Seq (Seq is a subtype of Iterable)

Variance

Given A <: B

  • If C[A] <: C[B], C is covariant
  • If C[A] >: C[B], C is contravariant
  • Otherwise C is nonvariant
class C[+A] { ... } // C is covariant
class C[-A] { ... } // C is contravariant
class C[A]  { ... } // C is nonvariant

For a function, if A2 <: A1 and B1 <: B2, then A1 => B1 <: A2 => B2

def fun1(seq : Seq[String]) : List[Int] = seq map (_.toInt) toList
def fun2(list : List[String]) : Seq[Int] = list map (_.toInt)         // fun1 <: fun2
var fun = fun2 _
var res : Seq[Int] = fun(List("1", "2"))
fun = fun1 _       // because List[String] <: Seq[String] and List[Int] <: Seq[Int] => fun1 <: fun2
res = fun(List("1", "2"))

Functions must be contravariant in their argument types and covariant in their result types, e.g.

trait Function1[-T, +U] {
  def apply(x: T): U
} // Variance check is OK because T is contravariant and U is covariant

Pattern Matching

Pattern matching is used for decomposing data structures

selector match { 
  case pattern1 => expression1
  ...
  case patternM => expressionM
  case _ => expressionN
}

Examlpe of pattern guard

expr match {
  case x : Int if x >= 0 => print(s"$x is positive or zero")
  case x : Int if x < 0 => print(s"$x is negative")
}

Wildcard patterns

The wildcard pattern _ matches any object

expr match {
  case (_, _) => print(s"$expr is a pair")
  case _ =>
}

Constant patterns

A constant pattern matches only itself

def describe(x: Any) = x match {
  case 5 => "five"
  case true => "truth"
  case "hello" => "hi!"
  case Nil => "the empty list"
  case _ => "something else"
}

describe(5)        // yields "five"
describe("5")      // yields "something else"
describe("hello")  // yields "hi!"
describe(Nil)      // yields "the empty list"

Variable patterns

A variable pattern matches any object, just like a wildcard. Moreover Scala binds the variable to whatever the object is.

expr match {
  case 0 => "zero"
  case somethingElse => s"not zero: $somethingElse"
}

Variable or constant?

  • Variable pattern - name starting with a lowercase letter
  • Constant pattern - all other
import math.Pi
expr match {
  case 0 => "zero"
  case Pi => "Pi"
  case somethingElse => s"not zero an pi: $somethingElse"
}

Constructor patterns

case class Email(name : String, domain : String)

expr match {
  case Email(name, domain) => println(s"$name@$domain")
  case _ => 
}
List(1, 2, 3) match {
  case Nil => ...
  case x :: Nil => ... // or case List(x) => ... 
  case x :: xs => ...
}

:: implemented using case class scala.collection.immutable.::

assert(scala.collection.immutable.::(1, Nil) == 1 :: Nil)

Extractor patterns

object EMail {
  // The injection method (optional)
  def apply(user: String, domain: String) = user +"@"+ domain
  
  // The extraction method (mandatory)
  def unapply(str: String): Option[(String, String)] = {
    val parts = str split "@"
    if (parts.length == 2) Some(parts(0), parts(1)) else None
  }
}

and

val EMail(name, domain) = "slawomir.sledz@gmail.com" // name = slawomir.sledz, doamin = gmail.com

Only with extraction method

object UpperCase {
  def unapply(s: String): Boolean = s.toUpperCase == s
}

expr match { 
  case a @ UpperCase() => print("UpperCase") 
  case _ => print("Not UpperCase")
}

Variable argument extractors

object Domain {

  // The injection method (optional)
  def apply(parts: String*): String = parts.reverse.mkString(".")
  
  // The extraction method (mandatory)
  def unapplySeq(whole: String): Option[Seq[String]] = Some(whole.split("\\.").reverse)
}
val Domain("pl", c, _*) = "a.b.c.pl"     // c = c
val Domain("pl", c, b, _*) = "a.b.c.pl"  // b = b, c = c

Sequence patterns

expr match {
  case List(0, _, _) => println("found it")
  case _ =>
}

Match against a sequence without specifying how long it can be (_* - matches any number of elements including zero elements)

expr match {
  case List(0, _*) => println("found it")
  case _ =>
}

Tuple patterns

expr match {
  case (a, b, c) => println("matched "+ a + b + c)
  case _ =>
}

Typed patterns

Convenient replacement for type tests and type casts.

x match {
  case s: String => s.length
  case m: Map[_, _] => m.size
  case _ => -1

Variable binding

Via the @ sign. Set the variable e to the matched object.

abstract class Expr
case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr

val expr = UnOp("-", UnOp("-", Number(1)))
val expr2 = expr match {
  case UnOp("-", e @ UnOp("-", _)) => e
  case _ =>
}

assert(expr2 == UnOp("-", Number(1)))

Monads

A monad is a parametric type M[T] with two operations:

  • flatMap
  • unit
trait M[T] {
  def flatMap[U](f: T => M[U]) : M[U]
  def unit[T](x: T) : M[T]
}

These operations must satisfy three important properties:

  • Associativity: (x flatMap f) flatMap g == x flatMap (y => f(y) flatMap g)
  • Left unit: unit(x) flatMap f == f(x)
  • Right unit: m flatMap unit == m

Random Generators

trait Generator[+T] { self =>
  def generate: T
  def map[S](f: T => S) : Generator[S] = new Generator[S] {
    def generate = f(self.generate)
  }
  def flatMap[S](f: T => Generator[S]) : Generator[S] = new Generator[S] {
    def generate = f(self.generate).generate
  }
}

Basic integer random generator

val integers = new Generator[Int] {
  val rand = new java.util.Random
  def generate = rand.nextInt()
}

Above definition can be mapped to the other domains to get booleans, pairs intervals using for-expression magic

val booleans = for {x <- integers} yield x > 0
val pairs = for {x <- integers; y<- integers} yield (x, y)
def interval(lo: Int, hi: Int) : Generator[Int] = for { X <- integers } yield lo + x % (hi - lo)

Partial Functions

trait PartialFunction[-A, +R] extends Function1[-A, +R] {
  def apply(x: A): R
  def isDefinedAt(x: A): Boolean
}
val pf: PartialFunction[Int, String] = {
  case 1 => "one"
  case 2 => "two"
  // no case for other numbers => pf is undefined for other than 1 and 2
}

print(pf.isDefinedAt(1))  // true
print(pf(1))              // one
print(pf.isDefinedAt(3))  // false

Implicit Conversions and Parameters

Cases:

  • conversions to an expected type
  • conversions of the receiverof a selection
  • implicit parameters

Rules for implicits

  • Marking Rule: Only definitions marked implicit are available
  • Scope Rule: An inserted implicit conversion must be in scope
  • companion object of the source or target of the conversion
  • import it as a single identifier
  • One-at-a-time Rule: Only one implicit is tried
  • Explicits-First Rule: Whenever code type checks as it is written, no implicits are attempted

One implicit conversion is more specific than another if

  • The argument type of the first one is a subtype of the second.
  • Both conversions are methods, and the enclosing class of the first one extends the enclosing class of the second

Implicit conversion to an expected type

Conversion from functions to action listeners

implicit def function2ActionListener(f: ActionEvent => Unit) = new ActionListener {
  def actionPerformed(event: ActionEvent) = f(event)
}

And thanks to the above, below compiles (if conversion is in scope)

button.addActionListener( (_: ActionEvent) => println("pressed!") )

Converting the receiver

Let's say we have

class Rational(n: Int, d: Int) {
...
  def + (that: Rational): Rational = ...
  def + (that: Int): Rational = ...
}
val oneHalf = new Rational(1, 2)    // 1/2
oneHalf + oneHalf                   // 1/1
oneHalf + 1                         // 1

and we want also to support

1 + oneHalf 

to do this add below to the scope

implicit def intToRational(x: Int) = new Rational(x, 1)

Implicit parameters

def maxList[T](elements: List[T])(implicit converter: T => Ordered[T]): T =
    elements match {
      case List() =>
        throw new IllegalArgumentException("empty list!")
      case List(x) => x
      case x :: rest =>
        val maxRest = maxList(rest) // (orderer) is implicit
        if (x > maxRest) x // orderer(x) is implicit
        else maxRest
    }

Above method header can be rewritten to use view bound

def maxList[T < % Ordered[T]](elements: List[T]): T = ...

T < % Ordered[T] - I can use any T , so long as T can be treated as an Ordered[T].

For-Comprehensions

Example

for (x <- 1 to M; y <- 1 to N) yield (x,y)

is equivalent to

(1 to M) flatMap (x => (1 to N) map (y => (x, y)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment