Part 1: Monadic Embedded DSLs
interface ILog {
void debug(String debug);
void info(String info);
void warn(String warn);
void error(String message,
Exception ex);
}
interface IKVStore {
String get(String key);
void update(String key,
String value);
void remove(String key);
}
class LoggedStubKVStore implements IKVStore {
private final ILog logger;
public LoggedStubKVStore(ILog log) {
this.logger = log;
}
@Override
public String get(String key) {
logger.debug("tried to get a value at key: " + key);
return "stub value";
}
@Override
public void update(String key, String data) {
logger.debug("tried to set the value at key: " + key + " to " + data);
}
@Override
public void remove(String key) {
logger.debug("tried to remove the value at key: " + key);
}
}
class HashMapKVStore implements IKVStore {
private final ILog logger;
private final HashMap<String, String> hashMap;
public HashMapKVStore(ILog logger, HashMap<String, String> hashMap) {
this.logger = logger;
this.hashMap = hashMap;
}
@Override
public void get(String key) {
logger.debug("tried to get a value at key: " + key);
return hashMap.get(key);
}
@Override
public void update(String key, String data) {
logger.debug("tried to set the value at key: " + key + " to " + data);
hashMap.put(key, data);
}
@Override
public void remove(String key) {
logger.debug("tried to remove the value at key: " + key);
hashMap.remove(key);
}
}
// class RedisKVStore implements IKVStore {
// ...
// }
// class LogstashLogger implements ILog {
// ...
// }
(beware, this is where it starts being Scala)
trait IKVStore[F[_]] {
def get(key: String): F[Option[String]]
def update(key: String, value: String): F[Unit]
def remove(key: String): F[Unit]
}
trait ILog[F[_]] {
def debug(debug: String): F[Unit]
def info(info: String): F[Unit]
def warn(warn: String): F[Unit]
def err(err: String, ex: Exception): F[Unit]
}
object PureMapKVStore {
type MapManipulator[A] = Map[String, String] => (A, Map[String, String])
}
class PureMapKVStore extends IKVStore[MapManipulator] {
override def get(key: String): MapManipulator[Option[String]] =
map => (map.get(key), map)
override def remove(key: String): MapManipulator[Unit] =
map => ((), map.remove(key))
override def update(key: String, data: String): MapManipulator[Option[String]] =
map => ((), map.update(key, data))
}
// class RedisKVStore extends IKVStore[IO] {
// ...
// }
// class LogstashLogger extends ILog[IO] {
// ...
// }
class IO[A] {
def flatMap[B](fun: (B => IO[B])): IO[B] = ???
def unsafePerformIO: A = ???
}
object IO {
def now[A](value: A): IO[A] = ???
def eval[A](fun: () => A): IO[A] = ???
}
pure(a: A): F[A];
bind(fa: F[A], fun: A => F[B]): F[B];
object ExtraKVStoreOps {
def getOrUpdate[F[_]: Monad](store: IKVStore[F],
key: String, newValue: String): F[Option[String]] =
for {
currentValue <- store.get(key)
updated <- currentValue match {
if (optionalResult.isDefined)
Monad[F].pure(optionalResult)
else
store.update(key, newValue)
}
} yield updated
}
trait ILogAlgebra[A] {
def act[F[_]: Monad](logger: ILog[F]): F[A]
}
case class Info(info: String) extends ILogAlgebra[Unit] {
override def act[F[_]: Monad](logger: ILog[F]): F[Unit] =
logger.info(this.info);
}
trait Transformation[F[_], G[_]] {
def transform[A](input: F[A]): G[A]
}
class FreeMonad[F[_], A] {
def run[G[_] : Monad](trans: Transformation[F, G]): G[A] = ???
}
object FreeMonad {
def pure[F[_], A](a: A): FreeMonad[F[_], A] = ???
def ofAlgebra[F[_], A](alg: F[A]): FreeMonad[F[_], A] = ???
}
def toLogInterpreter[F[_]](log: ILog[F]): Transformation[ILogAlgebra, F] =
new Transformation[ILogAlgebra, F] {
override def transform[A](input: ILogAlgebra[A]): F[A] = input.act(log)
}
def runLogProgram[F[_], A](program: FreeMonad[ILogAlgebra, A],
log: ILog[F]): F[A] =
program.run(toLogInterpreter(log))
def getOrUpdateFree(key: String,
newValue: String): FreeMonad[IKVStoreAlgebra, Option[String]] =
for {
currentValue <- FreeMonad.ofAlgebra(Get(key))
updated <- currentValue match {
case Some(valueExists) => FreeMonad.pure(valueExists)
case None => FreeMonad.ofAlgebra(Update(key, newValue))
}
} yield updated
(note we got rid of the F[_] type parameter and IKVStore[F] parameter)
case class Coproduct[L[_], R[_], A](run: Either[L[A], R[A]])
object Coproduct {
def fixParams[L[_], R[_]] = new {
type CoproductLR[A] = Coproduct[L, R, A]
def left[A](la: L[A]): CoproductLR[A] = Coproduct(Left(la))
def right[A](ra: R[A]): CoproductLR[A] = Coproduct(Right(ra))
}
}
type LogOrStoreAlgebra[A] = Coproduct[ILogAlgebra, IKVStoreAlgebra, A]
val LogOrStoreAlgebra = Coproduct.fixParams[ILogAlgebra, IKVStoreAlgebra]
def getAndLogToInfo(key: String): FreeMonad[LogOrStoreAlgebra, Option[String]] =
for {
currentValue <- FreeMonad.ofAlgebra(LogOrStoreAlgebra.right(Get(key)))
logMessage = s"got key $key with value $currentValue"
_ <- FreeMonad.ofAlgebra(LogOrStoreAlgebra.left(Info(logMessage)))
} yield currentValue
type MyEffects = Fx.fx2[IKVStoreAlgebra, ILogAlgebra]
def removeKeyAtKey(key: String): Eff[MyEffects, Option[String]] = for {
keyToRemove <- get[MyEffects](key)
_ <- info[MyEffects]("removing key: " + keyToRemove)
_ <- keyToRemove.traverseA(remove[MyEffects])
} yield keyToRemove