All Downloads are FREE. Search and download functionalities are using the official Maven repository.

libretto.examples.CoffeeMachine.scala Maven / Gradle / Ivy

The newest version!
package libretto.examples

import libretto.scaletto.StarterApp
import libretto.scaletto.StarterKit.scalettoLib.given
import scala.concurrent.duration.*

object CoffeeMachine extends StarterApp { app =>

  type CoffeeMachine = Rec[[CoffeeMachine] =>>
    (EspressoMenu |*| CoffeeMachine) |&|
    (LatteMenu    |*| CoffeeMachine) |&|
    Done
  ]

  type EspressoMenu =
    ShotCountChoice |*|
    Val[Beverage]       // `Val[A]` means a Scala value of type `A`
                        // flowing in the positive direction (left-to-right).
                        // Here it means that the coffee machine will *produce* a value of type Beverage.

  type LatteMenu =
    LatteOptions |*|
    Val[Beverage]

  type LatteOptions =
    SizeChoice      |*|
    ShotCountChoice |*|
    FlavorChoice

  // `Neg[A]` means a Scala value of type `A` flowing in the negative direction (right-to-left).
  // Here it means that the coffee machine is *asking for* input of type `A`.
  type ShotCountChoice = Neg[ShotCount]
  type SizeChoice      = Neg[Size]
  type FlavorChoice    = Neg[Option[Flavor]]

  enum ShotCount { case Single, Double       }
  enum Size      { case Small, Medium, Large }
  enum Flavor    { case Vanilla, Cinnamon    }

  case class Beverage(description: String)

  type LatteParams = (Size, ShotCount, Option[Flavor])

  override def blueprint: Done -⚬ Done =
    makeCoffeeMachine > useCoffeeMachine

  def espresso(shots: ShotCount): Beverage =
    Beverage("Espresso" + (shots match {
      case ShotCount.Double => " doppio"
      case ShotCount.Single => ""
    }))

  def latte(params: LatteParams): Beverage = {
    val (size, shots, flavor) = params
    val flavorStr = flavor.map(_.toString.toLowerCase + " ").getOrElse("")
    val shotsStr = shots match {
      case ShotCount.Double => " with an extra shot"
      case ShotCount.Single => ""
    }
    Beverage(s"$size ${flavorStr}latte$shotsStr")
  }

  object CoffeeMachine {
    // Hides one level of recursive definition of CoffeeMachine.
    // It is just `pack` from the DSL applied to a type argument, in order to help type inference.
    def pack: (
      (EspressoMenu |*| CoffeeMachine) |&|
      (LatteMenu    |*| CoffeeMachine) |&|
      Done
    ) -⚬ CoffeeMachine =
      app.pack[[X] =>> (EspressoMenu |*| X) |&| (LatteMenu |*| X) |&| Done]
  }

  val makeCoffeeMachine: Done -⚬ CoffeeMachine = rec { self =>
    val beverage: Done -⚬ (
      (EspressoMenu |*| CoffeeMachine) |&|
      (LatteMenu    |*| CoffeeMachine)
    ) =
      choice(
        onEspresso > snd(self),
        onLatte    > snd(self),
      )

    val end: Done -⚬ Done =
      id[Done]

    choice(beverage, end) > CoffeeMachine.pack
  }

  def onEspresso: Done -⚬ (EspressoMenu |*| Done) =
    id                                       [                                           Done  ]
      .>(introFst(promise[ShotCount]))    .to[ (Neg[ShotCount]  |*|  Val[ShotCount]) |*| Done  ]
      .>(assocLR)                         .to[  Neg[ShotCount]  |*| (Val[ShotCount]  |*| Done) ]
      .>(snd(makeBeverage(espresso)))     .to[  Neg[ShotCount]  |*| (Val[Beverage]   |*| Done) ]
                                          .to[  ShotCountChoice |*| (Val[Beverage]   |*| Done) ]
      .>(assocRL)                         .to[ (ShotCountChoice |*|  Val[Beverage])  |*| Done  ]
                                          .to[             EspressoMenu              |*| Done  ]

  def onLatte: Done -⚬ (LatteMenu |*| Done) =
    id                                       [                                                 Done  ]
      .>(introFst(promise[LatteParams]))  .to[ (Neg[LatteParams]    |*|  Val[LatteParams]) |*| Done  ]
      .>(assocLR)                         .to[  Neg[LatteParams]    |*| (Val[LatteParams]  |*| Done) ]
      .>(fst(collectLatteParams))         .to[    LatteOptions      |*| (Val[LatteParams]  |*| Done) ]
      .>(snd(makeBeverage(latte)))        .to[    LatteOptions      |*| (Val[Beverage]     |*| Done) ]
      .>(assocRL)                         .to[   (LatteOptions      |*|  Val[Beverage])    |*| Done  ]
                                          .to[                   LatteMenu                 |*| Done  ]

  /**
   * ```
   * ┏━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━┓
   * ┞─────────┐      ╎               ╎                       ┞─────────────┐
   * ╎Val[Spec]│→┄┄┐  ╎               ╎                   ┌┄┄→╎Val[Beverage]│
   * ┟─────────┘   ┆  ├─────────┐     ├─────────────┐     ┆   ┟─────────────┘
   * ┃             ├┄→╎Val[Spec]│→┄┄┄→╎Val[Beverage]│→┄┄┄→┤   ┨
   * ┞─────────┐   ┆  ├─────────┘     ├─────────────┘     ┆   ┞─────────────┐
   * ╎  Done   │→┄┄┘  ╎               ╎                   └┄┄→╎    Done     │
   * ┟─────────┘      ╎               ╎                       ┟─────────────┘
   * ┗━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━┛
   *
   * ```
   *
   * The [[Done]] on the in-port signals readiness to make this beverage.
   * The [[Done]] on the out-port signals this beverage has been made.
   */
  def makeBeverage[Spec](
    f: Spec => Beverage,
  ): (Val[Spec] |*| Done) -⚬ (Val[Beverage] |*| Done) =
    awaitPosSnd > mapVal(f) > signalPosSnd

  def collectLatteParams: Neg[LatteParams] -⚬ LatteOptions =
    id                                                           [                       LatteOptions                      ]
                                                            .from[ (Neg[Size] |*| Neg[ShotCount]) |*| Neg[Option[Flavor]]  ]
      ./<.fst(liftNegPair)                                  .from[ Neg[ (Size   ,     ShotCount)] |*| Neg[Option[Flavor]]  ]
      ./<(liftNegPair)                                      .from[ Neg[((Size   ,     ShotCount)   ,      Option[Flavor])] ]
      ./<(contramapNeg { case ((a, b), c) => (a, b, c) })   .from[ Neg[( Size   ,     ShotCount    ,      Option[Flavor])] ]
                                                            .from[ Neg[              LatteParams                         ] ]

  val useCoffeeMachine: CoffeeMachine -⚬ Done = {
    def go: (Done |*| CoffeeMachine) -⚬ Done = rec { go =>
      snd(unpack) > mainMenu(go)
    }

    introFst(done) > go
  }

  def mainMenu(
    repeat: (Done |*| CoffeeMachine) -⚬ Done,
  ): (Done |*| (((EspressoMenu |*| CoffeeMachine) |&| (LatteMenu |*| CoffeeMachine)) |&| Done)) -⚬ Done = {

    case class Espresso()
    case class Latte()
    case class Quit()
    type Item = Espresso | Latte | Quit

    val msg =
      """Choose your beverage:
        | e - espresso
        | l - latte
        | q - quit
        |""".stripMargin

    val parse: String => Option[Item] = {
      case "e" => Some(Espresso())
      case "l" => Some(Latte())
      case "q" => Some(Quit())
      case _   => None
    }

    val goEspresso: (Val[Espresso] |*| (EspressoMenu |*| CoffeeMachine)) -⚬ Done = fst(neglect) > VI(getEspresso) > repeat
    val goLatte:    (Val[Latte   ] |*| (LatteMenu    |*| CoffeeMachine)) -⚬ Done = fst(neglect) > VI(getLatte   ) > repeat
    val quit:       (Val[Quit    ] |*|              Done               ) -⚬ Done = fst(neglect) > join

    λ { case start |*| menu =>
      switch(start |> prompt(msg, parse))
        .Case[Espresso] { e => goEspresso(e |*| chooseL(chooseL(menu))) }
        .Case[Latte]    { l => goLatte   (l |*| chooseR(chooseL(menu))) }
        .Case[Quit]     { q => quit      (q |*| chooseR(menu))          }
        .endswitch
    }
  }

  def getEspresso: (Done |*| EspressoMenu) -⚬ Done =
    id                                 [  Done |*|             EspressoMenu            ]
                                    .to[  Done |*| (ShotCountChoice |*| Val[Beverage]) ]
      .>(assocRL)                   .to[ (Done |*| ShotCountChoice) |*| Val[Beverage]  ]
      .>(fst(promptShot))           .to[       Done                 |*| Val[Beverage]  ]
      .>(joinMap(id, serve))        .to[                           Done                ]

  def getLatte: (Done |*| LatteMenu) -⚬ Done =
    id                               [ Done |*|                                               LatteMenu                 ]
                                  .to[ Done |*| (((SizeChoice |*| ShotCountChoice) |*| FlavorChoice) |*| Val[Beverage]) ]
      .>(snd(assocLR > assocLR))  .to[ Done |*| (SizeChoice |*| (ShotCountChoice |*| (FlavorChoice |*| Val[Beverage]))) ]
      .>(VI(promptSize))          .to[      Done            |*| (ShotCountChoice |*| (FlavorChoice |*| Val[Beverage]))  ]
      .>(VI(promptShot))          .to[                      Done                 |*| (FlavorChoice |*| Val[Beverage])   ]
      .>(VI(promptFlavor))        .to[                                           Done              |*| Val[Beverage]    ]
      .>(joinMap(id, serve))      .to[                                                            Done                  ]

  def promptShot: (Done |*| ShotCountChoice) -⚬ Done = {
    val msg =
      """Choose strength:
        | s - single espresso shot
        | d - double espresso shot
        |""".stripMargin

    val parse: String => Option[ShotCount] = {
      case "s" => Some(ShotCount.Single)
      case "d" => Some(ShotCount.Double)
      case _   => None
    }

    id[Done |*| ShotCountChoice]    .to[    Done        |*| Neg[ShotCount] ]
      .>(fst(prompt(msg, parse)))   .to[ Val[ShotCount] |*| Neg[ShotCount] ]
      .>(fulfillAndSignal)          .to[                Done               ]
  }

  def promptSize: (Done |*| SizeChoice) -⚬ Done = {
    val msg =
      """Choose your size:
        | s - small
        | m - medium
        | l - large
        |""".stripMargin

    val parse: String => Option[Size] = {
      case "s" => Some(Size.Small)
      case "m" => Some(Size.Medium)
      case "l" => Some(Size.Large)
      case _   => None
    }

    id[Done |*| SizeChoice]         .to[    Done   |*| Neg[Size] ]
      .>(fst(prompt(msg, parse)))   .to[ Val[Size] |*| Neg[Size] ]
      .>(fulfillAndSignal)          .to[           Done          ]
  }

  def promptFlavor: (Done |*| FlavorChoice) -⚬ Done = {
    val msg =
      """Do you want to add extra flavor to your latte?
        | v - vanilla
        | c - cinnamon
        | n - no extra flavor
        |""".stripMargin

    val parse: String => Option[Option[Flavor]] = {
      case "v" => Some(Some(Flavor.Vanilla))
      case "c" => Some(Some(Flavor.Cinnamon))
      case "n" => Some(None)
      case _   => None
    }

    id[Done |*| FlavorChoice]       .to[    Done             |*| Neg[Option[Flavor]] ]
      .>(fst(prompt(msg, parse)))   .to[ Val[Option[Flavor]] |*| Neg[Option[Flavor]] ]
      .>(fulfillAndSignal)          .to[                     Done                    ]
  }

  def prompt[A](msg: String, parse: String => Option[A]): Done -⚬ Val[A] =
    rec { tryAgain =>
      printLine(msg)
        > readLine
        > mapVal { s => parse(s).toRight(()) }
        > liftEither
        > either(neglect > tryAgain, id)
    }

  def fulfillAndSignal[A]: (Val[A] |*| Neg[A]) -⚬ Done =
    ΛI(signalPosFst) > elimSnd(fulfill)

  def serve: Val[Beverage] -⚬ Done = {
    val dot: Done -⚬ Done = putStr(".") > delay(500.millis)
    val etc: Done -⚬ Done = dot > dot > dot > printLine("")

    delayVal(etc)
      .>(mapVal((b: Beverage) => s"☕ Here goes your ${b.description}."))
      .>(printLine)
      .>(etc)
      .>(printLine(""))
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy