In algebra, come nella formazione quotidiana dei concetti, le astrazioni sono formate raggruppando le cose in base ad alcune caratteristiche essenziali e omettendo le loro altre caratteristiche specifiche. L'astrazione è unificata sotto un unico simbolo o parola che denota le somiglianze. Diciamo che astraggiamo sulle differenze, ma questo significa davvero che ci stiamo integrando in base alle somiglianze.
Ad esempio, si consideri un programma che prende la somma dei numeri 1
, 2
e 3
:
val sumOfOneTwoThree = 1 + 2 + 3
Questo programma non è molto interessante, poiché non è molto astratto. Possiamo astrarre sui numeri che stiamo sommando, integrando tutti gli elenchi di numeri sotto un unico simbolo ns
:
def sumOf(ns: List[Int]) = ns.foldLeft(0)(_ + _)
E non ci interessa particolarmente nemmeno che sia una lista. List è un costruttore di tipi specifico (prende un tipo e restituisce un tipo), ma possiamo astrarre sul costruttore di tipi specificando quale caratteristica essenziale vogliamo (che può essere piegata):
trait Foldable[F[_]] {
def foldl[A, B](as: F[A], z: B, f: (B, A) => B): B
}
def sumOf[F[_]](ns: F[Int])(implicit ff: Foldable[F]) =
ff.foldl(ns, 0, (x: Int, y: Int) => x + y)
E possiamo avere Foldable
istanze implicite per List
e qualsiasi altra cosa per cui possiamo piegare.
implicit val listFoldable = new Foldable[List] {
def foldl[A, B](as: List[A], z: B, f: (B, A) => B) = as.foldLeft(z)(f)
}
val sumOfOneTwoThree = sumOf(List(1,2,3))
Inoltre, possiamo astrarre sia l'operazione che il tipo di operandi:
trait Monoid[M] {
def zero: M
def add(m1: M, m2: M): M
}
trait Foldable[F[_]] {
def foldl[A, B](as: F[A], z: B, f: (B, A) => B): B
def foldMap[A, B](as: F[A], f: A => B)(implicit m: Monoid[B]): B =
foldl(as, m.zero, (b: B, a: A) => m.add(b, f(a)))
}
def mapReduce[F[_], A, B](as: F[A], f: A => B)
(implicit ff: Foldable[F], m: Monoid[B]) =
ff.foldMap(as, f)
Ora abbiamo qualcosa di abbastanza generale. Il metodo mapReduce
ripiegherà qualsiasi F[A]
dato che possiamo dimostrare che F
è pieghevole e che A
è un monoide o può essere mappato in uno. Per esempio:
case class Sum(value: Int)
case class Product(value: Int)
implicit val sumMonoid = new Monoid[Sum] {
def zero = Sum(0)
def add(a: Sum, b: Sum) = Sum(a.value + b.value)
}
implicit val productMonoid = new Monoid[Product] {
def zero = Product(1)
def add(a: Product, b: Product) = Product(a.value * b.value)
}
val sumOf123 = mapReduce(List(1,2,3), Sum)
val productOf456 = mapReduce(List(4,5,6), Product)
Abbiamo astratto su monoidi e pieghevoli.