3

I have a function that looks like this:

def createBuilder(builder: InitialBuilder, name: Option[String], useCache: Boolean, timeout: Option[Long]): Builder = {
    val filters: List[Builder => Option[Builder]] = List(
      b => name.map(b.withName),
      b => if (useCache) Some(b.withCache) else None,
      b => timeout.map(b.withTimeout))

    filters.foldLeft(builder)((b,filter) => filter(b).getOrElse(b))
}

It defines 3 filter functions from Builder => Option[Builder] (converting from optional parameters). I want to apply them to an existing builder value, so in case of a None, I can return itself, unchanged.

The code above is the best I could come up with, but it feels that I should somehow be able to do this with a Monoid - return the identity in case of a None.

Unfortunately, I can't figure out how to define one that makes sense. Or, if there's a better/different way of doing this?

I'm using Cats, if that matters. Any ideas?

Igal Tabachnik
  • 31,174
  • 15
  • 92
  • 157
  • *return the identity in case of a None.* Return the identity (zero) of the monoid? Meaning if you'd have a `Monoid[String].zero` you'll return an empty string, is that ok? – Yuval Itzchakov Jul 03 '17 at 14:06
  • @YuvalItzchakov hmm, I'm sure I meant applying the `identity` function to return the initial `builder`... – Igal Tabachnik Jul 03 '17 at 14:09
  • Ah, so you're talking about a `Monoid[Builder]`? – Yuval Itzchakov Jul 03 '17 at 14:09
  • Yes, that sounds right :) – Igal Tabachnik Jul 03 '17 at 14:10
  • I'm having a hard time seeing how making a monoid for builder will create a cleaner abstraction for this one. – Yuval Itzchakov Jul 03 '17 at 14:15
  • 1
    @YuvalItzchakov no clue! That's why I'm here, I guess :D – Igal Tabachnik Jul 03 '17 at 14:16
  • It also bothers me that the filter type is `A => M[A]`, which looks like a Kleisli. But again, not sure what to do here... – Igal Tabachnik Jul 03 '17 at 14:18
  • But do you want to abstract over the type? Do you have any other builder like types which require generalization? – Yuval Itzchakov Jul 03 '17 at 14:26
  • @YuvalItzchakov actually not this particular one, but there are some other legacy builder-pattern code this could benefit from. – Igal Tabachnik Jul 03 '17 at 14:42
  • 1
    It doesn't really feel like `A => M[A]`. Seems more natural to map your `filters` to a list of endomorphic functions `val fs: List[Builder => Builder] = filters.map(f => (b: Builder) => f(b).getOrElse(b))`, and then `foldK` this `List`. (I think `foldK` should work automatically with -Ypartial-unification, but I can't make it work, so you may have to provide type arguments manually. Also, cats folds the list in reverse, because it uses `compose` instead of `andThen` for`MonoidK`'s `combineK`) – Kolmar Jul 03 '17 at 16:23

1 Answers1

1

I think in your case A => M[A] structure is a bit superfluous. The filter functions you use in the example are actually equivalent to Option[Builder => Builder]. That's because you don't use their Builder argument to decide whether the result should be Some or None. And you can further simplify the functions to Builder => Builder with .getOrElse(identity).

Here are 2 implementations that use this idea. They don't even really rely on cats.

def createBuilder(
  builder: InitialBuilder, name: Option[String], useCache: Boolean, timeout: Option[Long]
): Builder = {
  def builderStage[T](param: Option[T])(modify: T => Builder => Builder): Builder => Builder =
    param.fold(identity[Builder](_))(modify)

  val stages: List[Builder => Builder] = List(
    builderStage(name)(n => _ withName n),
    // `Boolean` is equivalent to `Option[Unit]`, and we convert it to that representation
    // Haskell has a special function to do such a conversion `guard`.
    // In Scalaz you can use an extension method `useCache.option(())`.
    // In cats a similar `option` is provided in Mouse library.
    // But you can just write this manually or define your own extension
    builderStage(if (useCache) ().some else none)(_ => _.withCache),
    builderStage(timeout)(t => _ withTimeout t)
  )

  // It should be possible to use `foldK` method in cats, to do a similar thing.
  // The problems are that it may be more esoteric and harder to understand, 
  // it seems you have to provide type arguments even with -Ypartial-unification,
  // it folds starting from the last function, because it's based on `compose`.
  // Anyway, `reduceLeft(_ andThen _)` works fine for a list of plain functions. 
  stages.reduceLeft(_ andThen _)(builder)
}

Another possibility is to flatten the List of Options, which simply removes Nones without coercing them to identity:

def createBuilder2(
  builder: InitialBuilder, name: Option[String], useCache: Boolean, timeout: Option[Long]
): Builder = {
  val stages: List[Option[Builder => Builder]] = List(
    name.map(n => _ withName n),
    if (useCache) Some(_.withCache) else None,
    timeout.map(t => _ withTimeout t)
  )

  stages.flatten.reduceLeft(_ andThen _)(builder)
}
Kolmar
  • 14,086
  • 1
  • 22
  • 25