Higher Kinded Types,

Type lambdas

y Type classes

Agenda

  • Higher Kinded TypesType lambdas
  • Type Classes y el Scope implícito
  • Patrón pimp-my-class
  • Conectando Type Lambdas y Type Classes

Higher Kinded Types

Los tipos de los tipos

Los tipos clasifican valores:

scala> val x = 1

scala> val s = "asdfadfas"

scala> val f = { x: Int => x + "n" }

scala> val l = List(1,2,3)

Tipos

Los tipos clasifican valores:

scala> val x = 1

scala> val s = "asdfadfas"

scala> val f = { x: Int => x + "n" }

scala> val l = List(1,2,3)

scala> :t x
Int

Tipos

Los tipos clasifican valores:

scala> val x = 1

scala> val s = "asdfadfas"

scala> val f = { x: Int => x + "n" }

scala> val l = List(1,2,3)

scala> :t x
Int

scala> :t s
String

Tipos

Los tipos clasifican valores:

scala> val x = 1

scala> val s = "asdfadfas"

scala> val f = { x: Int => x + "n" }

scala> val l = List(1,2,3)

scala> :t x
Int

scala> :t s
String

scala> :t f
Int => String

Tipos

Los tipos clasifican valores:

scala> val x = 1

scala> val s = "asdfadfas"

scala> val f = { x: Int => x + "n" }

scala> val l = List(1,2,3)

scala> :t x
Int

scala> :t s
String

scala> :t f
Int => String

scala> :t l
List[Int]

Tipos

Los kinds clasifican tipos

¿Cual es el  kind de  Int?

Los tipos propios son aquellos tipos de los que podemos construir un valor.

:k -v Int
*
This is a proper type.

Int es un tipo autocontenido. A diferencia de otros tipos, como List no requiere especificar ningún tipo.

Kinds

scala> :k -v String

Kinds

scala> :k -v String
*
This is a proper type.

Kinds

Kinds

scala> :k -v String
*
This is a proper type.

scala> :k -v Int => String
scala> :k -v String
*
This is a proper type.

scala> :k -v Int => String
*
This is a proper type.

Kinds

scala> :k -v String
*
This is a proper type.

scala> :k -v Int => String
*
This is a proper type.

scala> :k -v List[Float]

Kinds

scala> :k -v String
*
This is a proper type.

scala> :k -v Int => String
*
This is a proper type.

scala> :k -v List[Float]
*
This is a proper type.

Kinds

scala> :k -v String
*
This is a proper type.

scala> :k -v Int => String
*
This is a proper type.

scala> :k -v List[Float]
*
This is a proper type.
scala> case class Foo(a: Int, b: String)

scala> :k -v Foo

Kinds

scala> :k -v String
*
This is a proper type.

scala> :k -v Int => String
*
This is a proper type.

scala> :k -v List[Float]
*
This is a proper type.
scala> case class Foo(a: Int, b: String)

scala> :k -v Foo
*
This is a proper type.

Kinds

scala> :k -v String
*
This is a proper type.

scala> :k -v Int => String
*
This is a proper type.

scala> :k -v List[Float]
*
This is a proper type.
scala> case class Foo(a: Int, b: String)

scala> :k -v Foo
*
This is a proper type.

* Nota: La consola de Scala de verdad se equivoca al preguntarle el kind en estos casos. Otros lenguajes con Higher Kinded Types si retornan la respuesta correcta en estos casos

Kinds

scala> :k -v String
*
This is a proper type.

scala> :k -v Int => String
*
This is a proper type.

scala> :k -v List[Float]
*
This is a proper type.
scala> case class Foo(a: Int, b: String)

scala> :k -v Foo
*
This is a proper type.

scala> :k -v List

Kinds

scala> :k -v String
*
This is a proper type.

scala> :k -v Int => String
*
This is a proper type.

scala> :k -v List[Float]
*
This is a proper type.

A diferencia de los anteriores no podemos construir un valor de tipo solo  List

Debemos llenar el [_] en  List[_] con algún tipo.

List es un constructor de tipos, es decir ¡un tipo de orden superior!

scala> case class Foo(a: Int, b: String)

scala> :k -v Foo
*
This is a proper type.

scala> :k -v List

Es decir List  es un tipo que necesita a otro tipo para poder "especificarse" y así crear un tipo propio.

Kinds

Así como hay funciones de orden superior (funciones que reciben otras funciones, por ejemplo) existen tipos de orden superior (tipos que reciben otros tipos)

:k List 

Higher Kinded Types

Así como hay funciones de orden superior (funciones que reciben otras funciones, por ejemplo) existen tipos de orden superior (tipos que reciben otros tipos)

:k List 
* -> *

Higher Kinded Types

Así como hay funciones de orden superior (funciones que reciben otras funciones, por ejemplo) existen tipos de orden superior (tipos que reciben otros tipos)

:k List 
* -> *
:k Option

Higher Kinded Types

Así como hay funciones de orden superior (funciones que reciben otras funciones, por ejemplo) existen tipos de orden superior (tipos que reciben otros tipos)

:k List 
* -> *
:k Option
* -> *

Higher Kinded Types

Así como hay funciones de orden superior (funciones que reciben otras funciones, por ejemplo) existen tipos de orden superior (tipos que reciben otros tipos)

:k List 
* -> *
:k Option
* -> *
:k Future

Higher Kinded Types

Así como hay funciones de orden superior (funciones que reciben otras funciones, por ejemplo) existen tipos de orden superior (tipos que reciben otros tipos)

:k List 
* -> *
:k Option
* -> *
:k Future
* -> *

Higher Kinded Types

Así como hay funciones de orden superior (funciones que reciben otras funciones, por ejemplo) existen tipos de orden superior (tipos que reciben otros tipos)

:k List 
* -> *
:k Option
* -> *
:k Future
* -> *
:k Either

Higher Kinded Types

Así como hay funciones de orden superior (funciones que reciben otras funciones, por ejemplo) existen tipos de orden superior (tipos que reciben otros tipos)

:k List 
* -> *
:k Option
* -> *
:k Future
* -> *
:k Either
(*,*) -> *

Higher Kinded Types

Así como hay funciones de orden superior (funciones que reciben otras funciones, por ejemplo) existen tipos de orden superior (tipos que reciben otros tipos)

:k List 
* -> *
:k Option
* -> *
:k Future
* -> *
:k Either
(*,*) -> *

Higher Kinded Types

* Nota: La consola de Scala no devuelve los kinds en esta misma notación. La idea acá es transmitir lo importante.

Higher Kinded Types

Estos tipos se denominan:

tipos abstractos o constructores de tipos o higher kinded types

Así como hay funciones de orden superior (funciones que reciben otras funciones, por ejemplo) existen tipos de orden superior (tipos que reciben otros tipos)

:k List 
* -> *
:k Option
* -> *
:k Future
* -> *
:k Either
(*,*) -> *

Algunos paralelos

Higher Kinded Types

Algunos paralelos

def id(a: Int): Int = a

Higher Kinded Types

Algunos paralelos

def id(a: Int): Int = a 
type Id[A] = A 

Higher Kinded Types

Algunos paralelos

def id(a: Int): Int = a //:t id :: Int => Int
type Id[A] = A 

Higher Kinded Types

Algunos paralelos

def id(a: Int): Int = a //:t id :: Int => Int
type Id[A] = A          //:k Id ::  *  ->  *

Higher Kinded Types

Algunos paralelos

def id(a: Int): Int = a //:t id :: Int => Int
type Id[A] = A          //:k Id ::  *  ->  *
def id(f: Int => Int, a: Int): Int = f(a)

Higher Kinded Types

Algunos paralelos

def id(a: Int): Int = a //:t id :: Int => Int
type Id[A] = A          //:k Id ::  *  ->  *
def id(f: Int => Int, a: Int): Int = f(a)
type Id[A[_],B] = A[B]

Higher Kinded Types

Algunos paralelos

def id(a: Int): Int = a //:t id :: Int => Int
type Id[A] = A          //:k Id ::  *  ->  *
def id(f: Int => Int, a: Int): Int = f(a) //:t id :: (Int => Int, Int) => Int
type Id[A[_],B] = A[B]

Higher Kinded Types

Algunos paralelos

def id(a: Int): Int = a //:t id :: Int => Int
type Id[A] = A          //:k Id ::  *  ->  *
def id(f: Int => Int, a: Int): Int = f(a) //:t id :: (Int => Int, Int) => Int
type Id[A[_],B] = A[B]

¿Qué quiere decir A[_]?

Higher Kinded Types

Algunos paralelos

def id(a: Int): Int = a //:t id :: Int => Int
type Id[A] = A          //:k Id ::  *  ->  *
def id(f: Int => Int, a: Int): Int = f(a) //:t id :: (Int => Int, Int) => Int
type Id[A[_],B] = A[B]

Un tipo de orden superior que recibe un solo tipo

:k A :: * -> *

¿Qué quiere decir A[_]?

Higher Kinded Types

Algunos paralelos

def id(a: Int): Int = a //:t id :: Int => Int
type Id[A] = A          //:k Id ::  *  ->  *
def id(f: Int => Int, a: Int): Int = f(a) //:t id :: (Int => Int, Int) => Int
type Id[A[_],B] = A[B]                    //:k Id :: ( *  ->  * ,  * ) ->  *

Un tipo de orden superior que recibe un solo tipo

:k A :: * -> *

¿Qué quiere decir A[_]?

Higher Kinded Types

Higher Kinded Types

Ejemplo

¿Qué tipos coinciden con D?

Tipos propios como Int, StringList[Int] o Either[Throwable, MyClass]

¿Qué tipos coinciden con F[_]?

Tipos de orden superior que necesitan un (1) tipo como: FutureListOption, o Vector

¿Qué tipos coinciden con G[_,_]?

Tipos de orden superior que necesitan dos (2) tipos como: EitherFunction1 Validation

Higher Kinded Types

trait Example[ D , F[_] , G[_,_] ]
trait Impl0 extends Example[ Int , List , Either ]
trait Example[ D , F[_] , G[_,_] ]

Compila

trait Impl0 extends Example[ Int , List , Either ]
trait Example[ D , F[_] , G[_,_] ]
trait Impl1 extends Example[ List[Int] , List , Validation]

Compila

trait Impl0 extends Example[ Int , List , Either ]
trait Example[ D , F[_] , G[_,_] ]

Compila

trait Impl1 extends Example[ List[Int] , List , Validation]

Compila

trait Impl0 extends Example[ Int , List , Either ]
trait Example[ D , F[_] , G[_,_] ]
trait Impl2 extends Example[ List , List , Either ]

Compila

trait Impl1 extends Example[ List[Int] , List , Validation]

Compila

trait Impl0 extends Example[ Int , List , Either ]
trait Example[ D , F[_] , G[_,_] ]

No Compila

trait Impl2 extends Example[ List , List , Either ]

Compila

trait Impl1 extends Example[ List[Int] , List , Validation]

Compila

trait Impl0 extends Example[ Int , List , Either ]
trait Example[ D , F[_] , G[_,_] ]
trait Impl3 extends Example[ Int , Either , Either ]

No Compila

trait Impl2 extends Example[ List , List , Either ]

Compila

trait Impl1 extends Example[ List[Int] , List , Validation]

Compila

trait Impl0 extends Example[ Int , List , Either ]
trait Example[ D , F[_] , G[_,_] ]

No Compila

trait Impl3 extends Example[ Int , Either , Either ]

No Compila

trait Impl2 extends Example[ List , List , Either ]

Compila

trait Impl1 extends Example[ List[Int] , List , Validation]

Compila

trait Impl0 extends Example[ Int , List , Either ]
trait Example[ D , F[_] , G[_,_] ]
trait Impl4 extends Example[ Int , List , Future ]

No Compila

trait Impl3 extends Example[ Int , Either , Either ]

No Compila

trait Impl2 extends Example[ List , List , Either ]

Compila

trait Impl1 extends Example[ List[Int] , List , Validation]

Compila

trait Impl0 extends Example[ Int , List , Either ]
trait Example[ D , F[_] , G[_,_] ]

No Compila

trait Impl4 extends Example[ Int , List , Future ]

No Compila

trait Impl3 extends Example[ Int , Either , Either ]

No Compila

trait Impl2 extends Example[ List , List , Either ]

Compila

trait Impl1 extends Example[ List[Int] , List , Validation]

Compila

trait Impl0 extends Example[ Int , List , Either ]
trait Example[ D , F[_] , G[_,_] ]
trait Example[ F[_] ]

Sabemos que tipos como  ListOption Future pueden hacer el papel de  F

Sabemos que tipos como  ListOption Future pueden hacer el papel de  F

Either no encaja por que requiere dos tipos, mientras que  F requiere solo uno.

Either[t1, t2] no cuadra con F[t1]
trait Example[ F[_] ]

Sabemos que tipos como  ListOption Future pueden hacer el papel de  F

Either no encaja por que requiere dos tipos, mientras que  F requiere solo uno.

Either[String, t1] 
Either[t1, t2] no cuadra con F[t1]

Pero si pudieramos hacer algo como:

trait Example[ F[_] ]

Sabemos que tipos como  ListOption Future pueden hacer el papel de  F

Either no encaja por que requiere dos tipos, mientras que  F requiere solo uno.

Either[String, t1] si cuadra con F[t1]
Either[t1, t2] no cuadra con F[t1]

Pero si pudieramos hacer algo como:

trait Example[ F[_] ]

Un paralelo con funciones:

Aplicando parcialmente  plus:

def sumTwo(x: Int): Int = sum(2,x)
useFunction( sumTwo )
def plus(a: Int, b: Int): Int = a + b 
// :t plus :: (Int, Int) => Int
def useFunction(f: Int => Int): Int = f(4) 
// :t useFunction :: (Int => Int) => Int

Es la misma idea en el sistema de tipos:

def plus(a: Int, b: Int): Int = a + b 
// :t plus :: (Int, Int) => Int
def useFunction(f: Int => Int): Int = f(4) 
// :t useFunction :: (Int => Int) => Int
trait Example[ F[_] ]

Es la misma idea en el sistema de tipos:


:k Either :: (*,*) -> *

:k HigherKindedExample :: (* -> *) -> *
def plus(a: Int, b: Int): Int = a + b 
// :t plus :: (Int, Int) => Int
def useFunction(f: Int => Int): Int = f(4) 
// :t useFunction :: (Int => Int) => Int
trait Example[ F[_] ]

Es la misma idea en el sistema de tipos:

¿Como aplicar parcialmente el argumento String al tipo  Either?


:k Either :: (*,*) -> *

:k HigherKindedExample :: (* -> *) -> *
def plus(a: Int, b: Int): Int = a + b 
// :t plus :: (Int, Int) => Int
def useFunction(f: Int => Int): Int = f(4) 
// :t useFunction :: (Int => Int) => Int
trait Example[ F[_] ]

Es la misma idea en el sistema de tipos:

¿Como aplicar parcialmente el argumento String al tipo  Either?

object Example {

    type EitherWithLeftString[X]



}

Con un type alias:


:k Either :: (*,*) -> *

:k HigherKindedExample :: (* -> *) -> *
def plus(a: Int, b: Int): Int = a + b 
// :t plus :: (Int, Int) => Int
def useFunction(f: Int => Int): Int = f(4) 
// :t useFunction :: (Int => Int) => Int
trait Example[ F[_] ]

Es la misma idea en el sistema de tipos:

¿Como aplicar parcialmente el argumento String al tipo  Either?

object Example {

    type EitherWithLeftString[X] = Either[String, X]



}

Con un type alias:


:k Either :: (*,*) -> *

:k HigherKindedExample :: (* -> *) -> *
def plus(a: Int, b: Int): Int = a + b 
// :t plus :: (Int, Int) => Int
def useFunction(f: Int => Int): Int = f(4) 
// :t useFunction :: (Int => Int) => Int
trait Example[ F[_] ]

Es la misma idea en el sistema de tipos:

¿Como aplicar parcialmente el argumento String al tipo  Either?

object Example {

    type EitherWithLeftString[X] = Either[String, X]

    trait Impl extends Example[ EitherWithLeftString ]

}

Con un type alias:


:k Either :: (*,*) -> *

:k HigherKindedExample :: (* -> *) -> *
def plus(a: Int, b: Int): Int = a + b 
// :t plus :: (Int, Int) => Int
def useFunction(f: Int => Int): Int = f(4) 
// :t useFunction :: (Int => Int) => Int
trait Example[ F[_] ]

¿Qué pasa si queremos definir este tipo de forma  inline?

Es decir de forma anónima sin tener que darle un nombre

¿Por que quisieramos esto?

Para definir la clase o  trait sin tener que acompañarla con la definición de un  type alias.

trait Impl extends Example[ ??? ]

Type Lambdas

El paralelo en funciones:

def sumTwo(x: Int): Int = sum(2,x)
useFunction( sumTwo )

useFunction( { x: Int => sum(2,x) } )
object Example {

    type EitherWithLeftString[X] = Either[String, X]

    trait Impl extends Example[ EitherWithLeftString ]

}

Type Lambdas

Escribiendo type lambdas

trait Impl extends Example[ ({ type F[Y] = Either[String, Y] })#F ]

Hay una forma de definir type lambdas manualmente:

trait Impl extends Example[ Either[String, ?] ]

o también se puede escribir:

trait Impl extends Example[ Lambda[ A => Either[String, A] ] ]

La primera notación no suele servir con tipos anidados (e.g.   List[Either[String, ?]]  no funciona). Para esos casos está la segunda notación.

Proyectos como Scalaz ya lo usan

Usando un plugin del compilador podemos escribir:

BonO

Como se escriben type lambdas en Scala sin usar plugins

trait HigherKindedExampleImpl extends HigherKindedExample[ Either[String, ?] ]

¿Como lograr esto mismo sin usar Kind projector?

¿Por que no hacemos algo como esto?

trait HigherKindedExampleImpl extends HigherKindedExample[ Either[String,_] ]

Type Lambdas

¿Por que no hacemos algo como esto?

trait HigherKindedExampleImpl extends HigherKindedExample[ Either[String,_] ]

No va a servir. Esto no quiere decir lo que queremos.

Type Lambdas

¿Por que no hacemos algo como esto?

trait HigherKindedExampleImpl extends HigherKindedExample[ Either[String,_] ]

No va a servir. Esto no quiere decir lo que queremos.

En este contexto el underscore quiere decir tipo existencial

Type Lambdas

¿Por que no hacemos algo como esto?

trait HigherKindedExampleImpl extends HigherKindedExample[ Either[String,_] ]

No va a servir. Esto no quiere decir lo que queremos.

En este contexto el underscore quiere decir tipo existencial

Un tipo existencial es algún tipo en especifico que no nos interesa usar en nuestra implementación. Solo nos interesa que haya algún tipo ahí. 

Type Lambdas

¿Por que no hacemos algo como esto?

trait HigherKindedExampleImpl extends HigherKindedExample[ Either[String,_] ]

No va a servir. Esto no quiere decir lo que queremos.

En este contexto el underscore quiere decir tipo existencial

Un tipo existencial es algún tipo en especifico que no nos interesa usar en nuestra implementación. Solo nos interesa que haya algún tipo ahí. 

def printSize(arr: Array[_]) = println(arr.size)

Por ejemplo:

Type Lambdas

A la implementación no le interesa hacer nada con el tipo del contenido

¿Por que no hacemos algo como esto?

trait HigherKindedExampleImpl extends HigherKindedExample[ Either[String,_] ]

No va a servir. Esto no quiere decir lo que queremos.

En este contexto el underscore quiere decir tipo existencial

Un tipo existencial es algún tipo en especifico que no nos interesa usar en nuestra implementación. Solo nos interesa que haya algún tipo ahí. 

Es decir, en este contexto, Either[String,_] quiere decir un tipo propio y no un constructor de tipos de un parámetro

Type Lambdas

Truco: Usar tipos estructurales

Type Lambdas

Truco: Usar tipos estructurales

*

* Si Scala hubiera sido hecho con type lambdas en mente esto no sería necesario.

Este truco consiste de combinar dos características del lenguaje que no tienen nada que ver, en principio, con type lambdas.

Los usos mas avanzados de Scala son feos y complejos.

Type Lambdas

Truco: Usar tipos estructurales

  • Definir un tipo estructural (algo encerrado con {}) con definiciones de tipos adentro
scala> type X = {
    type F[Y] = Either[String, Y]
}

Type Lambdas

Truco: Usar tipos estructurales

  • Definir un tipo estructural (algo encerrado con {}) con definiciones de tipos adentro
  • Se puede obtener un tipo definido dentro de otro tipo usando # (algo así como el . a nivel de tipos)
scala> type X = {
    type F[Y] = Either[String, Y]
}

scala> val x: X#F[Int] = Right(1)
x: Either[String,Int] = Right(1)

Type Lambdas

Truco: Usar tipos estructurales

  • Definir un tipo estructural (algo encerrado con {}) con definiciones de tipos adentro
  • Se puede obtener un tipo definido dentro de otro tipo usando # (algo así como el . a nivel de tipos)
scala> type X = {
    type F[Y] = Either[String, Y]
}

scala> val x: X#F[Int] = Right(1)
x: Either[String,Int] = Right(1)

scala> :k -v X#F
* -> *

Type Lambdas

Truco: Usar tipos estructurales

  • Definir un tipo estructural (algo encerrado con {}) con definiciones de tipos adentro
  • Se puede obtener un tipo definido dentro de otro tipo usando # (algo así como el . a nivel de tipos)

*

scala> type X = {
    type F[Y] = Either[String, Y]
}

scala> val x: X#F[Int] = Right(1)
x: Either[String,Int] = Right(1)

scala> :k -v X#F
* -> *

Type Lambdas

Truco: Usar tipos estructurales

  • Definir un tipo estructural (algo encerrado con {}) con definiciones de tipos adentro
  • Se puede obtener un tipo definido dentro de otro tipo usando # (algo así como el . a nivel de tipos)
trait HigherKindedExampleImpl extends HigherKindedExample[   
    type F[Y] = Either[String, Y]
     ]

Juntándolo todo:

*

scala> type X = {
    type F[Y] = Either[String, Y]
}

scala> val x: X#F[Int] = Right(1)
x: Either[String,Int] = Right(1)

scala> :k -v X#F
* -> *

Type Lambdas

Truco: Usar tipos estructurales

  • Definir un tipo estructural (algo encerrado con {}) con definiciones de tipos adentro
  • Se puede obtener un tipo definido dentro de otro tipo usando # (algo así como el . a nivel de tipos)
trait HigherKindedExampleImpl extends HigherKindedExample[  {
    type F[Y] = Either[String, Y]
}    ]

Juntándolo todo:

*

scala> type X = {
    type F[Y] = Either[String, Y]
}

scala> val x: X#F[Int] = Right(1)
x: Either[String,Int] = Right(1)

scala> :k -v X#F
* -> *

Type Lambdas

Truco: Usar tipos estructurales

  • Definir un tipo estructural (algo encerrado con {}) con definiciones de tipos adentro
  • Se puede obtener un tipo definido dentro de otro tipo usando # (algo así como el . a nivel de tipos)
trait HigherKindedExampleImpl extends HigherKindedExample[ ({
    type F[Y] = Either[String, Y]
})#F ]

Juntándolo todo:

*

scala> type X = {
    type F[Y] = Either[String, Y]
}

scala> val x: X#F[Int] = Right(1)
x: Either[String,Int] = Right(1)

scala> :k -v X#F
* -> *

Type Lambdas

trait HigherKindedExampleImpl extends HigherKindedExample[ ({ type F[Y] = Either[String, Y] })#F ]

Usualmente ponen todo en una misma línea:

Type Lambdas

trait HigherKindedExampleImpl extends HigherKindedExample[ ({ type λ[α] = Either[String, α] })#λ ]

... y usualmente utilizan letras griegas:

Type Lambdas

... y usualmente utilizan letras griegas:

Type Lambdas

trait HigherKindedExampleImpl extends HigherKindedExample[ ({ type λ[α] = Either[String, α] })#λ ]

abstrayendo comportamientos

Type classes

Nada que ver con Clases de Programación Orientada a Objetos

No son una construcción del lenguaje. Mas bien son un patrón que utiliza distintas partes del lenguaje.

Motivación:

  • Polimorfismo
  • Desacoplamiento

Inspiradas en las Type Classes de Haskell

Type classes

Polimorfismo

Type classes

La misma  operación funcionando en distintos  tipos

Polimorfismo

Subtipado

Un mismo nombre para multiples instancias que estan relacionadas por una misma superclase.

Type classes

La misma  operación funcionando en distintos  tipos

Polimorfismo

Subtipado

La misma  operación funcionando en distintos  tipos

trait Persona {
    def numeroIdentificacion: String
}
case class PersonaNatural extends Persona
case class PersonaJuridica extends Persona

Type classes

Polimorfismo

Polimorfismo Paramétrico

La misma operación funcionando en distintos tipos, pero ignorando la diferencia entre los tipos

Subtipado

trait Persona {
    def numeroIdentificacion: String
}
case class PersonaNatural extends Persona
case class PersonaJuridica extends Persona

Type classes

La misma  operación funcionando en distintos  tipos

Polimorfismo

Polimorfismo Paramétrico

Subtipado

trait Persona {
    def numeroIdentificacion: String
}
case class PersonaNatural extends Persona
case class PersonaJuridica extends Persona
def invertirLista[A](xs: List[A]): List[A]

class Queue[A] {
    ...
}

Type classes

La misma  operación funcionando en distintos  tipos

Polimorfismo

Polimorfismo Ad hoc

Polimorfismo Paramétrico

La misma operación funcionando en distintos tipos, haciendo algo distinto para cada tipo. Por ejemplo overloading

Subtipado

trait Persona {
    def numeroIdentificacion: String
}
case class PersonaNatural extends Persona
case class PersonaJuridica extends Persona
def invertirLista[A](xs: List[A]): List[A]

class Queue[A] {
    ...
}

Type classes

La misma  operación funcionando en distintos  tipos

Polimorfismo

Polimorfismo Paramétrico

Subtipado

trait Persona {
    def numeroIdentificacion: String
}
case class PersonaNatural extends Persona
case class PersonaJuridica extends Persona

Type Classes

def invertirLista[A](xs: List[A]): List[A]

class Queue[A] {
    ...
}

Polimorfismo Ad hoc

Type classes

La misma  operación funcionando en distintos  tipos

Consisten de:

  • Un nombre
  • Uno o mas parámetros de tipo (Cosas encerradas en [_])
  • Un conjunto de declaraciones de funciones (en las que deben aparecer los tipos sea como parámetros o como tipos de retorno) 

(Pueden haber funciones implementadas en función de otras sin implementar)

Type classes

trait Ord[T] {
    def lessOrEq(a: T, b: T): Boolean
}
object intOrd extends Ord[Int] {
    def lessOrEq(a: Int, b: Int): Boolean = a <= b
}

Un ejemplo:

Una instancia:

Si existe una instancia de la type class TC para un tipo T se suele decir "T está en la type class TC". 

Por ejemplo, dado que definimos una instancia de Ord para Int podemos decir que Int está en la type class Ord

Type classes

(un paréntesis)

Para ciertas Type Classes tiene sentido enumerar un conjunto de reglas que toda instancia debería respetar para que tenga sentido.

Ej con Ord:

∀ a,b   ∈ A | lessOrEq(a,b) || lessOrEq(b,a)
∀ a,b,c ∈ A | if lessOrEq(a,b) && lessOrEq(b,c) then lessOrEq(a,c)

Es usual que estas propiedades se comprueben con tests que generan valores aleatorios para un tipo A específico.

La librería estándar tiene una type class similar para comparar elementos de un tipo y es la que se usa al llamar métodos como sorted: scala.math.Ordering 

Type classes

def sort[A](xs: List[A])(ordA: Ord[A]): List[A] = ...

Con esto podríamos abstraer ciertas cosas:

sort(List(5, 8, 10, 9))(intOrd)
sort(List(4.3, 7.8, 1.9, 0.5))(doubleOrd)
sort(List("def" ,"zxy", "abc"))(stringOrd)

Pasar la instancia explícitamente a los métodos que lo necesitan puede ser cansón

¡Parámetros implícitos!

Y podríamos utilizarla con tipos para los que hayamos definido una forma de comparar los elementos:

Type classes

def sort[A](xs: List[A])(ordA: Ord[A]): List[A] = ...

Con esto podríamos abstraer ciertas cosas:

Scope

>
 

Entorno de variables definidas:

Scope

> val x = 1

Entorno de variables definidas:

x : Int

Scope

> val x = 1
> val y = "hola!"

Entorno de variables definidas:

x : Int
y : String

Scope

> val x = 1
> val y = "hola!"
> x + z
> error: not found: value z
x : Int
y : String

Entorno de variables definidas:

Scope implícito

> val x = 1
> val y = "hola!"
>
x : Int
y : String

Entorno de variables nombradas:

Entorno de variables implícitas:

 

Scope implícito

> val x = 1
> val y = "hola!"
> implicit val w = "blabla"
x : Int
y : String
w : String

Entorno de variables nombradas:

Entorno de variables implícitas:

String => "blabla"

Scope implícito

def saludar(nombre: String)(implicit saludo: String): String = {
    s"$saludo, $nombre"
}
> implicit val saludoEnEspañol = "Hola"
> saludar("Laura")
res0: String = Hola, Laura

Una función puede tener parámetros implícitos:

La función se puede invocar sin especificar los parámetros implícitos siempre que en el scope implícito haya uno de ese tipo:

Scope implícito

> saludar("Laura")("Hi")
res0: String = Hi, Laura

Las funciones con parámetros implícitos se pueden llamar como cualquier otra función pasando explícitamente el valor de los parámetros implícitos:

Relación con Type classes

object Ord {
    implicit object intOrd extends Ord[Int] {
        def lessOrEq(a: Int, b: Int): Boolean = a <= b
    }

    implicit object stringOrd extends Ord[String] {
        def lessOrEq(a: String, b: String): Boolean = {
            (a compareTo b) <= 0
        }
    }

    def sort[A](xs: Array[A])(implicit ord: Ord[A]): Array[A] = ...
}

Para facilitar el uso de type classes se pueden usar implicits:

import Ord._
sort(List(5, 8, 10, 9))
sort(List("def" ,"zxy", "abc"))

​Equivalente al anterior. Notación que sirve para type classes de un solo parámetro.

Para facilitar el uso de type classes se pueden usar implicits:

import Ord._
sort(List(5, 8, 10, 9))
sort(List("def" ,"zxy", "abc"))

Relación con Type classes

object Ord {
    implicit object intOrd extends Ord[Int] {
        def lessOrEq(a: Int, b: Int): Boolean = a <= b
    }

    implicit object stringOrd extends Ord[String] {
        def lessOrEq(a: String, b: String): Boolean = {
            (a compareTo b) <= 0
        }
    }

    def sort[A: Ord](xs: Array[A]): Array[A] = ...
}

Scope implícito

Una forma de extraer valores del scope implícito:

def implicitly[T](implicit e: T): T = e

implicitly es una función, del preludio estándar, que recibe un tipo T y devuelve el valor implícito de valor T que se encuentre dentro del scope ímplicito actual.

Scope implícito

scala> implicit val x = "Hola!"
x: String = Hola!

scala> val y = implicitly[String]
y: String = Hola!

scala> y == "Hola!"
res0: Boolean = true
scala> implicitly[Int]
<console>:0: error: could not find implicit value for parameter e: Int
              implicitly[Int]

Ejemplo:

Si no se ha definido ninguno, falla con un error de compilación:

Scope implícito

scala> implicit val x = "Hola!"
x: String = Hola!

scala> implicit val y = "Chao!"
y: String = Chao!

scala> implicitly[String]
<console>:10: error: ambiguous implicit values:
 both value x of type => String
 and value y of type => String
 match expected type String
              implicitly[String]
                        ^

Si en el scope actual se han definido 2 o mas valores implícitos del mismo tipo, (y ambos son igual de específicos) entonces falla con un error de compilación:

Scope implícito

Los implícitos se pueden propagar automáticamente entre métodos:

def compare[A](x: A, y: A)(implicit ord: Ord[A]): Boolean = {
    ord.lessOrEq(x,y)
}

Un método que lo invoque debería proveer el argumento ord:

def min[A](x: A, y: A)(implicit ord: Ord[A]): Boolean = {
    if(compare(x,y)) {
        x
    } else {
        y
    }
}

Scope implícito

Los implícitos se pueden propagar automáticamente entre métodos:

def compare[A](x: A, y: A)(implicit ord: Ord[A]): Boolean = {
    ord.lessOrEq(x,y)
}

Un método que lo invoque debería proveer el argumento ord:

def min[A: Ord](x: A, y: A): Boolean = {
    if(compare(x,y)) {
        x
    } else {
        y
    }
}

Scope implícito

Los implicitos se pueden "componer" automáticamente:

case class Foo(a: String, b: Int)
case class Bar(m: String)
 

Entorno de variables implícitas:

Scope implícito

Los implicitos se pueden "componer" automáticamente:

case class Foo(a: String, b: Int)
case class Bar(m: String)

implicit def toBar(implicit foo: Foo): Bar = {
    Bar(s"${foo.a} -> ${foo.b}")
}
toBar: Foo => Bar 

Entorno de variables implícitas:

Scope implícito

Los implicitos se pueden "componer" automáticamente:

case class Foo(a: String, b: Int)
case class Bar(m: String)

implicit def toBar(implicit foo: Foo): Bar = {
    Bar(s"${foo.a} -> ${foo.b}")
}
toBar: Foo => Bar 

Entorno de variables implícitas:

Funciones en el scope implícito se denominan conversiones implícitas

Scope implícito

Los implicitos se pueden "componer" automáticamente:

case class Foo(a: String, b: Int)
case class Bar(m: String)

implicit def toBar(implicit foo: Foo): Bar = {
    Bar(s"${foo.a} -> ${foo.b}")
}

def printBar(implicit bar: Bar) = {
    println(s"bar -> $bar")
}
toBar: Foo => Bar 

Entorno de variables implícitas:

Scope implícito

Los implicitos se pueden "componer" automáticamente:

case class Foo(a: String, b: Int)
case class Bar(m: String)

implicit def toBar(implicit foo: Foo): Bar = {
    Bar(s"${foo.a} -> ${foo.b}")
}

def printBar(implicit bar: Bar) = {
    println(s"bar -> $bar")
}

scala> implicit val foo = Foo("hola", 1)
foo: Foo = Foo(hola,1)
toBar: Foo => Bar 
foo: Foo

Entorno de variables implícitas:

Scope implícito

Los implicitos se pueden "componer" automáticamente:

case class Foo(a: String, b: Int)
case class Bar(m: String)

implicit def toBar(implicit foo: Foo): Bar = {
    Bar(s"${foo.a} -> ${foo.b}")
}

def printBar(implicit bar: Bar) = {
    println(s"bar -> $bar")
}

scala> implicit val foo = Foo("hola", 1)
foo: Foo = Foo(hola,1)

scala> printBar
toBar: Foo => Bar 
foo: Foo

Entorno de variables implícitas:

Scope implícito

Los implicitos se pueden "componer" automáticamente:

case class Foo(a: String, b: Int)
case class Bar(m: String)

implicit def toBar(implicit foo: Foo): Bar = {
    Bar(s"${foo.a} -> ${foo.b}")
}

def printBar(implicit bar: Bar) = {
    println(s"bar -> $bar")
}

scala> implicit val foo = Foo("hola", 1)
foo: Foo = Foo(hola,1)

scala> printBar
bar -> Bar(hola -> 1)
toBar: Foo => Bar 
foo: Foo

Entorno de variables implícitas:

Es mejor evitarlos.

Son confusos. Es mejor ser explícito.

Algunos usos sensatos:

  • Cuando se usan con type classes
  • ExecutionContext

Casi nunca.

  • Slick's Session

¿Cuando es adecuado usar implicits?

Muchas librerías los usan, razonablemente. Y por eso vale la pena saber como funcionan

Ejemplo

Ejemplo

/**
 * Generic json value
 */
sealed trait JsValue { ... }

case object JsNull extends JsValue

case class JsBoolean(value: Boolean) extends JsValue

case class JsNumber(value: BigDecimal) extends JsValue

case class JsString(value: String) extends JsValue

case class JsArray(value: Seq[JsValue] = List()) extends JsValue { 
    ...
}

case class JsObject(private val underlying: Map[String, JsValue]) extends JsValue {
    ... 
}

Serialización JSON en Play-JSON (similar en Spray-JSON) 

Un objeto JSON es representado en Scala con un ADT:

Una representación canónica de un valor JSON

trait JsonConvertible {
    def toJsValue: JsValue
}

Una solución usando interfaces:

Ejemplo

trait JsonConvertible {
    def toJsValue: JsValue
}

class Persona(name: String, age: Int) extends JsonConvertible {
    def toJsValue = {
        JsObject(Map(
            "name" -> JsString( name ),
            "age"  -> JsNumber( age )
        ))
    }
}
        

Una solución usando interfaces:

Ejemplo

trait JsonConvertible {
    def toJsValue: JsValue
}

class Persona(name: String, age: Int) extends JsonConvertible {
    def toJsValue = {
        JsObject(Map(
            "name" -> JsString( name ),
            "age"  -> JsNumber( age )
        ))
    }
}
        

Una solución usando interfaces:

Problemas:

  • Acoplamiento: Mala separación de responsabilidades
  • ¿Cómo manejar múltiples formas de serializar un mismo objeto?
  • ¿Cómo manejar clases externas que no controlamos? ¿Extenderlas e implementar la interface?

Ejemplo

Separar en una type class la responsabilidad de convertir a JsValue.

trait Writes[A] {
  /**
   * Convert the object into a JsValue
   */
  def writes(o: A): JsValue

  ...
}

Una mejor solución

case class Ciudad(codigo: String, nombre: String)

Play JSON

object CiudadWrites extends Writes[Ciudad] {
    def write(c: Ciudad): JsValue = {
        JsObject(
            "codigo" -> JsString( c.codigo ),
            "nombre" -> JsString( c.nombre )
        )
    }
}

Es una mejora pero igual hay algo de trabajo manual.

Play JSON

Play Json, a partir de macros, puede derivar automáticamente el Writes de una clase, dado que en el scope implícito haya un Writes para cada uno de los tipos de atributos de la clase.

Play JSON

Dentro de Play existen Writes para los tipos básicos de Scala: String, Int, Double, Float, Boolean, etc..

Play Json, a partir de macros, puede derivar automáticamente el Writes de una clase, dado que en el scope implícito haya un Writes para cada uno de los tipos de atributos de la clase.

Play JSON

Dentro de Play existen Writes para los tipos básicos de Scala: String, Int, Double, Float, Boolean, etc..

import play.api.libs.json._

Play Json, a partir de macros, puede derivar automáticamente el Writes de una clase, dado que en el scope implícito haya un Writes para cada uno de los tipos de atributos de la clase.

Play JSON

Dentro de Play existen Writes para los tipos básicos de Scala: String, Int, Double, Float, Boolean, etc..

Json.writes[MiCaseClass]

El mecanismo de macro se invoca llamando:

y devuelve un Writes[MiCaseClass]

Play Json, a partir de macros, puede derivar automáticamente el Writes de una clase, dado que en el scope implícito haya un Writes para cada uno de los tipos de atributos de la clase.

case class Ciudad(codigo: String, nombre: String)
case class Persona(nombre: String, ciudadResidencia: Ciudad)

Play JSON

Dentro de Play existen Writes para los tipos básicos de Scala: String, Int, Double, Float, Boolean, etc..

Json.writes[MiCaseClass]

El mecanismo de macro se invoca llamando:

y devuelve un Writes[MiCaseClass]

Play Json, a partir de macros, puede derivar automáticamente el Writes de una clase, dado que en el scope implícito haya un Writes para cada uno de los tipos de atributos de la clase.

case class Ciudad(codigo: String, nombre: String)
case class Persona(nombre: String, ciudadResidencia: Ciudad)

Play JSON

case class Ciudad(codigo: String, nombre: String)
case class Persona(nombre: String, ciudadResidencia: Ciudad)
import play.api.libs.json.Json

scala> Json.writes[Persona]

Play JSON

case class Ciudad(codigo: String, nombre: String)
case class Persona(nombre: String, ciudadResidencia: Ciudad)
import play.api.libs.json.Json

scala> Json.writes[Persona]
<console>:13: error: No implicit writes for Ciudad available.
              Json.writes[Persona]

Play JSON

case class Ciudad(codigo: String, nombre: String)
case class Persona(nombre: String, ciudadResidencia: Ciudad)
import play.api.libs.json.Json

scala> Json.writes[Persona]
<console>:13: error: No implicit writes for Ciudad available.
              Json.writes[Persona]

scala> implicit val ciudadWrites = Json.writes[Ciudad]

Play JSON

case class Ciudad(codigo: String, nombre: String)
case class Persona(nombre: String, ciudadResidencia: Ciudad)
import play.api.libs.json.Json

scala> Json.writes[Persona]
<console>:13: error: No implicit writes for Ciudad available.
              Json.writes[Persona]

scala> implicit val ciudadWrites = Json.writes[Ciudad]

scala> Json.writes[Persona]
res1: play.api.libs.json.OWrites$$anon$1@2113b9b1

Play JSON

case class Ciudad(codigo: String, nombre: String)
case class Persona(nombre: String, ciudades: Array[Ciudad])

Play JSON

Haciendo un cambio:

... sigue funcionando:

import play.api.libs.json.Json

scala> implicit val ciudadWrites = Json.writes[Ciudad]

scala> Json.writes[Persona]
res1: play.api.libs.json.OWrites$$anon$1@2113b9b1

Pero no fue necesario definir un Writes[Array[Ciudad]] explícitamente

Bastó con un Writes[Ciudad]

Writes[A] => Writes[Array[A]]

Una conversión implícita:

¿Por qué funcionó esto?

Derivando Type classes con implicits

implicit def arrWrites[A](implicit wrt: Writes[A]): Writes[Array[A]] = {
    
    
    
    ???
    
    
}
implicit def arrWrites[A](implicit wrt: Writes[A]): Writes[Array[A]] = {
    new Writes[Array[A]] {
        def writes(as: Array[A]): JsValue = ???



    }
}

Derivando Type classes con implicits

¿Cuál es el subtipo de JsValue que corresponde a un arreglo de elementos?

implicit def arrWrites[A](implicit wrt: Writes[A]): Writes[Array[A]] = {
    new Writes[Array[A]] {
        def writes(as: Array[A]): JsValue = ???



    }
}

Derivando Type classes con implicits

¿Cuál es el subtipo de JsValue que corresponde a un arreglo de elementos?

JsArray

implicit def arrWrites[A](implicit wrt: Writes[A]): Writes[Array[A]] = {
    new Writes[Array[A]] {
        def writes(as: Array[A]): JsValue = ???



    }
}

Derivando Type classes con implicits

¿Cuál es el subtipo de JsValue que corresponde a un arreglo de elementos?

JsArray

implicit def arrWrites[A](implicit wrt: Writes[A]): Writes[Array[A]] = {
    new Writes[Array[A]] {
        def writes(as: Array[A]): JsValue = {
            val jsvals: Seq[JsValue] = ???
            JsArray( jsvals )
        }
    }
}

Derivando Type classes con implicits

Teniendo un Writes de A's podemos convertir en un JsValue cada elemento del arreglo de entrada:

implicit def arrWrites[A](implicit wrt: Writes[A]): Writes[Array[A]] = {
    new Writes[Array[A]] {
        def writes(as: Array[A]): JsValue = {
            val jsvals: Seq[JsValue] = ???
            JsArray( jsvals )
        }
    }
}

Derivando Type classes con implicits

implicit def arrWrites[A](implicit wrt: Writes[A]): Writes[Array[A]] = {
    new Writes[Array[A]] {
        def writes(as: Array[A]): JsValue = {
            val jsvals: Seq[JsValue] = as.map(a => wrt.write(a)).toSeq
            JsArray( jsvals )
        }
    }
}

Teniendo un Writes de A's podemos convertir en un JsValue cada elemento del arreglo de entrada:

Derivando Type classes con implicits

implicit def arrWrites[A](implicit wrt: Writes[A]): Writes[Array[A]] = {
    new Writes[Array[A]] {
        def writes(as: Array[A]): JsValue = {
            val jsvals: Seq[JsValue] = as.map(a => wrt.write(a)).toSeq
            JsArray( jsvals )
        }
    }
}

Teniendo un Writes de A's podemos convertir en un JsValue cada elemento del arreglo de entrada:

Derivando Type classes con implicits

type safety

Extensibilidad

Si no existe una instancia para el tipo no pasa el chequeo de tipos.

  • Agregar nuevas implementaciones para el mismo tipo
  • Agregar nuevos tipos con implementaciones del typeclass

Ventajas de type classes

  • Extensión retroactiva: permiten simular la inclusión de nuevos métodos dentro de una clase sin modificar su código fuente

Composición

Ventajas de type classes

Posibilidad de derivar nuevas instancias a partir de existentes

Desacoplamiento

No requieren implementar las funcionalidades en el cuerpo de las clases 

Promueven única responsabilidad

Bien diseñada una type class solo se ocupa de modelar un comportamiento muy específico

Type classes en librerias

Slick

Usar tipos ricos en Scala, que tienen su equivalente en la base de datos

Type classes en librerias

Slick

Usar tipos ricos en Scala, que tienen su equivalente en la base de datos

Type classes en librerias

Slick

Usar tipos ricos en Scala, que tienen su equivalente en la base de datos

Type classes en librerias

Play framework

Interpretar Query params

Type classes en librerias

Play framework

Interpretar Query params

Type classes en librerias

scalaz

Patrones de programación funcional

Ejercicios

Una forma de generalizar la noción de sumar cosas como:

1 + 2 == 3             // Para Int
"a" concat "b" == "ab" // Para String
trait Addable[T] {
  def zero: T
  def add(a: T, b: T): T
}

add es una operación que toma dos elementos de tipo T y devuelve otro de tipo T. Por ejemplo en Int puede ser +.

zero es un método que devuelve el elemento que combinado con cualquier elemento T devuelve el mismo elemento. Por ejemplo en Int con la suma ese elemento es 0.

Ejercicios

Dos instancias de ejemplo son:

implicit object IntAddable extends Addable[Int] {
    override def zero: Int = 0
    override def add(a: Int, b: Int): Int = a+b
}
implicit object StringAddable extends Addable[String] {
    override def zero: String = ""
    override def add(a: String, b: String): String = a concat b
}

Ejercicios

1.1. Definir un método addAll que reciba una lista de T's en el  typeclass Addable y que devuelva la "sumatoria" de todos los elementos.

Por ejemplo al invocarla con:

addAll( List(4,10,7,5) )

... debería arrojar el mismo resultado que:

0 + 4 + 10 + 7 + 5

La firma debería ser:

def addAll[T](ts: List[T])(implicit addable: Addable[T]): T

Ejercicios

1.2. Defina una función implícita que reciba implícitamente 2 Addables y devuelva un Addable de tuplas de 2 elementos donde el primer elemento es del tipo del primer Addable y el segundo elemento es del tipo del segundo Addable.

 

En otras palabras una función con esta firma:

implicit def product[S,T](implicit addS: Addable[S], addT: Addable[T]): Addable[(S,T)]

Compruebe que la siguiente llamada compila y devuelve el resultado esperado, sin tener que definir explícitamente un Addable[(Int,String)]:

addAll( List((1,"a"), (2,"b"), (3,"c")) )

Patrón pimp my class

Agregando funcionalidades dónde no las hay

Una forma de "agregar" nuevos métodos a una clase que ya ha sido definida.

Puede ser útil para agregar métodos utilitarios a librerías externas

Aprovecha el mecanismo de conversiones implícitas

Patrón pimp my class

¿Cómo funciona el siguiente código?

import scala.concurrent.duration._

val d = 5.seconds

Patrón pimp my class

¿Cómo funciona el siguiente código?

import scala.concurrent.duration._

val d = 5.seconds

La clase scala.Int no tiene un método seconds 

Patrón pimp my class

¿Cómo funciona el siguiente código?

import scala.concurrent.duration._

val d = 5.seconds

La clase scala.Int no tiene un método seconds 

Patrón pimp my class

¡Una conversión implícita a una clase que sí lo tenga y que utilice el entero!

Patrón pimp my class

package object duration {
    implicit class DurationInt(private val n: Int) {
        ...
        def seconds      = durationIn(SECONDS)
        def second       = seconds
        ...
    }
}

Patrón pimp my class

package object duration {
    implicit class DurationInt(private val n: Int) {
        ...
        def seconds      = durationIn(SECONDS)
        def second       = seconds
        ...
    }
}

...alternativamente:

package object duration {
    implicit def toDurationInt(n: Int) = new DurationInt(n)

    class DurationInt(private val n: Int) {
        ...
        def seconds      = durationIn(SECONDS)
        def second       = seconds
        ...
    }
}

Con type classes

Recuperando el estilo de Programación Orientada a Objetos

trait Ord[T] {
    def lessOrEq(a: T, b: T): Boolean
}

implicit object stringOrd extends Ord[String] {
    def lessOrEq(a: String, b: String): Boolean = (a compareTo b) <= 0
}

stringOrd.lessOrEq("hola", "chao")

Con type classes

Recuperando el estilo de Programación Orientada a Objetos

trait Ord[T] {
    def lessOrEq(a: T, b: T): Boolean
}

implicit object stringOrd extends Ord[String] {
    def lessOrEq(a: String, b: String): Boolean = (a compareTo b) <= 0
}

stringOrd.lessOrEq("hola", "chao")

Los typeclasses promueven un estilo de invocación raro

Con type classes

Recuperando el estilo de Programación Orientada a Objetos

class OrdOps[T](a: T)(implicit ord: Ord[T]) {
    def lessOrEq(b: T): Boolean = ord.lessOrEq(a,b)
}

Definir una clase de ayuda y una conversión implícita:

Con type classes

Recuperando el estilo de Programación Orientada a Objetos

class OrdOps[T](a: T)(implicit ord: Ord[T]) {
    def lessOrEq(b: T): Boolean = ord.lessOrEq(a,b)
}

implicit def toOrdOps[T: Ord](t: T) = new OrdOps(t)

Definir una clase de ayuda y una conversión implícita:

Con type classes

Recuperando el estilo de Programación Orientada a Objetos

class OrdOps[T](a: T)(implicit ord: Ord[T]) {
    def lessOrEq(b: T): Boolean = ord.lessOrEq(a,b)
}

implicit def toOrdOps[T: Ord](t: T) = new OrdOps(t)

"hola".lessOrEq("chao")
"hola" lessOrEq "chao"

Definir una clase de ayuda y una conversión implícita:

Con type classes

Mucho trabajo manual y repetitivo al definir typeclasses:

Escribir código que permita llamar un método llamado  ago sobre objetos de tipo  FiniteDuration

scala> 1.minute.ago
res21: org.joda.time.DateTime = 2015-10-14T12:27:54.236-05:00

scala> 3.hours.ago
res23: org.joda.time.DateTime = 2015-10-14T09:29:14.483-05:00

scala> DateTime.now
res24: org.joda.time.DateTime = 2015-10-14T12:29:24.371-05:00

Patrón pimp my class

Al final se debe poder escribir código como el siguiente:

Ejercicio 2.1

  • Usando la clase DateTime de JodaTime
  • DateTime.now() devuelve el momento actual
  • El métodos minusMillis de DateTime 

Algunas ayudas:

BonO

importar instancias de type classes y modulos de sintaxis en Scalaz

en Scalaz

¿Cómo traerse las ayudas de sintáxis en una librería como scalaz?

"hola" lte "chao"

Scalaz tiene un type class llamado Order que es más general que nuestro Ord

Dado que uno importe las cosas adecuadas uno puede hacer la siguiente llamada:

en Scalaz

¿Cómo traerse las ayudas de sintáxis en una librería como scalaz?

import scalaz.std.string.stringInstance

1. Traer la instancia del type class

"hola" lte "chao"

en Scalaz

¿Cómo traerse las ayudas de sintáxis en una librería como scalaz?

import scalaz.std.string.stringInstance
implicit object stringInstance extends Monoid[String] with Equal[String] with Order[String] with ...
"hola" lte "chao"

Por lo general en un mismo objeto reunen las instancias de multiples type classes​:

1. Traer la instancia del type class

en Scalaz

¿Cómo traerse las ayudas de sintáxis en una librería como scalaz?

import scalaz.std.string.stringInstance
implicit object stringInstance extends Monoid[String] with Equal[String] with Order[String] with ...
"hola" lte "chao"

Además, no todas las type classes están listadas en la definición de la instancia: Por ejemplo una instancia de Monoid también es una instancia de Semigroup

Por lo general en un mismo objeto reunen las instancias de multiples type classes​:

1. Traer la instancia del type class

en Scalaz

¿Cómo traerse las ayudas de sintáxis en una librería como scalaz?

2. Traerse la conversión implícita para sintáxis:

"hola" lte "chao"
import scalaz.syntax.order.ToOrderOps

Y con ambos imports el siguiente codigo compila:

En últimas, si nada sirve, causando mayor tiempo de compilación:

import scalaz._
import Scalaz._

CONECTANDO TYPE LAMBDAS Y TYPE CLASSES

Juntando hilos

CONECTANDO TYPE LAMBDAS Y TYPE CLASSES

Un Mappable es algo que tiene una operación  map que ejecuta una función dentro del contexto de un "contenedor"  F[_]

trait Mappable[F[_]] {
    def map[A,B](fa: F[A])(f: A => B): F[B]
}

Por ejemplo:

  • List
  • Option
  • Future

CONECTANDO TYPE LAMBDAS Y TYPE CLASSES

Una implementación de ejemplo:

object ListMappable extends Mappable[List] {
    def map[A,B](la: List[A])(f: A => B): List[B] = {
        la.map(f)
    }
}

La implementación solo llama el método map definido dentro de la clase List

CONECTANDO TYPE LAMBDAS Y TYPE CLASSES

Reto: Definir una instancia de Mappable para los Either que a su izquierda tienen un String y que transforman el valor a la derecha

CONECTANDO TYPE LAMBDAS Y TYPE CLASSES

Reto: Definir una instancia de Mappable para los Either que a su izquierda tienen un String y que transforman el valor a la derecha

Una instancia de Mappable necesita un constructor de tipos de un solo parámetro

CONECTANDO TYPE LAMBDAS Y TYPE CLASSES

Una instancia de Mappable necesita un constructor de tipos de un solo parámetro

Reto: Definir una instancia de Mappable para los Either que a su izquierda tienen un String y que transforman el valor a la derecha

Either es un constructor de tipos de dos parámetros

CONECTANDO TYPE LAMBDAS Y TYPE CLASSES

Either es un constructor de tipos de dos parámetros

¿Cómo aplicamos parcialmente un constructor de tipos?

Reto: Definir una instancia de Mappable para los Either que a su izquierda tienen un String y que transforman el valor a la derecha

Una instancia de Mappable necesita un constructor de tipos de un solo parámetro

CONECTANDO TYPE LAMBDAS Y TYPE CLASSES

¿Cómo aplicamos parcialmente un constructor de tipos?

bla,bla,bla...

Reto: Definir una instancia de Mappable para los Either que a su izquierda tienen un String y que transforman el valor a la derecha

Una instancia de Mappable necesita un constructor de tipos de un solo parámetro

Either es un constructor de tipos de dos parámetros

CONECTANDO TYPE LAMBDAS Y TYPE CLASSES

Usando el Either de la librería estándar:

object EitherWithLeftStringMappable extends Mappable[Either[String,?]] {
    def map[A,B](either: Either[String,A])(f: A => B): Either[String,B] = {
        either.right.map(f)
    }
}

CONECTANDO TYPE LAMBDAS Y TYPE CLASSES

def eitherMappable[L] = new Mappable[Either[L,?]] {
    def map[A,B](either: Either[L,A])(f: A => B): Either[L,B] = {
        either.right.map(f)
    }
}

Si queremos que el tipo a izquierda no sea String sino cualquiera lo podemos volver un parámetro:

object EitherWithLeftStringMappable extends Mappable[Either[String,?]] {
    def map[A,B](either: Either[String,A])(f: A => B): Either[String,B] = {
        either.right.map(f)
    }
}

Usando el Either de la librería estándar:

Ejercicios

3.1. Definir un  Mappable para las Listas de Options:

3.2. Generalizar el patrón de componer un Mappable dentro de otro Mappable para crear un tercero. Debe ser un método definido dentro de la  type class Mappable que reciba otro Mappable y devuelva la composición:

scala> ListOfOptionsMappable.map( List(Some(1),None,Some(3)) )( _ + 1 )
res0: List[Option[Int]] = List(Some(2), None, Some(4))
trait Mappable[F[_]] { self =>
    def map[A,B](fa: F[A])(f: A => B): F[B]
    def compose[G[_]](implicit other: Mappable[G]): Mappable[???] = ???
}

3.3. Utilizar la función definida en el ejercicio 2 para implementar el Mappable del ejercicio 1

Fin

Fuentes y enlaces adicionales

Higher Kinded Types

Fuentes y enlaces adicionales

Type Classes e Implicits

Ilustraciones

“But I don’t want to go among mad people," Alice remarked.
"Oh, you can’t help that," said the Cat: "we’re all mad here. I’m mad. You’re mad."
"How do you know I’m mad?" said Alice.
"You must be," said the Cat, "or you wouldn’t have come here.” 
― Lewis CarrollAlice in Wonderland

Higher Kinded Types, Type Lambdas y Typeclasses

By Miguel Vilá

Higher Kinded Types, Type Lambdas y Typeclasses

  • 2,832