Todas las funciones de un sistema (o su mayoría) deberían depender exclusivamente de sus parámetros de entrada y no deberían hacer otra cosa distinta a producir un resultado.
Un core puramente funcional (que consiste en funciones data in => data out) y una delgada capa exterior que se encargue de los efectos (metodos que ejecuten los side effects)
Una expresión e es referencialmente transparente si, para todos los programas p, todas las ocurrencias de e en p pueden ser reemplazadas por el resultado de evaluar e sin afectar el significado de p.
Una expresión e es referencialmente transparente si, para todos los programas p, todas las ocurrencias de e en p pueden ser reemplazadas por el resultado de evaluar e sin afectar el significado de p.
Una función f es pura si la expresión f(x) es referencialmente transparente para todos los x que son referencialmente transparentes
class Cafe {
def buyCoffee(cc: CreditCard): Coffee = {
val cup = new Coffee()
cc.charge(cup.price) //Calls CC service & store charge locally
cup
}
}
class Cafe {
def buyCoffee(cc: CreditCard, p: Payments): Coffee = {
val cup = new Coffee()
p.charge(cc, cup.price)
cup
}
}
class Cafe {
def buyCoffee(cc: CreditCard, p: Payments): Coffee = {
val cup = new Coffee()
p.charge(cc, cup.price)
cup
}
}
class Cafe {
def buyCoffee(cc: CreditCard, p: Payments): Coffee = {
val cup = new Coffee()
p.charge(cc, cup.price)
cup
}
}
class Cafe {
def buyCoffee(cc: CreditCard, p: Payments): Coffee = {
val cup = new Coffee()
p.charge(cc, cup.price)
cup
}
}
def buyCoffees(cc: CreditCard, p: Payments, n: Int): List[Coffee]
Eliminar completamente los side effects
class Cafe {
def buyCoffee(cc: CreditCard): (Coffee, Charge) = {
val cup = new Coffee()
(cup, Charge(cc, cup.price))
}
}
case class Charge(cc: CreditCard, amount: Double) {
def combine(other: Charge): Charge = {
if ( cc == other.cc )
Charge(cc, amount + other.amount )
else
throw new Exception("Can't combine charges to different cards")
}
}
Eliminar completamente los side effects
class Cafe {
def buyCoffee(cc: CreditCard): (Coffee, Charge) = ...
def buyCoffees(cc: CreditCard, n: Int): (List[Coffee], Charge) = {
val purchases : List[(Coffee, Charge)] = List.fill(n)(buyCoffee(cc))
val (coffees, charges) = purchases.unzip
(coffees, purchases.reduce( (c1,c2) => c1.combine(c2) ) )
}
}
def merge(charges: List[Charge]): List[Charge] = {
val grouped: Map[CreditCard, List[Charge]] = charges.groupBy(_.cc)
val values: List[List[Charge]] = grouped.values
values.map( charges => charges.reduce( (a,b) => a.combine(b) ).toList
}
def merge(charges: List[Charge]): List[Charge] = {
val grouped: Map[CreditCard, List[Charge]] = charges.groupBy(_.cc)
val values: List[List[Charge]] = grouped.values
values.map( charges => charges.reduce( (a,b) => a.combine(b) ).toList
}
def executeCharge(charge: Charge): Unit = {
...
}
Decir qué queremos obtener, pero no cómo queremos obtenerlo
(...) programs must be written for people to read, and only incidentally for machines to execute
- Structure and interpretation of computer programs
public List<String> encontrarReportesDeBugs(List<String> lines) {
List<String> resultado = new ArrayList<String>();
for(String line: lines) {
if(line.startsWith("BUG")) {//<- Esto es lo único importante!
resultado.add(line)
}
}
return resultado;
}
def encontrarReportesDeBugs(lines: List[String]): List[String] = {
lines.filter(line => line.startsWith("BUG"))
}
Poder pasar verbos y no solamente sujetos
Calcular el promedio, usando funciones de orden superior
Reduce es una función de alto nivel que funciona sobre iterables para combinar sus valores en un único valor:
class List[A] {
...
def reduce[A](f: (A,A) => A): A = ...
}
def add(a: Double, b: Double): Double = a + b
def sum(xs: List[Double]): Double = xs.reduce(add)
def mean(xs: List[Double]): Double = sum(xs) / xs.length
Calcular el promedio, usando funciones de orden superior
Computar las desviaciones:
class List[A] {
...
def map[B](f: A => B): List[B] = ...
}
Map es una función de orden superior sobre iterables que devuelve otro iterable con el resultado de aplicar la función a cada elemento del iterable:
def squaredDesviations(xs: List[Double]): List[Double] = {
val _mean = mean(xs)
xs.map { x => square(x - _mean) }
}
Computar las desviaciones:
def std(xs: List[Double]): Double = Math.sqrt( mean( squaredDesviations(xs) ) )
def std = (squaredDesviations _) andThen (mean _) andThen (Math.sqrt _)
def std(xs: List[Double]): Double = Math.sqrt( mean( squaredDesviations(xs) ) )
def square(x: Double): Double = x * x
def add(a: Double, b: Double): Double = a + b
def sum(xs: List[Double]): Double = xs.reduce(add)
def mean(xs: List[Double]): Double = sum(xs) / xs.length
def squaredDesviations(xs: List[Double]): List[Double] = {
val _mean = mean(xs)
xs.map { x => square(x - _mean) }
}
def std = (squaredDesviations _) andThen (mean _) andThen (Math.sqrt _)
Juntandolo todo:
sealed trait Game {
def board: Board
def isDraw: Boolean
def hasWinner: Boolean
def isFinished: Boolean = isDraw || hasWinner
}
case class DrawGame(board: Board) extends Game {
def isDraw: Boolean = true
def hasWinner: Boolean = false
}
case class WonGame(board: Board, winner: Winner) extends Game {
def isDraw: Boolean = false
def hasWinner: Boolean = true
}
case class ActiveGame(board: Board, currentPlayer: Player) extends Game {
def isDraw: Boolean = false
def hasWinner: Boolean = false
def otherPlayer: Player = if(currentPlayer == PlayerX) PlayerO else PlayerX
def putMark(mark: Player, position: Position): Game = {
if(currentPlayer != mark) {
throw IncorrectPlayerError
} else {
val nextBoard = board.putMark(mark, position)
nextBoard.winner match {
case Some(winner) => WonGame(nextBoard, winner)
case None =>
if(nextBoard.somebodyCanWin) {
ActiveGame(nextBoard, otherPlayer)
} else {
DrawGame(nextBoard)
}
}
}
}
}
¡Nada de esto sería posible si hubieramos modelado el estado permitiendo mutaciones sobre un objeto de la misma clase!
Los tipos no tienen por que limitarnos.
Los tipos no tienen por que limitarnos.
Pueden servir para expresarnos mejor:
Los tipos no tienen por que limitarnos.
Pueden servir para expresarnos mejor:
Los tipos no tienen por que limitarnos.
Pueden servir para expresarnos mejor:
Queremos una función que nos devuelva un usuario según su nombre de usuario.
Una posible firma de la función sería:
String => User
¿Qué pasa si no se encuentra el usuario?
Username => Option[User]
¡En la firma de la función se dice mucho!
Dice que se espera recibir y qué puede pasar al ejecutar la función
case class Meter(value: Double)
case class Meter(value: Double)
case class Meter(value: Double)
case class Meter(value: Double) extends AnyVal {
def +(other: Meter): Meter = Meter(value + other.value)
def toInches: Inch = Inch(39.3700787 * value)
}
val x = Meter(1.5)
val y = Meter(2.0)
val z = Inch(34.0)
x + y // <- OK
x + z // <- No compila!
x.toInches + z //<- OK
val x = Meter(1.5)
val y = Meter(2.0)
val z = Inch(34.0)
x + y // <- OK
x + z // <- No compila!
x.toInches + z //<- OK
def getSideLengths(figure: Figure): List[Meter] = ...
def getPerimeterLength(lengths: List[Meter]): Meter = ...
def encrypt(msg: Array[Byte]): Array[Byte] = ...
def decrypt(msg: Array[Byte]): Array[Byte] = ...
def send(msg: Array[Byte]) = ...
Una primera aproximación:
trait Encrypted
trait PlainText
case class Message[T](bytes: Array[Byte]) extends AnyVal {
...
}
def encrypt(msg: Message[PlainText]): Message[Encrypted] = ...
def decrypt(msg: Message[Encrypted]): Message[Decrypted] = ...
def send(msg: Message[Encrypted]) = ...
trait Encrypted
trait PlainText
case class Message[T](bytes: Array[Byte]) extends AnyVal {
...
}
def encrypt(msg: Message[PlainText]): Message[Encrypted] = ...
def decrypt(msg: Message[Encrypted]): Message[Decrypted] = ...
def send(msg: Message[Encrypted]) = ...
¿Que pasa si hacemos que la única forma de construir valores del tipo Message[Encrypted] sea a través de la función encrypt?
trait Encrypted
trait PlainText
case class Message[T](bytes: Array[Byte]) extends AnyVal {
...
}
def encrypt(msg: Message[PlainText]): Message[Encrypted] = ...
def decrypt(msg: Message[Encrypted]): Message[Decrypted] = ...
def send(msg: Message[Encrypted]) = ...
¿Que pasa si hacemos que la única forma de construir valores del tipo Message[Encrypted] sea a través de la función encrypt?
¡Entonces la única forma de llamar la función send será cifrando mensajes!