Last active
September 7, 2016 03:22
-
-
Save etorreborre/96519c69c045c649ac36eb7e15bdb39c to your computer and use it in GitHub Desktop.
Using Eff on a "real" use case
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* We need to create bidirectional maps for items having both a name an an id | |
* When we collect items, we need to check that there are no duplicated names or ids for a given item | |
*/ | |
// first let's define bi-directional maps | |
case class BiMap[K, V](keys: Map[K, V], values: Map[V, K]) { | |
def add(entry: BiMapEntry[K, V]): BiMap[K, V] = | |
BiMap(keys + (entry.key -> entry.value), values + (entry.value -> entry.key)) | |
def getValue(k: K): Option[V] = | |
keys.get(k) | |
def getKey(v: V): Option[K] = | |
values.get(v) | |
} | |
import cats.std.list._ | |
object BiMap { | |
def apply[K, V](entry: BiMapEntry[K, V]): BiMap[K, V] = | |
BiMap.empty[K, V].add(entry) | |
def empty[K, V]: BiMap[K, V] = | |
BiMap(Map(), Map()) | |
implicit class createBiMapEntry[K](k: K) { | |
def <->[V](v: V): BiMapEntry[K, V] = | |
BiMapEntry[K, V](k, v) | |
} | |
def fromList[K, V](list: List[BiMapEntry[K, V]]) = | |
list.foldMap(e => BiMap.apply[K, V](e)) | |
implicit def MonoidBiMap[K, V]: Monoid[BiMap[K, V]] = new Monoid[BiMap[K, V]] { | |
def empty = BiMap.empty[K, V] | |
def combine(m1: BiMap[K, V], m2: BiMap[K, V]): BiMap[K, V] = | |
BiMap(m1.keys ++ m2.keys, m1.values ++ m2.values) | |
} | |
} | |
case class BiMapEntry[K, V](key: K, value: V) | |
/** | |
* Now a stack of effects | |
*/ | |
import cats.data._ | |
import org.atnos.eff._ | |
import Effects._ | |
/** | |
* Update a bimap with a possible error | |
*/ | |
import cats.std.option._ | |
import cats.syntax.functor._ | |
def updateMap[K](bimap: BM[K], key: K, item: Item): DuplicateError Xor BiMap[K, Item] = | |
Xor.fromOption(bimap.getValue(key).as(DuplicateError(s"$key is already present")), bimap.add(key <-> item)).swap | |
/** | |
* Update the state of the bi-maps | |
*/ | |
import Eff._ | |
import Effects._ | |
import StateEffect._ | |
import DisjunctionEffect._ | |
import cats.syntax.flatMap._ | |
// the effect stack | |
type BM[K] = BiMap[K, Item] | |
// our state will pair 2 bi-maps | |
type BMS = (BM[Name], BM[Id]) | |
type SBM[A] = State[BMS, A] | |
type XorDup[A] = DuplicateError Xor A | |
type S = SBM |: XorDup |: NoEffect | |
// add an item and possibly update the maps | |
def addItem(item: Item): Eff[S, Unit] = | |
for { | |
maps <- get[S, BMS] | |
(nameMap, idMap) = maps | |
updatedNames <- fromXor[S, DuplicateError, BM[Name]](updateMap(nameMap, item.name, item)) | |
updatedIds <- fromXor[S, DuplicateError, BM[Id]] (updateMap(idMap, item.id, item)) | |
_ <- put[S, BMS]((updatedNames, updatedIds)) | |
} yield () | |
/** | |
* traverse a list of items | |
*/ | |
import cats.syntax.traverse._ | |
// note: no type annotation on traverse | |
def makeMaps(as: List[Item]): Eff[S, Unit] = | |
as.traverseU(addItem).as(()) | |
/** | |
* Define a Monoid instance for a pair of Monoids | |
* (I couldn't find this in cats) | |
*/ | |
object Monoidx { | |
implicit def PairMonoid[A : Monoid, B : Monoid]: Monoid[(A, B)] = new Monoid[(A, B)] { | |
def empty = (Monoid[A].empty, Monoid[B].empty) | |
def combine(m1: (A, B), m2: (A, B)) = | |
(Monoid[A].combine(m1._1, m2._1), Monoid[B].combine(m1._2, m2._2)) | |
} | |
} | |
/** | |
* get the results | |
*/ | |
val items = List(Item(Name("name1"), Id("id1")), Item(Name("name2"), Id("id2"))) | |
run(runDisjunctionEither( | |
execZeroFirst( | |
makeMaps(items) | |
) | |
) | |
) must beRight[(BM[Name], BM[Id])] | |
/** | |
* The monad transformers version of that is actually not too bad | |
*/ | |
import cats.std.list._ | |
type S = (BiMap[Name, Item], BiMap[Id, Item]) | |
def updateMap[K](bimap: BM[K], key: K, item: Item): DuplicateError Xor BiMap[K, Item] = | |
Xor.fromOption(bimap.getValue(key).as(DuplicateError(s"$key is already present")), bimap.add(key <-> item)).swap | |
// necessary implicit for StateT | |
implicit def m: Monad[StateT[DuplicateError Xor ?, S, ?]] = | |
StateT.stateTMonadState[DuplicateError Xor ?, S] | |
def addItem(item: Item): StateT[DuplicateError Xor ?, S, Unit] = | |
StateT[DuplicateError Xor ?, S, Unit] { case (names, ids) => | |
for { | |
m1 <- update(names, _.name, item) | |
m2 <- update(ids, _.id, item) | |
} yield ((m1, m2), ()) | |
} | |
// the type annotation is necessary on traverse | |
def makeMap(as: List[Item]): DuplicateError Xor (BiMap[Name, Item], BiMap[Id, Item]) = | |
as.traverse[StateT[DuplicateError Xor ?, S, ?], Unit](addItem).runS((BiMap.empty, BiMap.empty)) | |
/** | |
* For the record we eventually went with a simple foldLeft(Xor.right(BiMap.empty)) { ... } | |
* | |
* Same functionality, but a lot less type annotations headache :-) | |
* | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment