Skip to content

Instantly share code, notes, and snippets.

@AlexRogalskiy
Created December 19, 2020 20:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AlexRogalskiy/bba259f5e9bcc74fab3409226856cd7e to your computer and use it in GitHub Desktop.
Save AlexRogalskiy/bba259f5e9bcc74fab3409226856cd7e to your computer and use it in GitHub Desktop.
Scala reader monad mapping
import scala.collection._
import scala.collection.generic._
import scala.concurrent.{ Future, ExecutionContext }
/**
* a Typeclass representing a class that can map and flatMap (collections, Option, Future..).
* effectively, it's a Monad without enforcing the axioms of a Monad.
*/
trait CanMap[A, B, M[_]] {
def map(l: M[A])(f: A => B): M[B]
def flatMap(l: M[A])(f: A => M[B]): M[B]
def pure(v: B): M[B]
}
object CanMap {
/**
* the typeclass for Option
*/
implicit def canmapopt[A, B] = new CanMap[A, B, Option] {
def map(l: Option[A])(f: A => B): Option[B] = l.map(f)
def flatMap(l: Option[A])(f: A => Option[B]): Option[B] = l.flatMap(f)
def pure(v: B): Option[B] = Some(v)
}
/**
* the typeclass for anything that can be transformed to a TraversableLike (almost all the collections)
*/
implicit def canmaptrav[A, B, M[+_]](implicit bf: CanBuildFrom[M[A], B, M[B]], ev: M[A] => TraversableLike[A, M[A]], eb: M[B] => TraversableLike[B, M[B]]) = new CanMap[A, B, M] {
def map(l: M[A])(f: (A) => B): M[B] = l.map(f)
def flatMap(l: M[A])(f: A => M[B]): M[B] = l.flatMap[B, M[B]] { (a: A) =>
f(a)
}
def pure(v: B): M[B] = {
val build = bf()
build += v
build.result
}
}
/**
* the typeclass for Future
*/
implicit def canmapfuture[A, B](implicit ex: ExecutionContext) = new CanMap[A, B, Future] {
def map(l: Future[A])(f: A => B): Future[B] = l.map(f)
def flatMap(l: Future[A])(f: A => Future[B]): Future[B] = l.flatMap(f)
def pure(v: B): Future[B] = Future.successful(v)
}
}
/**
* A monad to abstract dependencies in the code, see https://coderwall.com/p/kh_z5g
*/
object Reader {
/**
* an implicit to convert a function A => B in a Reader[A, B]
*/
implicit def reader[C, R](block: C => R): Reader[C, R] = Reader(block)
/**
* create a reader from an already resolved result
*/
def pure[C, A](a: A) = Reader((c: C) => a)
/**
* transform a sequence of Reader in a Reader of a sequence. I.E. move the Reader out of each item and keep it over the list.
*/
def sequence[C, R, D[_]](list: D[Reader[C, R]])(implicit canMap: CanMap[Reader[C, R], R, D]): Reader[C, D[R]] = reader { conn =>
canMap.map(list) { r: Reader[C, R] =>
r(conn)
}
}
/**
* provide a connection to read the content of a reader
*/
def withConnection[C, R](connection: C)(reader: Reader[C, R]): R = reader(connection)
}
/**
* the Reader Monad in itself. run is the wrapped function. C is the argument type, A is the return type.
* C is contravariant, so that we can have:
*
* trait Connection
* class RealConnection extends Connection
*
* and have Reader[Connection, _] as a subtype of Reader[RealConnection, _] so that the we can use a RealConnection to run a Reader[Connection,_]
*/
case class Reader[-C, +A](run: C => A) {
/**
* shortcut to apply the wrapped function
*/
def apply(c: C) = run(c)
/**
* map a reader's return type to a new return type
*/
def map[B](f: A => B): Reader[C, B] =
Reader(c => f(run(c)))
/**
* flatMap to merge two readers. The type of connection of the second reader has to be a subtype of this reader's connection
* to be able to use the same connection on both readers.
*/
def flatMap[B, D <: C](f: A => Reader[D, B]): Reader[D, B] =
Reader(c => f(run(c))(c))
/**
* Combine two readers
*/
def zip[B, D <: C](other: Reader[D, B]): Reader[D, (A, B)] =
this.flatMap { a =>
other.map { b => (a, b) }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment