Skip to content

Instantly share code, notes, and snippets.

@noelwelsh
Created May 12, 2017 16:19
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save noelwelsh/18b19947a3e206ae7e19c01b197f8493 to your computer and use it in GitHub Desktop.
Save noelwelsh/18b19947a3e206ae7e19c01b197f8493 to your computer and use it in GitHub Desktop.
Simple website login implemented using the free monad
object Website {
import cats.free.Free
import cats.Comonad
import scala.io._
final case class User(username: String)
sealed trait Page
final case object Welcome extends Page
final case object TryAgain extends Page
type LoginProgram[A] = Free[LoginOp,A]
sealed trait LoginOp[A]
final case class Ask[A](prompt: String, reader: String => A) extends LoginOp[A]
final case class Retry[A,B](ask: LoginProgram[A], f: A => LoginProgram[Option[B]], tries: Int) extends LoginOp[Option[B]]
final case class Login(username: String, password: String) extends LoginOp[Option[User]]
final case class Pure[A](a: A) extends LoginOp[A]
final case class Display(page: Page) extends LoginOp[Unit]
object LoginOp {
def ask[A](prompt: String, reader: String => A) =
Free.liftF[LoginOp,A](Ask(prompt, reader))
def retry[A,B](ask: LoginProgram[A], f: A => LoginProgram[Option[B]], tries: Int) =
Free.liftF[LoginOp,Option[B]](Retry(ask, f, tries))
def login(username: String, password: String) =
Free.liftF[LoginOp,Option[User]](Login(username, password))
def display(page: Page) =
Free.liftF[LoginOp,Unit](Display(page))
implicit object loginOpInstances extends Comonad[LoginOp] {
override def coflatMap[A, B](fa: LoginOp[A])(f: (LoginOp[A]) ⇒ B): LoginOp[B] =
Pure(f(fa))
override def extract[A](x: LoginOp[A]): A =
x match {
case Login(u, p) => (u, p) match {
case ("Noel", "password") => Some(User("noelw"))
case _ => None
}
case Display(p) =>
p match {
case TryAgain =>
println("Sorry, couldn't login you in. Try again!")
case Welcome =>
println("Welcome back!")
}
()
case Ask(p, r) =>
println(p)
r(StdIn.readLine)
case r: Retry[a,b] =>
def loop(counter: Int): Option[b] =
counter match {
case 0 => None
case n => r.f(r.ask.run: a).run match {
case None => loop(n - 1)
case Some(a) => Some(a)
}
}
loop(r.tries)
case Pure(a) => a
}
override def map[A, B](fa: LoginOp[A])(f: (A) ⇒ B): LoginOp[B] =
Pure(f(extract(fa)))
}
}
val loginPrompt = LoginOp.ask("Enter login stuff", (s: String) => {
val split = s.split(" ")
(split(0), split(1))
})
def loginAttempt(details: (String, String)) = {
val (u, p) = details
LoginOp.login(u, p)
}
val attemptLogin = LoginOp.retry(loginPrompt, loginAttempt, 3)
object Example {
val login =
for {
user <- attemptLogin
page <- LoginOp.display(user.fold[Page](TryAgain){ u => Welcome })
} yield page
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment