Skip to content

Instantly share code, notes, and snippets.

@softprops
Last active November 16, 2018 01:52
Show Gist options
  • Save softprops/5981830 to your computer and use it in GitHub Desktop.
Save softprops/5981830 to your computer and use it in GitHub Desktop.
case class Counter(x: Int = 0)
/** A type class for combining types through aggregation */
sealed trait Aggregate[As, Elem] {
def apply(el: Elem): As
def empty: As
def apply(as: As, el: Elem): As
}
/** A companion to Aggregate exposing a single method, `values`
* which produces an arbirary type from the aggregation of elements
*/
object Aggregate {
implicit object IntSetAggregate extends Aggregate[Set[Int], Int] {
def empty = Set.empty[Int]
def apply(el: Int) = Set(el)
def apply(as: Set[Int], el: Int) = as + el
}
implicit object IntCounterAggregate extends Aggregate[Counter, Int] {
def empty = Counter()
def apply(el: Int) = Counter(el)
def apply(as: Counter, el: Int) = as.copy(x = as.x + el)
}
/** @param pairs is a set of keys and values
* @return a Map keyed on the pairs initial value with a value of As
* representing some arbitrary aggregation
*/
def values[K, V, As]
(pairs: Iterable[(K, V)])
(implicit aggr: Aggregate[As, V]) =
(Map.empty[K, As].withDefaultValue(aggr.empty) /: pairs) {
case (m, (k, v)) => m.updated(k, aggr(m(k), v))
}
}
val in = Set((1, 2), (1, 3), (3, 4))
Aggregate.values[Int, Int, Set[Int]](in) // Map(1 -> Set(2, 3), 3 -> Set(4))
Aggregate.values[Int, Int, Counter](in) // Map(1 -> Counter(5), 3 -> Counter(4))
@jedws
Copy link

jedws commented Jul 13, 2013

You basically have a less flexible version of Monoid:

trait Monoid[A] {
  def zero: A
  def append(a1: A, a2: A)
}
object Monoid {
  def apply[A: Monoid] =
    implicitly[Monoid[A]]

then for Int addition:

implicit object IntMonoid extends Monoid[Int] {
  def zero = 0
  def append(a1: Int, a2: Int) = a1 + a2
}

and say for String append:

implicit object StringMonoid extends Monoid[String] {
  def zero = ""
  def append(a1: String, a2: String) = a1 ++ a2
}

Now you can compose, abstracting over the inner Monoid:

implicit def MapMonoid[K, V: Monoid]: Monoid[Map[K, V]] = new Monoid[Map[K, V]] {
  def zero = Map()
  def append(a1: Map[K, V], a2: Map[K, V]) = {
    val M = Monoid[V]
    (a1.keySet ++ a2.keySet).foldLeft(Map.empty[K, V]) {
      case (m, k) => m.updated(k, M.append(a1.get(k).getOrElse(M.zero), a2.get(k).getOrElse(M.zero)))
    }
  }
}

And use it:

Monoid[Map[Int, Int]].append(Map(1 -> 2, 2 -> 3), Map(1 -> 3, 3 -> 4))
   // Map[Int,Int] = Map(1 -> 5, 2 -> 3, 3 -> 4)

Monoid[Map[Int, String]].append(Map(1 -> "2", 2 -> "3"), Map(1 -> "3", 3 -> "4"))
  // Map[Int,String] = Map(1 -> 23, 2 -> 3, 3 -> 4)

@softprops
Copy link
Author

Thanks @jedws. This was an example of the typeclass pattern used as an example to teach a co worker of just that. I know what monoids are :) They weren't needed to explain the typeclass pattern. using a monoid as an example of how to use typeclasses means a i may also have to explain what monoids were which could have been helpful but would also have made the lesson more complicated than I intended. I find it most effective to introduce concepts one at time when teaching others. Adding complexity sometimes muddys the main idea of what what you are trying to teach. A followup on how a concept called monoids can be implemented in terms of typeclasses may have been something like your example. It wasn't necessary to explain monoids in order to explain typeclasses here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment