Search code examples
scalafunctional-programmingscalazscala-catsmonoids

Apply all filter functions to a value


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?


Solution

  • 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)
    }