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

libretto.examples.supermarket.SupermarketProvider.scala Maven / Gradle / Ivy

The newest version!
package libretto.examples.supermarket

import libretto.scaletto.StarterKit.*
import libretto.examples.supermarket.baskets.*
import libretto.examples.supermarket.money.*
import scala.concurrent.duration.*

object SupermarketProvider extends SupermarketInterface {

  override val goods: Goods.type = Goods
  import goods.*

  private type BorrowedBasket = Basket |*| -[Basket]
  private type ItemSelection = Beer |&| ToiletPaper
  private type GoodsSupply = Unlimited[ItemSelection]
  private type CoinSink = Unlimited[-[Coin]]

  override opaque type Shopping[ItemsInBasket] =
    ItemsInBasket |*| (BorrowedBasket |*| (GoodsSupply |*| CoinSink))

  override opaque type Supermarket =
    Unlimited[Shopping[One]]

  override given comonoidSupermarket: Comonoid[Supermarket] =
    Unlimited.comonoidUnlimited

  override given basketReadiness[Items]: Signaling.Positive[Shopping[Items]] =
    Signaling.Positive.bySnd(using Signaling.Positive.byFst(using signalingJunctionBorrowedBasket))

  override def enterAndObtainBasket: Supermarket -⚬ Shopping[One] =
    λ { supermarket =>
      val shopping: $[Shopping[One]] =
        Unlimited.single(supermarket)

      val (empty |*| ((basket |*| negBasket) |*| (goodsSupply |*| coinSink))) =
        shopping

      // delay supply of goods until basket is obtained
      val (basket1 |*| goodsSupply1) =
        (basket |*| goodsSupply) |> sequence_PN[Basket, GoodsSupply]

      val (basketNumber |*| basket2) = serialNumber(basket1)
      val basket3 = basket2 waitFor {
        basketNumber |> printLine(n => s"${Console.RED}+1${Console.RESET} basket $n is now in use")
      }

      (empty |*| ((basket3 |*| negBasket) |*| (goodsSupply1 |*| coinSink)))
    }

  override def returnBasketAndLeave: Shopping[One] -⚬ One =
    λ { case (unit |*| ((basket |*| negBasket) |*| (goodsSupply |*| coinSink))) =>
      val (basketNumber |*| basket1) = serialNumber(basket)
      val basket2 = basket1 waitFor {
        basketNumber |> printLine(n => s"${Console.GREEN}-1${Console.RESET} basket $n is now available")
      }
      unit
        .alsoElim(returnBasket(basket2 |*| negBasket))
        .alsoElim(closeSupply(goodsSupply))
        .alsoElim(closeCoinSink(coinSink))
    }

  override def addToiletPaperToBasket[Items]: Shopping[Items] -⚬ Shopping[ToiletPaper |*| Items] =
    addItemToBasket(chooseToiletPaper)

  override def addBeerToBasket[Items]: Shopping[Items] -⚬ Shopping[Beer |*| Items] =
    addItemToBasket(chooseBeer)

  override def payForToiletPaper[Items]: (Coin |*| Shopping[ToiletPaper |*| Items]) -⚬ (ToiletPaper |*| Shopping[Items]) =
    payForItem[ToiletPaper, Items]

  override def payForBeer[Items]: (Coin |*| Shopping[Beer |*| Items]) -⚬ (Beer |*| Shopping[Items]) =
    payForItem[Beer, Items]

  private given signalingJunctionBorrowedBasket: SignalingJunction.Positive[BorrowedBasket] =
    SignalingJunction.Positive.byFst

  private def returnBasket  : (Basket |*| -[Basket]) -⚬ One = backvert
  private def receiveBasket : One -⚬ (-[Basket] |*| Basket) = forevert

  private def chooseBeer        : ItemSelection -⚬ Beer        = chooseL
  private def chooseToiletPaper : ItemSelection -⚬ ToiletPaper = chooseR

  private def closeSupply   : GoodsSupply -⚬ One = Unlimited.discard
  private def closeCoinSink : CoinSink    -⚬ One = Unlimited.discard

  private def sendCoin: (Coin |*| CoinSink) -⚬ CoinSink =
    par(id, Unlimited.getFst) > assocRL > elimFst(money.sendCoin)

  private def supplyItem[Item: SignalingJunction.Positive](
    chooseItem: ItemSelection -⚬ Item,
  ): GoodsSupply -⚬ (Item |*| GoodsSupply) =
    λ { goodsSupply =>
      val (itemSelection |*| goodsSupply1) = Unlimited.getSome(goodsSupply)
      val item: $[Item]                    = chooseItem(itemSelection)
      (item |> delayUsing(randomDelay)) |*| goodsSupply1
    }

  private def randomDelay: Done -⚬ Done =
    constVal(()) > mapVal(_ => (scala.util.Random.nextDouble * 1000 + 100).toInt.millis) > delay

  private def addItemToBasket[Item: SignalingJunction.Positive, Items](
    pick: ItemSelection -⚬ Item,
  ): Shopping[Items] -⚬ Shopping[Item |*| Items] =
    λ { case (items |*| (basket |*| (goodsSupply |*| coinSink))) =>
      // don't let the customer pick an item before basket is ready
      val (basket1 |*| goodsSupply1) =
        (basket |*| goodsSupply) |> sequence_PN

      val (item |*| goodsSupply2) =
        goodsSupply1 |> supplyItem(pick)

      // don't make basket ready again before item is obtained
      val (item1 |*| basket2) =
        item sequence basket1

      (item1 |*| items) |*| (basket2 |*| (goodsSupply2 |*| coinSink))
    }

  private def payForItem[
    Item: SignalingJunction.Positive,
    Items,
  ]: (Coin |*| Shopping[Item |*| Items]) -⚬ (Item |*| Shopping[Items]) =
    λ { case (coin |*| ((item |*| items) |*| (basket |*| (goodsSupply |*| coinSink)))) =>
      // prevent customer from using the item before Coin is provided
      val (item1 |*| coin1) = item sequenceAfter coin

      // prevent returning basket before purchase is paid
      val (basket1 |*| coin2) = basket sequenceAfter coin1

      item1 |*| (items |*| (basket1 |*| (goodsSupply |*| sendCoin(coin2 |*| coinSink))))
    }

  private def makeGoodsSupply: Done -⚬ (GoodsSupply |*| Done) = rec { self =>
    Unlimited.createWith[Done, ItemSelection, Done](
      case0 = id[Done],
      case1 = fork > fst(choice(produceBeer, produceToiletPaper)),
      caseN = forkMap(self, self) > IXI > snd(join),
    )
  }

  private def coinsToBank: One -⚬ (CoinSink |*| CoinBank) =
    done > Unlimited.createWith[Done, -[Coin], CoinBank](
      case0 = newCoinBank,
      case1 = newCoinBank > λ { bank => λ.closure { coin => depositCoin(coin |*| bank) } },
    )

  def openSupermarket(capacity: Int): Done -⚬ (Supermarket |*| CoinBank) =
    λ { trigger =>
      val ((trigger1 |*| trigger2) |*| one) = fork(trigger) |> introSnd

      // make only as many baskets as there are permitted concurrently shopping customers
      val baskets: $[LList1[Basket]] =
        makeBaskets(capacity)(trigger1)

      // Pretend there is an infinite supply of baskets, via pooling.
      // When the pool is empty, next customer will not obtain a basket until a basket is returned to the pool.
      // `collectedBaskets` will become available once there's no chance that anyone will still use them.
      val ((basketSupply: $[Unlimited[BorrowedBasket]]) |*| (collectedBaskets: $[LList1[Basket]])) =
        baskets |> Unlimited.pool

      // `goodsSupplyClosed` will signal once there's no chance that anyone still needs it.
      val ((goodsSupply: $[GoodsSupply]) |*| (goodsSupplyClosed: $[Done])) =
        makeGoodsSupply(trigger2)

      // Any coins sent to `coinSink` will appear in `coinBank`.
      val ((coinSink: $[CoinSink]) |*| (coinBank: $[CoinBank])) =
        coinsToBank(one)

      // Since `GoodsSupply` is a comonoid, it can be split arbitrarily
      // and given to an unlimited number of customers
      val unlimitedGoodsSupply: $[Unlimited[GoodsSupply]] =
        goodsSupply |> Unlimited.fromComonoid

      // Since `CoinSink` is a comonoid, it can be split arbitrarily
      // and given to an unlimited number of customers
      val unlimitedCoinSinks: $[Unlimited[CoinSink]] =
        coinSink |> Unlimited.fromComonoid

      val unlimitedShopping: $[Unlimited[One |*| (BorrowedBasket |*| (GoodsSupply |*| CoinSink))]] = {
        import Unlimited.{zip, map}
        zip(basketSupply |*| zip(unlimitedGoodsSupply |*| unlimitedCoinSinks)) |> map(introFst)
      }

      // wait for goods supply to shut down and all baskets returned before returning today's revenue
      val finalCoinBank: $[CoinBank] =
        coinBank
          .waitFor(goodsSupplyClosed)
          .waitFor(destroyBaskets(collectedBaskets))

      unlimitedShopping |*| finalCoinBank
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy