Upgrade to Pro — share decks privately, control downloads, hide ads and more …

A sighting of traverseFilter and foldMap in Practical FP in Scala

A sighting of traverseFilter and foldMap in Practical FP in Scala

Philip Schwarz
PRO

October 01, 2023
Tweet

More Decks by Philip Schwarz

Other Decks in Programming

Transcript

  1. a sighting of
    traverseFilter
    and foldMap in
    @philip_schwarz
    slides by http://fpilluminated.com/
    by

    View Slide

  2. trait ShoppingCart[F[_]] {
    def add(userId: UserId, itemId: ItemId, quantity: Quantity): F[Unit]
    def get(userId: UserId): F[CartTotal]
    def delete(userId: UserId): F[Unit]
    def removeItem(userId: UserId, itemId: ItemId): F[Unit]
    def update(userId: UserId, cart: Cart): F[Unit]
    }
    object ShoppingCart {
    def make[F[_]: GenUUID: MonadThrow](
    items: Items[F],
    redis: RedisCommands[F, String, String],
    exp: ShoppingCartExpiration
    ): ShoppingCart[F] = new ShoppingCart[F] {
    override def add(userId: UserId, itemId: ItemId, quantity: Quantity): F[Unit] =
    redis.hSet(userId.show, itemId.show, quantity.show) *>
    redis.expire(userId.show, exp.value).void
    override def get(userId: UserId): F[CartTotal] =
    redis.hGetAll(userId.show).flatMap { itemIdToQuantityMap: Map[String, String] =>
    itemIdToQuantityMap.toList
    .traverseFilter { case (id, qty) =>
    for {
    itemId <- ID.read[F, ItemId](id)
    quantity <- MonadThrow[F].catchNonFatal(Quantity(qty.toInt))
    maybeCartItem <- items.findById(itemId).map(_.map(_.cart(quantity)))
    } yield maybeCartItem
    }
    .map { items =>
    CartTotal(items, items.foldMap(_.subTotal))
    }
    }

    }
    }
    with some minor renaming, to ease comprehension
    for anyone lacking context – see repo for original

    View Slide

  3. override def get(userId: UserId): F[CartTotal] =
    redis.hGetAll(userId.show).flatMap { itemIdToQuantityMap: Map[String, String] =>
    itemIdToQuantityMap.toList
    .traverseFilter { case (id, qty) =>
    for {
    itemId <- ID.read[F, ItemId](id)
    quantity <- MonadThrow[F].catchNonFatal(Quantity(qty.toInt))
    maybeCartItem <- items.findById(itemId).map(_.map(_.cart(quantity)))
    } yield maybeCartItem
    }
    .map { items =>
    CartTotal(items, items.foldMap(_.subTotal))
    }
    }
    override def get(userId: UserId): F[CartTotal] =
    redis.hGetAll(userId.show).flatMap { itemIdToQuantityMap: Map[String, String] =>
    itemIdToQuantityMap.toList
    .traverseFilter { case (id, qty) =>
    for {
    itemId <- ID.read[F, ItemId](id)
    quantity <- MonadThrow[F].catchNonFatal(Quantity(qty.toInt))
    maybeCartItem <- items.findById(itemId).map(_.map(_.cart(quantity)))
    } yield maybeCartItem
    }
    .map { items =>
    CartTotal(items, items.foldMap(_.subTotal))
    }
    }
    ^⇧P Type Info
    ^⇧P Type Info

    View Slide

  4. override def get(userId: UserId): F[CartTotal] =
    redis.hGetAll(userId.show).flatMap { itemIdToQuantityMap: Map[String, String] =>
    itemIdToQuantityMap.toList
    .traverseFilter { case (id, qty) =>
    for {
    itemId <- ID.read[F, ItemId](id)
    quantity <- MonadThrow[F].catchNonFatal(Quantity(qty.toInt))
    maybeCartItem <- items.findById(itemId).map(_.map(_.cart(quantity)))
    } yield maybeCartItem
    }
    .map { items: List[cart.CartItem] =>
    CartTotal(items, items.foldMap(_.subTotal))
    }
    }
    override def get(userId: UserId): F[CartTotal] =
    redis.hGetAll(userId.show).flatMap { itemIdToQuantityMap: Map[String, String] =>
    itemIdToQuantityMap.toList
    .traverseFilter { case (id, qty) =>
    for {
    itemId <- ID.read[F, ItemId](id)
    quantity <- MonadThrow[F].catchNonFatal(Quantity(qty.toInt))
    maybeCartItem <- items.findById(itemId).map(_.map(_.cart(quantity)))
    } yield maybeCartItem
    }
    .map { items: List[cart.CartItem] =>
    CartTotal(items, items.foldMap(_.subTotal))
    }
    }
    ^⇧P Type Info
    ^⇧P Type Info
    @typeclass trait TraverseFilter[F[_]] extends FunctorFilter[F] {

    A combined traverse and filter. Filtering is handled via Option instead of Boolean such that the
    output type B can be different than the input type A.
    def traverseFilter[G[_], A, B](fa: F[A])(f: A => G[Option[B]])(implicit G: Applicative[G]): G[F[B]]
    List[(String,String)] => ((String,String) => F[Option[CartItem]]) => F[List[CartItem]]
    def make[F[_]: GenUUID: MonadThrow]
    type MonadThrow[F[_]] = MonadError[F, Throwable]
    An applicative that also allows you to raise and or handle an error value.
    This type class allows one to abstract over error-handling applicatives.
    trait ApplicativeError[F[_], E] extends Applicative[F] { …
    This type class allows one to abstract over error-handling monads.
    trait MonadError[F[_], E] extends ApplicativeError[F, E] with Monad[F] { …
    Applicative
    Monad
    Functor
    TraverseFilter
    FunctorFilter
    ApplicativeError
    MonadError
    FunctorFilter[F] allows you to map and filter out elements simultaneously.
    @typeclass trait FunctorFilter[F[_]] extends Serializable
    TraverseFilter, also known as Witherable, represents list-like structures
    that can essentially have a traverse and a filter applied as a single
    combined operation (traverseFilter).
    @typeclass trait TraverseFilter[F[_]] extends FunctorFilter[F] {
    Traverse
    Foldable

    View Slide

  5. trait ShoppingCart[F[_]] {
    def add(userId: UserId, itemId: ItemId, quantity: Quantity): F[Unit]
    def get(userId: UserId): F[CartTotal]
    def delete(userId: UserId): F[Unit]
    def removeItem(userId: UserId, itemId: ItemId): F[Unit]
    def update(userId: UserId, cart: Cart): F[Unit]
    }
    object ShoppingCart {
    def make[F[_]: GenUUID: MonadThrow](
    items: Items[F],
    redis: RedisCommands[F, String, String],
    exp: ShoppingCartExpiration
    ): ShoppingCart[F] = new ShoppingCart[F] {
    override def add(userId: UserId, itemId: ItemId, quantity: Quantity): F[Unit] =
    redis.hSet(userId.show, itemId.show, quantity.show) *>
    redis.expire(userId.show, exp.value).void
    override def get(userId: UserId): F[CartTotal] =
    redis.hGetAll(userId.show).flatMap { itemIdToQuantityMap: Map[String, String] =>
    itemIdToQuantityMap.toList
    .traverseFilter { case (id, qty) =>
    for {
    itemId <- ID.read[F, ItemId](id)
    quantity <- MonadThrow[F].catchNonFatal(Quantity(qty.toInt))
    maybeCartItem <- items.findById(itemId).map(_.map(_.cart(quantity)))
    } yield maybeCartItem
    }
    .map { items: List[cart.CartItem] =>
    CartTotal(items, items.foldMap(_.subTotal))
    }
    }

    }
    }

    View Slide

  6. override def get(userId: UserId): F[CartTotal] =
    redis.hGetAll(userId.show).flatMap { itemIdToQuantityMap: Map[String, String] =>
    itemIdToQuantityMap.toList
    .traverseFilter { case (id, qty) =>
    for {
    itemId <- ID.read[F, ItemId](id)
    quantity <- MonadThrow[F].catchNonFatal(Quantity(qty.toInt))
    maybeCartItem <- items.findById(itemId).map(_.map(_.cart(quantity)))
    } yield maybeCartItem
    }
    .map { items =>
    CartTotal(items, items.foldMap(_.subTotal))
    }
    }
    ⇧⌘P Show implicit arguments
    trait Foldable[F[_]] extends UnorderedFoldable[F] with FoldableNFunctions[F] {

    Fold implemented by mapping A values into B and then
    combining them using the given Monoid[B] instance.
    def foldMap[A, B](fa: F[A])(f: A => B)(implicit B: Monoid[B]): B =
    foldLeft(fa, B.empty)((b, a) => B.combine(b, f(a)))
    ^⇧P Type Info
    ⌘P Parameter Info
    override def get(userId: UserId): F[CartTotal] =
    redis.hGetAll(userId.show).flatMap { itemIdToQuantityMap: Map[String, String] =>
    itemIdToQuantityMap.toList
    .traverseFilter { case (id, qty) =>
    for {
    itemId <- ID.read[F, ItemId](id)
    quantity <- MonadThrow[F].catchNonFatal(Quantity(qty.toInt))
    maybeCartItem <- items.findById(itemId).map(_.map(_.cart(quantity)))
    } yield maybeCartItem
    }
    .map { items =>
    CartTotal(items, items.foldMap(_.subTotal))(moneyMonoid)
    }
    }
    implicit val moneyMonoid: Monoid[Money] =
    new Monoid[Money] {
    override def empty: Money = USD(0)
    override def combine(x: Money, y: Money): Money = x + y
    }
    case class CartItem(item: Item, quantity: Quantity) {
    def subTotal: Money = USD(item.price.amount * quantity.value)
    }
    List[CartItem] => (CartItem => Money) => Monoid[Money] => Money
    A monoid is a semigroup with an identity. A monoid is a specialization of a
    semigroup, so its operation must be associative. Additionally, combine(x,
    empty) == combine(empty, x) == x. For example, if we have Monoid[String],
    with combine as string concatenation, then empty = "".
    trait Monoid[@sp(Int, Long, Float, Double) A] extends Any with Semigroup[A] {
    A semigroup is any set A with an associative operation (combine).
    trait Semigroup[@sp(Int, Long, Float, Double) A] extends Any with Serializable {
    Semigroup
    Monoid
    final class Money private
    (val amount: BigDecimal)
    (val currency: Currency)
    extends Quantity[Money] {

    View Slide