Last active
July 25, 2023 12:37
-
-
Save nzpr/f0a28e2517cce0ecf4810a05878110c7 to your computer and use it in GitHub Desktop.
Type classes
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
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 |
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
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