Intro a Programación Funcional en Scala

Cristian Spinetta | Gonzalo Guglielmo

Agenda

  • Qué es Scala?
  • Sintaxis: de Java a Scala
  • Inmutabilidad en Scala
  • Programación funcional: Qué es?
  • Estructuras funcionales
  • Codear con Side Effects

Qué es Scala?

  • Corre sobre la JVM
  • Java++
  • Funcional + OO
  • Tipado estático + inferencia de tipos
  • Conciso/expresivo
  • REPL

Sintaxis: De Java a Scala

Sintaxis

Ejemplo en SCALA

Operaciones elementales

val radius: Double = 2.3;

var color: String = "red";

def area(radius: Double): Double = {
  val pi: Double = 3.141592;
  val sqRad: Double = radius * radius;
  return pi * sqRad;
}

val description: String =
  "Area is: " + area(radius);

'return' implícito

val radius: Double = 2.3;

var color: String = "red";

def area(radius: Double): Double = {
  val pi: Double = 3.141592;
  val sqRad: Double = radius * radius;
  pi * sqRad;
}

val description: String =
  "Area is: " + area(radius);

Ejemplo en SCALA

Operaciones elementales

Sintaxis

val radius: Double = 2.3;

var color: String = "red";

def area(radius: Double): Double = {
  val pi: Double = 3.141592;
  val sqRad: Double = radius * radius;
  return pi * sqRad;
}

val description: String =
  "Area is: " + area(radius);

inferencia de tipos

val radius = 2.3;

var color = "red";

def area(radius: Double) = {
  val pi = 3.141592;
  val sqRad = radius * radius;
  pi * sqRad;
}

val description = "Area is: " + area(radius);

Ejemplo en SCALA

Operaciones elementales

Sintaxis

val radius: Double = 2.3;

var color: String = "red";

def area(radius: Double): Double = {
  val pi: Double = 3.141592;
  val sqRad: Double = radius * radius;
  pi * sqRad;
}

val description: String =
  "Area is: " + area(radius);
val radius = 2.3;

var color = "red";

def area(radius: Double) = {
  val pi = 3.141592;
  val sqRad = radius * radius;
  pi * sqRad;
}

val description = "Area is: " + area(radius);

; opcional

val radius = 2.3

var color = "red"

def area(radius: Double) = {
  val pi = 3.141592
  val sqRad = radius * radius
  pi * sqRad
}

val description = "Area is: " + area(radius)

Ejemplo en SCALA

Operaciones elementales

Sintaxis

val radius = 2.3

var color = "red"

def area(radius: Double) = {
  val pi = 3.141592
  val sqRad = radius * radius
  pi * sqRad
}

val description = "Area is: " + area(radius)

{ } opcional

val radius = 2.3

var color = "red"

def area(radius: Double) = 3.141592 * radius * radius

val description = "Area is: " + area(radius)

Ejemplo en SCALA

Operaciones elementales

Sintaxis

val radius = 2.3

var color = "red"

def area(radius: Double) = 3.141592 * radius * radius

val description = "Area is: " + area(radius)

String interpolation

val radius = 2.3

var color = "red"

def area(radius: Double) = 3.141592 * radius * radius

val description = s"Area is: ${area(radius)}"

Ejemplo en SCALA

Operaciones elementales

Sintaxis

public class User {

    private Integer id;
    private String name;
    private LocalDate birthday;

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

    public Integer getId() { return id; }
    public void setId(Integer id){ this.id = id;}
   // Getter and Setter for rest of fields...

    @Override
    public boolean equals(Object o) {
        // usually a lot of code...
    }

    @Override
    public int hashCode() {
        // more code!
    }
}
case class User(id: Int,
                name: String,
                birthday: LocalDate)

JAVA

SCALA

DTO

  • Constructor
  • Getters (y Setters si hay var)
  • copy()
  • toString()
  • equals() y hashCode()

Sintaxis

val status = if (age >= minAge) "adult" else "minor"

if como expresión

Sin sentencias: todo es una expresión

Sintaxis

def audit(message: String): Unit = {
  auditClient.post(message)
  logger.debug("audit with message: $message")
}

Valor Unit

Inmutabilidad

en Scala

Los valores asignados no cambian

Inmutabilidad

A balanced attitude for Scala programmers

“Prefer vals, immutable objects, and methods without side effects. Reach for them first. Use vars, mutable objects, and methods with side effects when you have a specific need and justification for them.”

scala> val name = "Peter"
name: String = Peter

scala> name = "Paul"
<console>:8: error: reassignment to val
       name = "Paul"
            ^

Variable inmutable

scala> val names = List("Peter", "Paul")
names: List[String] = List(Peter, Paul)

scala> names :+ "Moley"
res0: List[String] = List(Peter, Paul, Moley)

Lista inmutable

Ejemplo: Cuenta bancaria

Inmutabilidad

class BankAccount {
  var balance = 0

  def deposit(amount: Int) {
    if (amount > 0) balance += amount
    else balance
  }

  def withdraw(amount: Int): Int =
    if (amount >= 0 && amount <= balance) {
      balance -= amount
      balance
    } else {
      println("insufficient funds")
      balance
    }
}

Con mutabilidad

val account = new BankAccount
account.deposit(30) // return 30
account.deposit(20) // return 20
account.withdraw(10) // return 10

// Current balance: 40
case class BankAccount(balance: Int) {

  def deposit(amount: Int) =
    if (amount > 0)
      BankAccount(balance = balance + amount)
    else
      this

  def withdraw(amount: Int) =
    if (amount >= 0 && amount <= balance) {
      BankAccount(balance = balance - amount)
    } else {
      println("insufficient funds")
      this
    }
}

Con inmutabilidad

BankAccount(0)
  .deposit(30) // return a new BankAccount
  .deposit(20) // return a new one
  .withdraw(10) // return a new one

// Balance in the last BankAccount: 40

Inmutabilidad: No se cambia el valor, se crea uno nuevo.

Funcional

Funcional: Qué es?

  • Output determinado sólo por su input
  • Evaluar una expresión siempre da el mismo resultado
  • Se puede reemplazar una expresión por su resultado: transparencia referencial

Funciones puras, valores y composición, nada más!

Implicancias:

Estructuras funcionales

Funciones de Orden Superior

Son aquellas funciones que reciben funciones como parámetro o retornan funciones como resultado.

class Person(firstName: String, lastName: String, val age: Int) {
    def itIsTrueThat(f: Person => Boolean): Boolean = f(this)
}



def isOlderThan30(p: Person): Boolean = p.age > 30



val mirtha = new Person("Mirtha", "Legrand", 9999)


def isOlderThan30(p: Person): Boolean = p.age > 30
def isOlderThan50(p: Person): Boolean = p.age > 50
def isOlderThan5000(p: Person): Boolean = p.age > 5000
def isOlderThan10000(p: Person): Boolean = p.age > 10000

mirtha.itIsTrueThat(isOlderThan30) // true
mirtha.itIsTrueThat(isOlderThan50) // true
mirtha.itIsTrueThat(isOlderThan5000) // true
mirtha.itIsTrueThat(isOlderThan10000) // false

Funciones de Orden Superior

def isOlderThan(n: Int, p: Person): Boolean = p.age > n

Podríamos extraer la edad mínima contra la que queremos comparar:

Pero, ¿ya no podemos pasarla como parámetro directamente?

Aplicación Parcial

Aplicar una función con menos parámetros de los que originalmente recibe, devolviendo una nueva función que recibe los parámetros que le faltan y retorna lo que retornaba la función original.

Aplicación Parcial

def isOlderThan(n: Int)(p: Person): Boolean = p.age > n

Una función de tipo Int => Person => Boolean

Ejemplo:

val olderThan300: (Person => Boolean) = isOlderThan(300)

La podemos aplicar solo con el parámetro n:

mirtha.itIsTrueThat(olderThan300) // true

Y usamos la nueva función con la persona:

o

mirtha.itIsTrueThat(isOlderThan(300)) // true

Funciones anónimas (Lambdas)

Son funciones sin nombre, definidas en línea para ser usadas una única vez.

mirtha.itIsTrueThat(isOlderThan(300)) // true
mirtha.itIsTrueThat((p: Person) => p.age > 300) // true
mirtha.itIsTrueThat(p => p.age > 300) // true con inferencia de tipos
mirtha.itIsTrueThat(_.age > 300) // true con _
List<Person> students = new ArrayList<Person>(new Person("Juan", 9),
                                                 new Person("Pepe", 6));
List<Person> nerds = new ArrayList<Person>();


for (Person student : students) {
    if (student.getGrade >= 9) {
        nerds.add(student);
    }

}

return nerds;

Veamos éste código en Java 1.7

Resolvemos el mismo problema en Scala

val students = Person("Juan", 9) :: Person("Pepe", 6) :: Nil
students.filter(_.grade >= 9)

Declaratividad: ocuparse de qué problema resolver, y no tanto del cómo resolverlo

Expresividad: se refiere a lo auto-descriptivo del código

val unNerd = students.find(_.grade >= 9)

¿De qué tipo es unNerd?

Dado el siguiente código:

Cómo resolvemos...

  • Uso de null
  • Excepciones
  • Logging
  • Métricas
  • Mantener un estado global
  • ...

?

Effect

Tipo de datos con info extra

sealed trait Option[+T]

case class Some[T](value: T) extends Option[T]
case object None extends Option[Nothing]

Option[T]: Effect para modelar null

Ejemplo:

¿De qué tipo es nerd?

Volviendo al find()...

Option[Person]

students
  .find(_.grade >= 9)
  .map(nerd => Response.Ok(nerd.name))
  .getOrElse(Response.NotFound("no nerd found"))
val nerd = students.find(_.grade >= 9)
val nerd: Option[Person] = students.find(_.grade >= 9)

Ejemplo de uso:

Effect

Otro ejemplo: errores

def divide(dividend: Int, divisor: Int):Int = {
  if (divisor == 0)
    throw new RuntimeException(
      "Divisor can't be 0")
  dividend / divisor
}

En imperativo usaríamos Exception

sealed trait Either[+A, +B]

case class Left[A, B](left: A)
  extends Either[A, B]

case class Right[A, B](right: A)
  extends Either[A, B]

En funcional usamos Effect...

def divide(dividend: Int,
           divisor: Int):
    Either[String, Int] = {
  if (divisor == 0)
    Left("Divisor can't be 0")
  else
    Right(dividend / divisor)
}

Es un side-effect

  • Pierdo Transparencia Referencial
  • Es dependiente del contexto
  • Difícil de tracear/razonar/refactorizar/etc.

Either[T]: Effect para modelar errores

Es puro (sin side-effect)

  • Tiene Transparencia Referencial
  • Tipado
  • Separación de responsabilidades

Effect

Tiene la forma F[T]

type F[T] = Option[T]

Puede tener un valor T o no

type F[T] = Either[E, T]

Devuelve un error E o un resultado T

type F[T] = List[T]

Genera entre 0 y N valores T

Muchos otros...

(Para algún E)

Programa en F que computa un valor T

Composición

case class Email(subject: String, text: String)
val addMissingSubject = (email: Email) =>
  if (email.subject.isEmpty) email.copy(subject = "No subject")
  else email
val checkSpelling = (email: Email) =>
  email.copy(text = email.text.replaceAll("youre", "you're"))
val removeInappropriateLanguage = (email: Email) =>
  email.copy(text = email.text.replaceAll("dynamic typing", "**CENSORED**"))
val addAdvertismentToFooter = (email: Email) =>
  email.copy(text = email.text + "\nThis mail sent via Super Awesome Free Mail")
val pipeline = addMissingSubject
  .andThen(checkSpelling)
  .andThen(removeInappropriateLanguage)
  .andThen(addAdvertismentToFooter)

Ejemplo: preparar Email

Funciones con valores (en "crudos")

scala> pipeline(Email(subject = "Apply for Dev", text = "Hi! Do you have opportunities
 with dynamic typing lang like Groovy or with static typing like Scala?"))
res1: Email = 
Email(Apply for Dev,Hi! Do you have opportunities with **CENSORED** lang like Groovy or
 with static typing like Scala?
This mail sent via Super Awesome Free Mail)

(f o g) (x)

Composición

case class Email(subject: String, text: String)
type EmailPipeline = Email => Option[Email]

val checkSubject: EmailPipeline = (email: Email) =>
  if (email.subject.isEmpty) None
  else Some(email)
val fixSpelling: EmailPipeline = (email: Email) =>
  Some(email.copy(text = email.text.replaceAll("youre", "you're")))
val removeInappropriateLanguage: EmailPipeline = (email: Email) =>
  if (email.text.contains("dynamic typing")) None
  else Some(email)
val addAdvertismentToFooter: EmailPipeline = (email: Email) =>
  Some(email.copy(text = email.text + "\nThis mail sent via Super Awesome Free Mail"))

Ejemplo: preparar Email

Funciones con Effects

Las funciones devuelven Option[Email]

scala> checkSubject.andThen(fixSpelling)
<console>:13: error: type mismatch;
 found   : Email => Option[Email]
 required: Option[Email] => ?
              checkSubject.andThen(fixSpelling)
                                   ^

Funciones con Effect no componen directamente

Composición

def effectComposition[A, B, C, F[_]](f: A => F[B], g: B => F[C]): A => F[C]

Necesitamos componer funciones que devuelven Effect:

Funciones con Effects

A => F[B]
B => F[C]

+

A => F[C]

En código:

Ejemplo: funciones que retornan Option[T]

def optionComposition[A, B, C](f: A => Option[B],
                               g: B => Option[C]): A => Option[C] =
  (a: A) => f(a) match {
    case Some(b) => g(b)
    case None => None
  }
def optionComposition[A, B, C](f: A => Option[B],
                               g: B => Option[C]): A => Option[C] =
  (a: A) => f(a).flatMap(b => g(b))

No se puede generalizar para cualquier Effect

Composición

sealed trait Option[+T] {
  def flatMap[B](f: T => Option[B]): Option[B] = this match {
    case Some(c) => f(c)
    case None => None
  }
}
case class Some[T](value: T) extends Option[T]
case object None extends Option[Nothing]

Que tal si cada Effect implementa flatMap()?

Funciones con Effects

F[A]
A => F[B]

+

F[B]

Option[T] ya lo implementa:

Volviendo a nuestro ejemplo de emails:

val email = Email(subject = "Apply for Dev", text = "Hi! Do you have opportunities
 with dynamic typing lang like Groovy or with static typing like Scala?")

checkSubject(email)                          // Some(Email(..))
  .flatMap(fixSpelling)                      // Some(Email(..))
  .flatMap(removeInappropriateLanguage)      // None
  .flatMap(addAdvertismentToFooter)          // None

Composición

trait Monad[F[_]] {
  def unit[A](value: A): F[A]
  def flatMap[A, B](effect: F[A])(f: A => F[B]): F[B]
}

flatMap() viene de una abstracción muy nombrada últimamente: Monad

de Effects mediante flatMap()

Un Effect es un Monad si implementa:

Option es un Monad con unit:

def unit[A](value: A): Option[A] = Some(value)

Either es un Monad con unit:

def unit[A, B](value: B): Either[A, B] = Right(value)

List es un Monad con unit:

def unit[A](value: A): List[A] = List(value)

Ofrece una manera conveniente de secuenciar operaciones que generan Effect

Y además debe cumplir las leyes de identidad y asociatividad

Algunas Monad implementadas en Scala:

Composición

Scala ofrece una manera muy conveniente de operar con Monads: for-comprehension

de Effects mediante Monad

case class Email(subject: String, text: String, from: String, to: List[String])
case class Person(name: String, email: String)
case class Envelope(email: Email, destination: Person)

def getPersonByEmail(email: String): Option[Person] = ???

Ejemplo: Preparar Envelope para los Emails

def generateEnvelopes(emails: List[Email]): List[Envelope] =
  emails
    .flatMap(email => email.to
      .flatMap(destEmail => getPersonByEmail(destEmail)
        .map(destination => Envelope(email, destination))))
def generateEnvelopesViaFor(emails: List[Email]): List[Envelope] = {
  for {
    email       <- emails
    destEmail   <- email.to
    destination <- getPersonByEmail(destEmail)
  } yield {
    Envelope(email, destination)
  }
}

Lo mismo mediante for-comprehension

En resumen...

  • Programación funcional consiste en funciones puras, valores y composición.
  • Los side-effects (Excepciones, logging, metrics, IO, etc.) se representan con Effects.
  • Monad provee una manera muy conveniente para componer Effects.
  • Scala trae muy buen soporte para trabajar con Effects y Monads.
Made with Slides.com