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, 2e 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 Foldableistanze implicite per Liste 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 mapReduceripiegherà 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.