Logging without Tradeoffs

Logging without Tradeoffs

This is a little detour into the world of logging in functional
programming. In this talk we will discuss journalling, timeliness,
memory & resource usage; laugh at tutorials that scream "WRITER" then
demonstrate it with a buggy piece of code that should never go to
production; and finally build up a small library of code for logging
that isn't horribly crippled.

This was presented at ScalaSyd, May 2015.

42d9867a0fee0fa6de6534e9df0f1e9b?s=128

Mark Hibberd

May 06, 2015
Tweet

Transcript

  1. Logging Without Tradeoffs

  2. define: logging

  3. Logging is the cutting, skidding, on-site processing, and loading of

    trees or logs onto trucks or skeleton cars. In forestry, the term logging is sometimes used in a narrow sense concerning the logistics of moving wood from the stump to somewhere outside the forest, usually a sawmill or a lumber yard. Wikipedia - On Logging! (i.e. can’t be wrong) (disclaimer: could actually be wrong)
  4. … logistics of moving wood from the stump to somewhere

    outside the forest, usually a sawmill or a lumber yard. Wikipedia - On Logging! (i.e. can’t be wrong) (disclaimer: could actually be wrong)
  5. Sawmill

  6. Sawmill Lumber Yard

  7. how?

  8. &1

  9. an example

  10. None
  11. None
  12. ! 1 case class Stock(name: String) ! ! ! !

    ! ! ! ! ! ! ! !
  13. ! 1 case class Stock(name: String) 2 3 sealed trait

    Order 4 case class Buy(stock: Stock, qty: Long) extends Order 5 case class Sell(stock: Stock, qty: Long) extends Order 6 case object Hold extends Order ! ! ! ! ! ! ! !
  14. ! 1 case class Stock(name: String) 2 3 sealed trait

    Order 4 case class Buy(stock: Stock, qty: Long) extends Order 5 case class Sell(stock: Stock, qty: Long) extends Order 6 case object Hold extends Order 7 8 case class Tick(time: Timestamp, stock: Stock, cents: Long) ! ! ! ! ! !
  15. ! 1 case class Stock(name: String) 2 3 sealed trait

    Order 4 case class Buy(stock: Stock, qty: Long) extends Order 5 case class Sell(stock: Stock, qty: Long) extends Order 6 case object Hold extends Order 7 8 case class Tick(time: Timestamp, stock: Stock, cents: Long) 9 10 case class Quanity(stock: String, qty: Long) \ ! ! !
  16. ! 1 case class Stock(name: String) 2 3 sealed trait

    Order 4 case class Buy(stock: Stock, qty: Long) extends Order 5 case class Sell(stock: Stock, qty: Long) extends Order 6 case object Hold extends Order 7 8 case class Tick(time: Timestamp, stock: Stock, cents: Long) 9 10 case class Quanity(stock: String, qty: Long) 11 12 case class Budget(cash: Long, stocks: List[Quanitity]) ! !
  17. ! 1 case class Stock(name: String) 2 3 sealed trait

    Order 4 case class Buy(stock: Stock, qty: Long) extends Order 5 case class Sell(stock: Stock, qty: Long) extends Order 6 case object Hold extends Order 7 8 case class Tick(time: Timestamp, stock: Stock, cents: Long) 9 10 case class Quanity(stock: String, qty: Long) 11 12 case class Budget(cash: Long, stocks: List[Quantity]) 13 14 case class Decision(model: Model, budget: Budget, order: Order)
  18. 1 def bot(exchange: Exchange, model: Model, budget: Budget): Error \/

    Unit = 2 exchange.next match { 3 case None => 4 5 case Some(ticks) => 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 }
  19. 1 def bot(exchange: Exchange, model: Model, budget: Budget): Error \/

    Unit = 2 exchange.next match { 3 case None => 4 Disconnected(exchange).left 5 case Some(ticks) => 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 }
  20. 1 def bot(exchange: Exchange, model: Model, budget: Budget): Error \/

    Unit = 2 exchange.next match { 3 case None => 4 Disconnected(exchange).left 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 8 9 10 11 12 13 14 15 16 17 18 19 20 }
  21. 1 def bot(exchange: Exchange, model: Model, budget: Budget): Error \/

    Unit = 2 exchange.next match { 3 case None => 4 Disconnected(exchange).left 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 10 case None => 11 12 13 14 15 16 17 18 19 } 20 }
  22. 1 def bot(exchange: Exchange, model: Model, budget: Budget): Error \/

    Unit = 2 exchange.next match { 3 case None => 4 Disconnected(exchange).left 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 ProgrammersTryingToLoseYourMoney(decision, guard).left 10 case None => 11 12 13 14 15 16 17 18 19 } 20 }
  23. 1 def bot(exchange: Exchange, model: Model, budget: Budget): Error \/

    Unit = 2 exchange.next match { 3 case None => 4 Disconnected(exchange).left 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 ProgrammersTryingToLoseYourMoney(decision, guard).left 10 case None => 11 exchange.trade(decision.order, decision.budget) match { 12 case Banned(reason) => 13 14 case TradeRejected => 15 16 case TradeAccepted(budget) => 17 18 } 19 } 20 }
  24. 1 def bot(exchange: Exchange, model: Model, budget: Budget): Error \/

    Unit = 2 exchange.next match { 3 case None => 4 Disconnected(exchange).left 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 ProgrammersTryingToLoseYourMoney(decision, guard).left 10 case None => 11 exchange.trade(decision.order, decision.budget) match { 12 case Banned(reason) => 13 BannedFor(decision, reason).left 14 case TradeRejected => 15 16 case TradeAccepted(budget) => 17 18 } 19 } 20 }
  25. 1 def bot(exchange: Exchange, model: Model, budget: Budget): Error \/

    Unit = 2 exchange.next match { 3 case None => 4 Disconnected(exchange).left 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 ProgrammersTryingToLoseYourMoney(decision, guard).left 10 case None => 11 exchange.trade(decision.order, decision.budget) match { 12 case Banned(reason) => 13 BannedFor(decision, reason).left 14 case TradeRejected => 15 bot(exchange, decision.model, decision.budget) 16 case TradeAccepted(budget) => 17 18 } 19 } 20 }
  26. 1 def bot(exchange: Exchange, model: Model, budget: Budget): Error \/

    Unit = 2 exchange.next match { 3 case None => 4 Disconnected(exchange).left 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 ProgrammersTryingToLoseYourMoney(decision, guard).left 10 case None => 11 exchange.trade(decision.order, decision.budget) match { 12 case Banned(reason) => 13 BannedFor(decision, reason).left 14 case TradeRejected => 15 bot(exchange, decision.model, decision.budget) 16 case TradeAccepted(budget) => 17 bot(exchange, decision.model, budget) 18 } 19 } 20 }
  27. ! 1 def decide(seed: Model, ticks: List[Tick], budget: Budget): Decision

    = { 2 3 4 5 6 7 8 9 10 }
  28. ! 1 def decide(seed: Model, ticks: List[Tick], budget: Budget): Decision

    = { 2 val model = seed.update(ticks, budget) 3 4 5 6 7 8 9 10 }
  29. ! 1 def decide(seed: Model, ticks: List[Tick], budget: Budget): Decision

    = { 2 val model = seed.update(ticks, budget) 3 val predictions = model.predictions 4 5 6 7 8 9 10 }
  30. ! 1 def decide(seed: Model, ticks: List[Tick], budget: Budget): Decision

    = { 2 val model = seed.update(ticks, budget) 3 val predictions = model.predictions 4 val offer = 5 predictions 6 .sortBy(p => -1 * p.confidence * p.score) 7 .map(_.offer) 8 .head 9 10 }
  31. ! 1 def decide(seed: Model, ticks: List[Tick], budget: Budget): Decision

    = { 2 val model = seed.update(ticks, budget) 3 val predictions = model.predictions 4 val offer = 5 predictions 6 .sortBy(p => -1 * p.confidence * p.score) 7 .map(_.offer) 8 .head 9 Decision(model, budget, offer) 10 }
  32. fp much?

  33. ! 1 def bot(exchange: Exchange, model: Model, budget: Budget): Error

    \/ Unit = 2 exchange.next match { 3 case None => 4 Disconnected(exchange).left 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 ProgrammersTryingToLoseYourMoney(decision, guard).left 10 case None => 11 exchange.trade(decision.order, decision.budget) match { 12 case Banned(reason) => 13 BannedFor(decision, reason).left 14 case TradeRejected => 15 bot(exchange, decision.model, decision.budget) 16 case TradeAccepted(budget) => 17 bot(exchange, decision.model, budget) 18 } 19 } 20 }
  34. ! 1 def bot(exchange: Exchange, model: Model, budget: Budget): EitherT[IO,

    Error, Unit] = 2 exchange.next.flatMap({ 3 case None => 4 EitherT.left(Diconnected(exchange)) 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 EitherT.left(ProgrammersTryingToLoseYourMoney(decision, guard)) 10 case None => 11 exchange.trade(decision.order, decision.budget) match { 12 case Banned(reason) => 13 EitherT.left(BannedFor(decision, reason)) 14 case TradeRejected => 15 bot(exchange, decision.model, decision.budget) 16 case TradeAccepted(budget) => 17 bot(exchange, decision.model, budget) 18 } 19 } 20 })
  35. None
  36. ! Error \/ Unit ! EitherT[IO, Error, Unit]

  37. ! Error \/ Unit ! EitherT[IO, Error, Unit] ! IO[Error

    \/ Unit]
  38. so logging…

  39. ! 1 def bot(exchange: Exchange, model: Model, budget: Budget): EitherT[IO,

    Error, Unit] = 2 exchange.next.flatMap({ 3 case None => 4 EitherT.left(Diconnected(exchange)) 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 EitherT.left(ProgrammersTryingToLoseYourMoney(decision, guard)) 10 case None => 11 exchange.trade(decision.order, decision.budget) match { 12 case Banned(reason) => 13 EitherT.left(BannedFor(decision, reason)) 14 case TradeRejected => 15 bot(exchange, decision.model, decision.budget) 16 case TradeAccepted(budget) => 17 bot(exchange, decision.model, budget) 18 } 19 } 20 })
  40. ! 1 def bot(exchange: Exchange, model: Model, budget: Budget): EitherT[IO,

    Error, Unit] = 2 exchange.next.flatMap({ 3 case None => 4 EitherT.left(Diconnected(exchange)) 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 EitherT.left(ProgrammersTryingToLoseYourMoney(decision, guard)) 10 case None => 11 exchange.trade(decision.order, decision.budget) match { 12 case Banned(reason) => 13 EitherT.left(BannedFor(decision, reason)) 14 case TradeRejected => 15 bot(exchange, decision.model, decision.budget) 16 case TradeAccepted(budget) => 17 bot(exchange, decision.model, budget) 18 } 19 } 20 }) ! avoid it
  41. ! 1 sealed trait Error 2 3 case class BannedFor(

    4 decision: Decision 5 , reason: Reason 6 ) extends Error 7 8 case object Disconnected 9 extends Error 10 11 case class ProgrammersTryingToLoseYourMoney( 12 decision: Decision 13 , check: Check 14 ) extends Error !
  42. when you can’t?

  43. None
  44. (,)

  45. ! 1 def decide(seed: Model, ticks: List[Tick], budget: Budget): Decision

    = { 2 val model = seed.update(ticks, budget) 3 val predictions = model.predictions 4 val offer = 5 predictions 6 .sortBy(p => -1 * p.confidence * p.score) 7 .map(_.offer) 8 .head 9 Decision(model, budget, offer) 10 }
  46. ! 1 def decide(s: Model, t: List[Tick], b: Budget): Writer[Log,

    Decision] = { 2 val model = seed.update(ticks, budget) 3 val predictions = model.predictions 4 val offer = 5 predictions 6 .sortBy(p => -1 * p.confidence * p.score) 7 .map(_.offer) 8 .head 9 Decision(model, budget, offer) <++ (“all predictions: ” + predictions) 10 }
  47. ! 1 def bot(exchange: Exchange, model: Model, budget: Budget): EitherT[IO,

    Error, Unit] = 2 exchange.next.flatMap({ 3 case None => 4 EitherT.left(Diconnected(exchange)) 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 EitherT.left(ProgrammersTryingToLoseYourMoney(decision, guard)) 10 case None => 11 exchange.trade(decision.order, decision.budget) match { 12 case Banned(reason) => 13 EitherT.left(BannedFor(decision, reason)) 14 case TradeRejected => 15 bot(exchange, decision.model, decision.budget) 16 case TradeAccepted(budget) => 17 bot(exchange, decision.model, budget) 18 } 19 } 20 })
  48. ! 1 def bot(exchange: Exchange, model: Model, budget: Budget): EitherT[IO,

    Error, Unit] = 2 exchange.next.flatMap({ 3 case None => 4 EitherT.left(Diconnected(exchange)) 5 case Some(ticks) => 6 val decision = decide(model, ticks, budget) 7 check(decision) match { 8 case Some(guard) => 9 EitherT.left(ProgrammersTryingToLoseYourMoney(decision, guard)) 10 case None => 11 exchange.trade(decision.order, decision.budget) match { 12 case Banned(reason) => 13 EitherT.left(BannedFor(decision, reason)) 14 case TradeRejected => 15 bot(exchange, decision.model, decision.budget) 16 case TradeAccepted(budget) => 17 bot(exchange, decision.model, budget) 18 } 19 } 20 }) ! fail at it
  49. 2 def bot(exchange: Exchange, model: Model, budget: Budget): EitherT[IO, Error,

    Unit] = 3 exchange.next.flatMap({ 4 case None => 5 EitherT.left(Diconnected(exchange)) 6 case Some(ticks) => 7 val decision = decide(model, ticks, budget) 8 println("got ticks: " + ticks.size) 9 check(decision) match { 10 case Some(guard) => 11 EitherT.left(ProgrammersTryingToLoseYourMoney(decision, guard)) 12 case None => 13 exchange.trade(decision.order, decision.budget) match { 14 case Banned(reason) => 15 EitherT.left(BannedFor(decision, reason)) 16 case TradeRejected => 17 println("trade-rejected: " + decision) 18 bot(exchange, decision.model, decision.budget) 19 case TradeAccepted(budget) => 20 bot(exchange, decision.model, budget) 21 } 22 } 23 }) ! fail at it
  50. 2 def bot(exchange: Exchange, model: Model, budget: Budget): EitherT[IO, Error,

    Unit] = 3 exchange.next.flatMap({ 4 case None => 5 EitherT.left(Diconnected(exchange)) 6 case Some(ticks) => 7 val decision = decide(model, ticks, budget) 8 putStrLn("got ticks: " + ticks.size).liftIO >> ( 9 check(decision) match { 10 case Some(guard) => 11 EitherT.left(ProgrammersTryingToLoseYourMoney(decision, guard)) 12 case None => 13 exchange.trade(decision.order, decision.budget) match { 14 case Banned(reason) => 15 EitherT.left(BannedFor(decision, reason)) 16 case TradeRejected => 17 putStrLn("trade-rejected: " + decision).liftIO >> 18 bot(exchange, decision.model, decision.budget) 19 case TradeAccepted(budget) => 20 bot(exchange, decision.model, budget) 21 } 22 }) 23 }) ! fail at it (again)
  51. after some time… we realise the need to pass arguments

    to functions
  52. and pass functions as arguments…

  53. 3 case class Trace(trace: String => IO[Unit]) !

  54. ! def bot( e: Exchange , m: Model , b:

    Budget , t: Trace ): EitherT[IO, Error, Unit]
  55. one weird trick…

  56. ! def bot( e: Exchange , m: Model , b:

    Budget , t: Trace ): EitherT[IO, Error, Unit]
  57. ! type App[A] = ReaderT[EitherT[IO, _], Trace[IO], A] ! def

    bot( e: Exchange , m: Model , b: Budget ): App[IO, Error, Unit]
  58. but really… passing arguments is cool

  59. ! def bot( e: Exchange , m: Model , b:

    Budget , t: Trace ): EitherT[IO, Error, Unit]
  60. 2 def bot(e: Exchange, m: Model, b: Budget, t: Trace):

    EitherT[IO, Error, Unit] 3 exchange.next.flatMap({ 4 case None => 5 EitherT.left(Diconnected(exchange)) 6 case Some(ticks) => 7 val decision = decide(model, ticks, budget) 8 t.trace(“got ticks: " + ticks.size).liftIO >> ( 9 check(decision) match { 10 case Some(guard) => 11 EitherT.left(ProgrammersTryingToLoseYourMoney(decision, guard)) 12 case None => 13 exchange.trade(decision.order, decision.budget) match { 14 case Banned(reason) => 15 EitherT.left(BannedFor(decision, reason)) 16 case TradeRejected => 17 t.trace(“trade-rejected: " + decision).liftIO >> 18 bot(exchange, decision.model, decision.budget) 19 case TradeAccepted(budget) => 20 bot(exchange, decision.model, budget) 21 } 22 }) 23 })
  61. this is what most people reach after doing fp (poorly)

    for a while
  62. and this is also the start of my actual talk

  63. ! Error \/ Unit ! EitherT[IO, Error, Unit] ! IO[Error

    \/ Unit]
  64. ! Error \/ Unit ! EitherT[F, Error, Unit] ! F[Error

    \/ Unit]
  65. ! def bot[F[_]: Monad]( e: Exchange[F] , m: Model ,

    b: Budget ): EitherT[F, Error, Unit] ! bot[IO]( real , model , budget ): EitherT[IO, Error, Unit] ! bot[Option]( memory , model , budget ): EitherT[Option, Error, Unit]
  66. ! def bot[F[_]: Monad]( e: Exchange[F] , m: Model ,

    b: Budget ): EitherT[F, Error, Unit] ! bot[IO]( real , model , budget ): IO[Error \/ Unit] ! bot[Option]( memory , model , budget ): Option[Error \/ Unit]
  67. 3 case class Trace(trace: String => IO[Unit]) !

  68. 3 case class Trace[F[_]](trace: String => F[Unit]) !

  69. 3 case class Trace[F[_]](trace: String => F[Unit]) ! ! avoid

    strings
  70. 3 case class Trace[F[_], A](trace: A => F[Unit]) ! !

    avoid strings
  71. 3 case class Trace[F[_], A](trace: A => F[Unit]) ! !

    seen this before?
  72. 3 type Trace[F[_], A] = ReaderT[F, A, Unit] !

  73. 1 sealed trait Log 2 3 case class Received( 4

    ticks: List[Tick] 5 ) extends Error 6 7 case class Reject( 8 decision: Decision 9 ) 10 11 case class Accepted( 12 decision: Decision 13 ) 14 15 case class Prediction ( 16 predictions: NonEmptyList[Prediction] 17 , selected: Offer 18 )
  74. ! def bot[F[_]: Monad]( e: Exchange[F] , m: Model ,

    b: Budget , t: Trace[F, Log] ): EitherT[F, Error, Unit]
  75. ! def bot[F[_]: Monad]( e: Exchange[F] , m: Model ,

    b: Budget , t: Trace[F, Log] ): EitherT[F, Error, Unit]
  76. ! def bot[F[_]: Monad]( e: Exchange[F] , m: Model ,

    b: Budget , t: Trace[EitherT[F, _, ?], Log] ): EitherT[F, Error, Unit]
  77. ! def bot[F[_]: Monad]( e: Exchange[F] , m: Model ,

    b: Budget , t: Trace[EitherT[F, _, ?], Log] ): EitherT[F, Error, Unit]
  78. ! def bot[F[_]: Monad]( e: Exchange[F] , m: Model ,

    b: Budget , t: Trace[EitherT[F, _, ?], Log] ): EitherT[F, Error, Unit]
  79. 2 type Trace[F[_], A] = 3 ReaderT[F, A, Unit] 4

    5 type Traceable[F[_], A, B] = 6 ReaderT[F, Trace[F], A]
  80. ! def bot[F[_]: Monad]( e: Exchange[F] , m: Model ,

    b: Budget ): Traceable[EitherT[F, Error, ?], Log, Unit]
  81. 2 type Trace[F[_], A] = 3 ReaderT[F, A, Unit] 4

    5 type Traceable[F[_], A, B] = 6 ReaderT[F, Trace[F], A] 7 8 type Bot[F[_], A] = 9 Traceable[F[_], Log, A]
  82. ! def bot[F[_]: Monad]( e: Exchange[F] , m: Model ,

    b: Budget ): Bot[EitherT[F, Error, ?], Unit]
  83. ! def decide( seed: Model , ticks: List[Tick] , budget:

    Budget ): Writer[Log, Decision]
  84. ! def decide[F[_]: Monad]( seed: Model , ticks: List[Tick] ,

    budget: Budget ): Bot[F, Decision]
  85. 2 type Trace[F[_], A] = 3 ReaderT[F, A, Unit] 4

    5 type Traceable[F[_], A, B] = 6 ReaderT[F, Trace[F], A] 7 8 type Bot[F[_], A] = 9 Traceable[F[_], Log, A]
  86. None
  87. ! 1 def log[F[_]](message: A): Traceable[F, A, Unit] = 2

    ReaderT(t => t.trace(message))
  88. ! 1 def decide[F[_]](s: Model, t: List[Tick], b: Budget): Bot[F,

    Decision] = { 2 val model = seed.update(ticks, budget) 3 val predictions = model.predictions 4 val offer = 5 predictions 6 .sortBy(p => -1 * p.confidence * p.score) 7 .map(_.offer) 8 .head 9 log(Prediction(prediction, off)).as( 10 Decision(model, budget, offer)) 11 }
  89. 2 def bot(e: Exchange, m: Model, b: Budget): Bot[EitherT[F, Error

    ?], Unit] = 3 exchange.next.flatMap({ 4 case None => 5 EitherT.left(Diconnected(exchange)).lift 6 case Some(ticks) => 7 val decision = decide(model, ticks, budget).lift 8 log(Received(ticks)) >> ( 9 check(decision) match { 10 case Some(guard) => 11 EitherT.left(ProgrammersTryingToLoseYourMoney(decision, guard)).lift 12 case None => 13 exchange.trade(decision.order, decision.budget) match { 14 case Banned(reason) => 15 EitherT.left(BannedFor(decision, reason)).lift 16 case TradeRejected => 17 log(Rejected(decision)) >> 18 bot(exchange, decision.model, decision.budget) 19 case TradeAccepted(budget) => 20 bot(exchange, decision.model, budget) 21 } 22 }) 23 })
  90. 1 2 def writer[F[_]: Applicative, A]: Trace[WriterT[F, Vector[A], ?]] =

    3 Trace(a => WriterT.tell(a)) 4 5 def stream[A](out: java.io.PrintStream, show: A => String) : Trace[IO, A] = 6 Trace(a => IO { out.println(show(a)) }) 7 8 def out[A](show: A => String): Trace[IO, A] = 9 stream(Console.out) 10 11 def err[A](show: A => String): Trace[IO, A] = 12 stream(Console.err) 13 14 def noop[F[_]: Applicative, A]: Trace[F, A] = 15 Trace(_ => ().pure[F]) 16 17 def hoistEither[F[_], A](t: Trace[F, A]): Trace[EitherT[F, _, A]] = 18 ??? 19 20 def io[F[_], A](t: Trace[WriterT[F, Vector[A], ?], A]):Trace[IO, A] = 22 ???
  91. 2 val d1: Decision = 3 decide(seed, ticks, budget).run(noop[Id, Log])

    4 5 val d2: (Vector[Log], Decision) = 6 decide(seed, ticks, budget).run(writer[Log]).run 7 8 val d3: IO[Decision] = 9 decide(seed, ticks, budget).run(out(render)).run
  92. 2 type Trace[F[_], A] = 3 ReaderT[F, A, Unit] 4

    5 type Traceable[F[_], A, B] = 6 ReaderT[F, Trace[F], A] 7 8 type Bot[F[_], A] = 9 Traceable[F[_], Log, A]
  93. 1 2 def writer[F[_]: Applicative, A]: Trace[WriterT[F, Vector[A], ?]] =

    3 Trace(a => WriterT.tell(a)) 4 5 def stream[A](out: java.io.PrintStream, show: A => String) : Trace[IO, A] = 6 Trace(a => IO { out.println(show(a)) }) 7 8 def out[A](show: A => String): Trace[IO, A] = 9 stream(Console.out) 10 11 def err[A](show: A => String): Trace[IO, A] = 12 stream(Console.err) 13 14 def noop[F[_]: Applicative, A]: Trace[F, A] = 15 Trace(_ => ().pure[F]) 16 17 def hoistEither[F[_], A](t: Trace[F, A]): Trace[EitherT[F, _, A]] = 18 ??? 19 20 def io[F[_], A](t: Trace[WriterT[F, Vector[A], ?], A]):Trace[IO, A] = 22 ???
  94. 3 case class Trace[F[_], A]( 4 trace: A => F[Unit]

    5 , hoist: Trace[WriterT[F, Vector[A], ?], A] 6 )
  95. None