Search code examples
scalapartialfunction

Adapter for a PartialFunction


I am trying to come up with a combinator that would allow me do something like this:

 def pfAdapter(pf: PartialFunction[String, String]): PartialFunction[(String,String), String]) = { 
   case (a,b) if(pf.isDefinedAt(a)) => pf(a)
 }

Basically, I have a Map[String, String], and want to use it as a PartialFunction that takes two parameters - ignore the second element, and use the first one as a key.

The approach above works, but I don't like the fact that pf essentially gets evaluated twice (there may be no way around that), and that it's not "elegant" ... I was wondering if there is some kind of a combinator I don't know about that would let me do something like { _._1 } andThen pf. This last attempt doesn't work, obviously, because the result of it is always defined, and will simply fail on non-existent keys, I am just using it as an illustration of how the solution I am looking for would ideally look.

Ideas, anyone?


Solution

  • The function itself (pf.apply) is not really evaluated twice, but its isDefinedAt is evaluated twice for successful matches with your definition. And that means evaluating twice unapply-s and guards in the initial PartialFunction pf.

    By the way there is a combinator in Scalaz that does a similar thing: pf.first.andThen(_._1), but it is basically equivalent to your definition.

    You can write a small test, to see if pf.isDefinedAt is evaluated twice and run it with several possible implementations of pfAdapter:

    object Unapply {
      def unapply(s: String): Boolean = {
        println(s"unapplying on $s")
        s == "1"
      }
    }
    
    val m = Map("1" -> 1, "2" -> 2)
    def pf: PartialFunction[String, String] = {
      case Unapply() => "11"
    }
    
    def pfAdapter1[A, B, T](pf: PartialFunction[A, B]): PartialFunction[(A, T), B] =
      Function.unlift((t: (A, T)) => pf.lift(t._1))
    
    def pfAdapter2[A, B, T](pf: PartialFunction[A, B]): PartialFunction[(A, T), B] =
      new PartialFunction[(A, T), B] {
        def isDefinedAt(arg: (A, T)) = pf.isDefinedAt(arg._1)
        def apply(arg: (A, T)) = pf(arg._1)
      }
    
    def pfAdapter3[A, B, T](pf: PartialFunction[A, B]): PartialFunction[(A, T), B] = {
      case (a,b) if pf.isDefinedAt(a) => pf(a)
    }
    
    def pfAdapter4[A, B, T](pf: PartialFunction[A, B]): PartialFunction[(A, T), B] = {
      import scalaz.Scalaz._
      pf.first.andThen(_._1)
    }
    
    println(m collect pfAdapter1(pf))
    println(m collect pfAdapter2(pf))
    println(m collect pfAdapter3(pf))
    println(m collect pfAdapter4(pf))
    

    And the result of executing this code is as follows:

    unapplying on 1
    unapplying on 2
    List(11)
    unapplying on 1
    unapplying on 1
    unapplying on 2
    List(11)
    unapplying on 1
    unapplying on 1
    unapplying on 2
    List(11)
    unapplying on 1
    unapplying on 1
    unapplying on 2
    List(11)
    

    So the first implementation of pfAdapter: Function.unlift((t: (A, T)) => pf.lift(t._1)) actually does avoid evaluating isDefinedAt twice.

    This works, because Map.collect is implemented with PartialFunction.applyOrElse, and the documentation for applyOrElse states, that:

    For all partial function literals the compiler generates an applyOrElse implementation which avoids double evaluation of pattern matchers and guards. This makes applyOrElse the basis for the efficient implementation for many operations and scenarios, such as:

    ...

    • lift and unlift do not evaluate source functions twice on each invocation