Skip to content

Instantly share code, notes, and snippets.

@nzpr
Last active July 25, 2023 12:37
Show Gist options
  • Save nzpr/f0a28e2517cce0ecf4810a05878110c7 to your computer and use it in GitHub Desktop.
Save nzpr/f0a28e2517cce0ecf4810a05878110c7 to your computer and use it in GitHub Desktop.
Type classes
import cats.FlatMap
/**
* 1. You don't need to make assumptions on what is the type of the robot action (that its Either in your case),
* just tell that it is a type constructor. [_] says this T is a higher kinded type - type depending on other type
* Which means it can bear internal state (this is how I understand and interpret it for myself).
*
* 2. Why you want to mix drawing capability with moving capability_
* Better separate concerns and make type class that defines moving behaviour.
*/
trait Moving[T[_]] {
// Type class methods always have an instance of a type as input.
// Which means that type class is a trait that gives some capability to any type.
// And its not a data type that has some capability.
def left(t: T[_], x: Int): T[Unit]
def right(t: T[_], x: Int): T[Unit]
def up(t: T[_], x: Int): T[Unit]
def down(t: T[_], x: Int): T[Unit]
def moveToPosition(t: T[_], x: Int, y: Int): T[Unit]
}
/** And make type class define something that can draw. */
trait Drawing[T[_]] {
def setSize(t: T[_], x: Int): T[Unit]
def setColor(t: T[_], r: Int, g: Int, b: Int): T[Unit]
def startDraw(t: T[_]): T[Unit]
def stopDraw(t: T[_]): T[Unit]
}
/**
* You want your T to have implementation of a FlatMap to perform sequential operations and modify some internal state.
* Its not necessarily Either, but something that can do flatmap.
* Return type says that this function does not return anything, just does something with internal state of T
* and return Unit.
*/
def drawSquare[T[_]: FlatMap](robot: T[_])(tMoving: Moving[T], tDrawing: Drawing[T]): T[Unit] = {
// this import is required to use <- syntax for FlatMap
import cats.syntax.all.*
for {
_ <- tDrawing.setSize(robot, 1)
_ <- tDrawing.setColor(robot, 0, 0, 0)
_ <- tMoving.moveToPosition(robot, 0, 0)
_ <- tDrawing.startDraw(robot)
_ <- tMoving.right(robot, 4)
_ <- tMoving.down(robot, 4)
_ <- tMoving.left(robot, 4)
_ <- tMoving.up(robot, 4)
_ <- tDrawing.stopDraw(robot)
} yield ()
}
/** You can make type classes dependencies implicit which will make caller code using less boilerplate */
def drawSquare1[T[_]: FlatMap](robot: T[_])(implicit tMoving: Moving[T], tDrawing: Drawing[T]): T[Unit] = ???
/** or with dome more syntactic sugar you can make it like this. */
def drawSquare2[T[_]: FlatMap: Moving: Drawing](robot: T[_]): T[Unit] = ???
/**
* After introducing syntax for your type classes you can do this.
* This won't compile here because you need a syntax, we can go over this later to not mix things.
* I think its important to see how code is managed without syntax to not fall into feeling that its the same
* OOP style
*/
//def drawSquare[T[_]: FlatMap: Moving: Drawing](robot: T): T[Unit] = {
// import cats.syntax.all._
// for {
// _ <- robot.setSize(1)
// _ <- robot.setColor(0, 0, 0)
// _ <- robot.moveToPosition(0, 0)
// _ <- robot.startDraw()
// _ <- robot.right(4)
// _ <- robot.down(4)
// _ <- robot.left(4)
// _ <- robot.up(4)
// _ <- robot.stopDraw()
// } yield ()
//}
/**
* So as a result of rewriting the code you have small composable things
* - definition of what it does mean to move
* - definition of what it does mean to draw
* - function defining how to draw square if you give it anything that can draw and move. How exactly draw and move, it
* does not matter.
*/
/**
* This is how you can use the code you've written.
* Implementations of your type classes for EitherT.
*/
object EitherRobot{
// Pick data type for which you have to define type classes,
// which will be the implementation of your robot.
// This is your RobotAction
type T[A] = Either[String, A]
// Implementation for moving capability for your data type
// this implemntation does nothing except
val movingForEither = new Moving[T] {
override def left(t: T[_], x: Int): T[Unit] = t.map(_ => println(s"left $x"))
override def right(t: T[_], x: Int): T[Unit] = t.map(_ => println(s"right $x"))
override def up(t: T[_], x: Int): T[Unit] = t.map(_ => println(s"up $x"))
override def down(t: T[_], x: Int): T[Unit] = t.map(_ => println(s"down $x"))
override def moveToPosition(t: T[_], x: Int, y: Int): T[Unit] = t.map(_ => println(s"moveToPosition $x"))
}
val drawingForEither = new Drawing[T] {
override def setSize(t: T[_], x: Int): T[Unit] = t.map(_ => println(s"setSize $x"))
override def setColor(t: T[_], r: Int, g: Int, b: Int): T[Unit] = t.map(_ => println(s"setColor $r $g $b"))
override def startDraw(t: T[_]): T[Unit] = t.map(_ => println(s"startDraw"))
override def stopDraw(t: T[_]): T[Unit] = t.map(_ => println(s"stopDraw"))
}
val eitherRobot = Right(1L)
}
import EitherRobot._
/**
* Call your logic using some implementation.
* You can go ahead and use this function anywhere, compose it with others not thinking of what is the actual
* implementation fo the behaviour is.
*/
drawSquare[T](eitherRobot)(movingForEither, drawingForEither)
// with syntax this can be just drawSquare[T](eitherRobot)
// if Moving and Drawing implementations are in the scope
// output of this program is
// setSize 1
// setColor 0 0 0
// moveToPosition 0
// startDraw
// right 4
// down 4
// left 4
// up 4
// stopDraw
import cats.{FlatMap, Monad}
import cats.data.EitherT
import cats.effect.IO
/**
* 1. You don't need to make assumptions on what is the type of the robot action (that its Either in your case),
* just tell that it is a type constructor. [_] says this T is a higher kinded type - type depending on other type
* Which means it can bear internal state (this is how I understand and interpret it for myself).
*
* 2. Why you want to mix drawing capability with moving capability_
* Better separate concerns and make type class that defines moving behaviour.
*/
trait Moving[T[_]] {
// Type class methods always have an instance of a type as input.
// Which means that type class is a trait that gives some capability to any type.
// And its not a data type that has some capability.
def left(t: T[_], x: Int): T[Unit]
def right(t: T[_], x: Int): T[Unit]
def up(t: T[_], x: Int): T[Unit]
def down(t: T[_], x: Int): T[Unit]
def moveToPosition(t: T[_], x: Int, y: Int): T[Unit]
}
/** And make type class define something that can draw. */
trait Drawing[T[_]] {
def setSize(t: T[_], x: Int): T[Unit]
def setColor(t: T[_], r: Int, g: Int, b: Int): T[Unit]
def startDraw(t: T[_]): T[Unit]
def stopDraw(t: T[_]): T[Unit]
}
/**
* You want your T to have implementation of a FlatMap to perform sequential operations and modify some internal state.
* Its not necessarily Either, but something that can do flatmap.
* Return type says that this function does not return anything, just does something with internal state of T
* and return Unit.
*/
def drawSquare[T[_]: FlatMap](robot: T[_])(tMoving: Moving[T], tDrawing: Drawing[T]): T[Unit] = {
// this import is required to use <- syntax for FlatMap
import cats.syntax.all._
for {
_ <- tDrawing.setSize(robot, 1)
_ <- tDrawing.setColor(robot, 0, 0, 0)
_ <- tMoving.moveToPosition(robot, 0, 0)
_ <- tDrawing.startDraw(robot)
_ <- tMoving.right(robot, 4)
_ <- tMoving.down(robot, 4)
_ <- tMoving.left(robot, 4)
_ <- tMoving.up(robot, 4)
_ <- tDrawing.stopDraw(robot)
} yield ()
}
/** You can make type classes dependencies implicit which will make caller code using less boilerplate */
def drawSquare1[T[_]: FlatMap](robot: T[_])(implicit tMoving: Moving[T], tDrawing: Drawing[T]): T[Unit] = ???
/** or with dome more syntactic sugar you can make it like this. */
def drawSquare2[T[_]: FlatMap: Moving: Drawing](robot: T[_]): T[Unit] = ???
/**
* After introducing syntax for your type classes you can do this.
* This won't compile here because you need a syntax, we can go over this later to not mix things.
* I think its important to see how code is managed without syntax to not fall into feeling that its the same
* OOP style
*/
//def drawSquare[T[_]: Monad: Moving: Drawing](robot: T): T[Unit] = {
// import cats.syntax.all._
// for {
// _ <- robot.setSize(1)
// _ <- robot.setColor(0, 0, 0)
// _ <- robot.moveToPosition(0, 0)
// _ <- robot.startDraw()
// _ <- robot.right(4)
// _ <- robot.down(4)
// _ <- robot.left(4)
// _ <- robot.up(4)
// _ <- robot.stopDraw()
// } yield ()
//}
/**
* So as a result of rewriting the code you have small composable things
* - definition of what it does mean to move
* - definition of what it does mean to draw
* - function defining how to draw square if you give it anything that can draw and move. How exactly draw and move, it
* does not matter.
*/
/**
* This is how you can use the code you've written.
* Implementations of your type classes for EitherT.
*/
object EitherTRobot{
// Pick data type for which you have to define type classes,
// which will be the implementation of your robot.
// This is your RobotAction
type T[A] = EitherT[IO, String, A]
// Implementation for moving capability for your data type
// this implemntation does nothing except
val movingForEitherT = new Moving[T] {
override def left(t: T[_], x: Int): T[Unit] = t.map(_ => println(s"left $x"))
override def right(t: T[_], x: Int): T[Unit] = t.map(_ => println(s"right $x"))
override def up(t: T[_], x: Int): T[Unit] = t.map(_ => println(s"up $x"))
override def down(t: T[_], x: Int): T[Unit] = t.map(_ => println(s"down $x"))
override def moveToPosition(t: T[_], x: Int, y: Int): T[Unit] = t.map(_ => println(s"moveToPosition $x"))
}
val drawingForEitherT = new Drawing[T] {
override def setSize(t: T[_], x: Int): T[Unit] = t.map(_ => println(s"setSize $x"))
override def setColor(t: T[_], r: Int, g: Int, b: Int): T[Unit] = t.map(_ => println(s"setColor $r $g $b"))
override def startDraw(t: T[_]): T[Unit] = t.map(_ => println(s"startDraw"))
override def stopDraw(t: T[_]): T[Unit] = t.map(_ => println(s"stopDraw"))
}
val eitherTRobot = EitherT.rightT[IO,String](1L)
}
import EitherTRobot._
import cats.effect.unsafe.implicits.global
/**
* Call your logic using some implementation.
* You can go ahead and use this function anywhere, compose it with others not thinking of what is the actual
* implementation fo the behaviour is.
*/
drawSquare[T](eitherTRobot)(movingForEitherT, drawingForEitherT).value.unsafeRunSync()
// output of this program is
// setSize 1
// setColor 0 0 0
// moveToPosition 0
// startDraw
// right 4
// down 4
// left 4
// up 4
// stopDraw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment