Scala is Love

Камен Коцев

HackConf 2017

Какво прави един език за програмиране хубав?

Има ли най-хубав език за програмиране?

Програмисти обсъждащи езици за програмиране

Езици за програмиране

характеристика

  1. Трябва да могат да се изпълнят
    • процесора да може да ги изпълни
  2. Трябва да са удобни
    • програмистът да може да изрази всичко което иска - с (малко) код
  3. Да "скалират" добре (pun intended)
  4. Да помагат на програмиста
    • да допуска по-малко грешки при писане

1. Трябва да могат да се изпълнят

от процесора

Покрива се от всеки език за програмиране с който бихме работили

2. Трябва да са удобни

за програмиста

Най-често решение:

Процедурно програмиране

Процедурно програмиране

дефиниция

  • Описва инструкции
  • Изпълняват се в определен ред
  • Всяка инструкция променя state-а
  • Изпълнението води до решение на проблема/задачата

Процедурно програмиране

пример

int a = 5;
int b = 7;
int c = 3;
int d = 0;

d = a + b + c;

Процедурно програмиране

пример 2

// Fraction A
int numeratorA = 3
int denominatorA = 4
// Fraction B
int numeratorB = 2
int denominatorB = 5

// Fraction C = A + B  =>  3/4 + 2/5 = 23/20
int numeratorC =
    numeratorA * denominatorB +
    numeratorB * denominatorA

int denominatorC =
    denominatorA * denominatorB

А ако бяха много дроби?

Проблем:

работата с много на брой примитивни типове е трудна и уязвима на грешки

Решение:

ООП

ООП

характеристика

  • Обединява известни типове данни за да построи нови типове данни
  • Помага работата на програмиста
  • Допускат се значително по-малко грешки при писане
  • Повтаря се значително по-малко код

ООП

пример

class Fraction {
    private int num;
    private int den;
    Fraction(int num, int den) {
        this.num = num; this.den = den;
    }

    public Fraction add(Fraction other) {
        return new Fraction(
            this.num * other.den + this.den * other.num,
            this.den * other.den
        );
    }
}
// ...
// C = A + B
Fraction C = A.add(B)

Процедурно + ООП  ==  <3

Езици за програмиране

характеристика

  1. Трябва да могат да се изпълнят
    • процесора да може да ги изпълни
  2. Трябва да са удобни
    • програмистът да може да изрази всичко което иска - с (малко) код
  3. Да "скалират" добре
  4. Да помагат на програмиста
    • да допуска по-малко грешки при писане

3. Да "скалират" добре

  • Да могат да изпълняват кода бързо
  • Да има начин скоростта на изпълнение да се подобри без да се променя кода

Процесори Intel 1970 - 2010

Не можем да получим повече скорост от един процесор

Ще работим с много процесори!

Асинхронно = Acync

=>

Процедурно/ООП + Async

проблеми

  • shared memory
  • race conditions
  • deadlocks
  • others

Процедурно програмиране

дефиниция (пак)

  • Описва инструкции
  • Изпълняват се в определен ред
  • Всяка инструкция променя state-а
  • Изпълнението води до решение на проблема/задачата

Процедурно/ООП + Async

има ли решения?

  • Locking memory
  • Semaphors
  • Mutexes
  • ... others

Проблем при променянето на стойности?

=>

Няма да променяме стойности!

Immutable

дефиниция

  • стойност, която не може да бъде променяна
  • всяка промяна на стойността води до създаване на копие на стойността с промяната вътре

Има си своята цена, но е решение!

Езици за програмиране

характеристика

  1. Трябва да могат да се изпълнят
    • процесора да може да ги изпълни
  2. Трябва да са удобни
    • програмистът да може да изрази всичко което иска - с (малко) код
  3. Да "скалират" добре
  4. Да помагат на програмиста
    • да допуска по-малко грешки при писане

4. Да помагат на програмиста

да допуска по-малко грешки при писане

Проблемите най-често идват от писането със страничен ефект (Side effect)

Страничен ефект

дефиниция

Страничен ефект

решение

Pure functions - функции без страничен ефект

 

same input => same output

Pure functions

Можем ли да пишем само с pure functions?

Да, но рядко ще ни е полезно

Можем обаче да изнесем страничния ефект извън бизнес логиката

Pure functions

бонус

Тестват се много лесно!

Same input = Same output

Езици за програмиране

характеристика

  1. Трябва да могат да се изпълнят
    • процесора да може да ги изпълни
  2. Трябва да са удобни
    • програмистът да може да изрази всичко което иска - с (малко) код
  3. Да "скалират" добре
  4. Да помагат на програмиста
    • да допуска по-малко грешки при писане

Какво получаваме?

Easy to write

+

Immutable data

+

Pure functions

Какво получаваме?

Easy to

write

immutable

data

Pure

functions

Recursion

Higher order

functions

First-class

functions

Functional programming

Функционално програмиране

  • Immutable data
  • Pure functions
  • Higher order functions
  • Recursion
  • First class functions

Къде е Scala тук?

  1. Scala не е по-добра Java!
  2. Scala не е Haskell on JVM!

Scala е мост между процедурните/ооп парадигми и функционалните парадигми

Дава ни инструментите, които са ни нужни, за да пишем на стила, който ни допада/трябва

Защо обичам да пиша на Scala?

  • Кратък, лесен за четене код
  • Функционален език
  • Лесен синтаксис
  • Свобода в избора на стил
  • Високо И ниско ниво

Да видим малко код!

public class User {
    private Integer id;
    private String name;

    User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User(" + this.id + ", " + this.name + ")";
    }
}

Защо обичам да пиша на Scala

public class User {
    private Integer id;
    private String name;

    User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "User(" + this.id + ", " + this.name + ")";
    }
}

Защо обичам да пиша на Scala

public class User {
    public Integer id;
    public String name;

    User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User(" + this.id + ", " + this.name + ")";
    }
}

Защо обичам да пиша на Scala

public class User(Integer id, String name) {
    public Integer id;
    public String name;

    @Override
    public String toString() {
        return "User(" + this.id + ", " + this.name + ")";
    }
}

Защо обичам да пиша на Scala

1. Scala compiler infers types when they are obvious

String a = "Hello!"
a = "Hello!"
val a = "Hello!"
or
val a: String = "Hello!"
public class User(Integer id, String name) {
    public Integer id;
    public String name;

    @Override
    public String toString() {
        return "User(" + this.id + ", " + this.name + ")";
    }
}

Защо обичам да пиша на Scala

2. toString метода зависи само от конструктора и при immutable обекти е ясен

val j = User(1, "John")
print(j) // User(1, John)
val k = User(2, "Kate")
print(k) // User(2, Kate)
public class User(Integer id, String name) {
    public Integer id;
    public String name;

    @Override
    public String toString() {
        return "User(" + this.id + ", " + this.name + ")";
    }
}

Защо обичам да пиша на Scala

3. Ако указваме типа и имената на атрибутите при извикването на конструктора, и обекта ни ще е immutable -> няма смисъл да ги пишем втори път

public class User(Integer id, String name) {
    public Integer id;
    public String name;

    @Override
    public String toString() {
        return "User(" + this.id + ", " + this.name + ")";
    }
}

Защо обичам да пиша на Scala

4. Ако дефинираме immutable обект можем да приемем че ще се използва във всички пакети освен ако не сме казали нещо друго (Ще е public по default)

case class User(id: Int, name: String)

Защо обичам да пиша на Scala

Immutable обект, с който може да се прави pattern matching

Защо обичам да пиша на Scala

public boolean exists(Integer el, List<Integer> list) {
    for(int i=0; i<list.size(); i++) {
        if(list.get(i).equals(el)) {
            return true;
        }
    }
    return false;
}

Защо обичам да пиша на Scala

def exists(x: Int, l: List[Int]): Boolean = {
  if(l.isEmpty)
    false
  else if(l.head == x)
    true
  else
    exists(x, l.tail)
}

Защо обичам да пиша на Scala

def exists(x: Int, l: List[Int]): Boolean = {
  l match {
    case Nil => false
    case head :: tail =>
      if(head == x) true
      else exists(x, tail)
  }
}

Защо обичам да пиша на Scala

def exists(x: Int, l: List[Int]): Boolean = l match {
  case Nil => false
  case head :: tail if head == x => true
  case _ => exists(x, l.tail)
}

Защо обичам да пиша на Scala

def exists(x: Int, l: List[Int]): Boolean = l match {
  case Nil => false
  case `x` :: _ => true
  case _ => exists(x, l.tail)
}

implicits в Scala

демо type conversion

case class Name(value: String)
case class User(id: Int, name: Name)

//...

val u = User(1, Name("John"))

implicits в Scala

демо type conversion

case class Name(value: String)
case class User(id: Int, name: Name)

//...

implicit def stringToName(s: String): Name = Name(s)

val u = User(1, "John") // Ok

implicits в Scala

демо type conversion

implicit def booleanToInt(b: Boolean): Int =
    if(b) 1 else 0

// ... 

def addOne(x: Int): Int = x + 1

// ...

addOne(5) // = 6
addOne(true) // = 2

implicits в Scala

демо

implicit class StringHelpers(x: String) {
  def emphasize: String = s"!!!${x.toUpperCase}!!!"
}

val s = "John"
print(s.emphasize) // !!!JOHN!!!

implicits в Scala

демо

case class User(id: Int, name: String)

val input =
  """ { "id": 5, "name": "Jill"} """

JSON.parse(input).as[User]

Bare with me

implicits в Scala

trait JSValue {
  def as[T](implicit r: JSReads[T]) = r.read(this)
}

case class JSObject(value: Seq[(String, JSValue)]) extends JSValue
case class JSNumber(value: BigDecimal) extends JSValue
case class JSString(value: String) extends JSValue
case class JSBoolean(value: Boolean) extends JSValue
case class JSArray(value: Seq[JSValue]) extends JSValue
case object JSNull extends JSValue

implicits в Scala

trait JSReads[A] {
  def read(js: JSValue): JSResult[A]
}

object JSReads {
  def simpleReads[A](f: PartialFunction[JSValue, A]): JSReads[A] = JSReads[A] {
    f.lift.andThen {
      case Some(x) => JSSuccess(x)
      case None => JSError("Could not parse")
    }
  }

  implicit val intReads: JSReads[Int] =
    simpleReads{ case JSNumber(x) => x.toInt }
  implicit val stringReads: JSReads[String] =
    simpleReads{ case JSString(x) => x }
  implicit val booleanReads: JSReads[Boolean] =
    simpleReads{ case JSBoolean(x) => x }
  implicit val doubleReads: JSReads[Double] =
    simpleReads{ case JSNumber(x) => x.toDouble }
}

implicits в Scala

object JSON {
  def read[T](value: JSValue)(implicit r: JSReads[T]): JSResult[T] =
    r.read(value)

  def parse(json: String): JSValue = {
    json.trim match {
      case x if x.startsWith("[") && x.endsWith("]") =>
        val children = recogniseJson(x.tail.init)
        val res = children.map{case (k,v) => parse(v)}

        JSArray(res)
      case x if x.startsWith("{") && x.endsWith("}") =>
        val children = recogniseJson(x.tail.init)
        val res = children.map{case (k,v) => k -> parse(v)}
        JSObject(res)
      case "null" => JSNull
      case x if x.startsWith("\"") && x.endsWith("\"") => JSString(x.tail.init)
      case x if x.forall(x => x.isDigit || x == '.') => JSNumber(BigDecimal(x))
      case x if x == "true" || x == "false" => JSBoolean(x.toBoolean)
      case x => throw new Exception("WTF is " + x)
    }
  }

   // ...
}

implicits в Scala

object JSON {
  def read[T](value: JSValue)(implicit r: JSReads[T]): JSResult[T] =
    r.read(value)

  def parse(json: String): JSValue = {
    // ... 
  }
}

implicits в Scala

case class User(id: Int, name: String)

object User {
  implicit val read: JSReads[User] = {
    case JSObject(Seq(("id",JSNumber(id)), ("name", JSString(name)))) =>
      JSSuccess(User(id.toInt, name))

    case _ => JSError("Cannot parse to user")
  }
}

implicits в Scala

демо

case class User(id: Int, name: String)

val input =
  """ { "id": 5, "name": "Jill"} """

JSON.parse(input).as[User]

Защо още обичам да пиша на Scala

  • High-order functions
  • Implicits
  • Extractors
  • Type classes
  • Future/Option/Either/Try
  • ...

Благодаря за вниманието

Камен Коцев

HackConf 2017

Въпроси?

Добра литература:

Scala is Love

By Kamen Kotsev

Scala is Love

  • 711