Search code examples
scalacombinators

Conditionally using .reverse in the same line with Scala


I have a composition of combinators in Scala, and the last one is .top, which I could use as .top(num)(Ordering[(Int, Int)].reverse) depending on a boolean parameter.

How do I implement this composition of combinators to use or not use .reverse depending on the boolean parameter, in the same line? I mean, without creating another val to indicate whether .reverse is used?

val mostPopularHero = sparkContext
  .textFile("resource/marvel/Marvel-graph.txt") // build up superhero co-apperance data
  .map(countCoOccurrences) // convert to (hero ID, number of connections) RDD
  .reduceByKey((x, y) => x + y) // combine entries that span more than one line
  .map(x => (x._2, x._1)) // flip it from (hero ID, number of connections) to (number of connections, hero ID)
  .top(num)(Ordering[(Int, Int)].reverse)

Solution

  • Solution 0

    As nicodp has already pointed out, if you have a boolean variable b in scope, you can simply replace the expression

    Ordering[(Int, Int)]
    

    by an if-expression

    if (b) Ordering[(Int, Int)] else Ordering[(Int, Int)].reverse
    

    I have to admit that this is the shortest and clearest solution I could come up with.

    However... I didn't quite like that the expression Ordering[(Int, Int)] appears in the code twice. It doesn't really matter in this case, because it's short, but what if the expression were a bit longer? Apparently, even Ruby has something for such cases.

    So, I tried to come up with some ways to not repeat the subexpression Ordering[(Int, Int)]. The nicest solution would be if we had a default Id-monad implementation in the standard library, because then we could simply wrap the one value in pure, and then map it using the boolean. But there is no Id in standard library. So, here are a few other proposals, just for the case that the expression in question becomes longer:

    Solution 1

    You can use blocks as expressions in scala, so you can replace the above Ordering[(Int, Int)] by:

    {val x = Ordering[(Int, Int)]; if (b) x else x.reverse}
    

    Update: Wait! This is shorter than the version with repetition! ;)

    Solution 2

    Define the function that conditionally reverses an ordering, declare Ordering[(Int, Int)] as the type of the argument, and then instead of re-typing Ordering[(Int, Int)] as an expression, use implicitly:

    ((x: Ordering[(Int, Int)]) => if (b) x else x.reverse)(implicitly)
    

    Solution 3

    We don't have Id, but we can abuse constructors and eliminators of other functors. For example, one could wrap the complex expression in a List or Option, then map it, then unpack the result. Here is a variant with Some:

    Some(Ordering[(Int, Int)]).map{ x => if(b) x else x.reverse }.get
    

    Ideally, this would have been Id instead of Some. Notice that Solution 1 does something similar with the default ambient monad.

    Solution 4

    Finally, if the above pattern occurs more than once in your code, it might be worth it to introduce some extra syntax to deal with it:

    implicit class ReversableOrderingOps[X](ord: Ordering[X]) {
      def reversedIf(b: Boolean): Ordering[X] = if (b) ord.reverse else ord
    }
    

    Now you can define orderings like this:

    val myConditionHolds = true
    val myOrd = Ordering[(Int, Int)] reversedIf myConditionHolds
    

    or use it in your lengthy expression directly:

    val mostPopularHero = sparkContext
      .textFile("resource/marvel/Marvel-graph.txt")
      .map(countCoOccurrences)
      .reduceByKey((x, y) => x + y)
      .map(x => (x._2, x._1))
      .top(num)(Ordering[(Int, Int)] reversedIf myConditionHolds)