Search code examples
scalaliskov-substitution-principle

Is `PartialFunction extends Function` a violation of LSP?


The Liskov Substitution Principle state that

if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of that program.

However, in Scala, there is the PartialFunction that is not applicable/defined in all cases.

trait Function1 {
  def apply(x: A): R
}

trait PartialFunction extends Function1 {
  def apply(x: A): R
  def isDefinedAt(x: A): Boolean
}

If you apply a PartialFunction to an undefined value, you will receive an exception.

A convenient way to create a PartialFunction in scala is to use pattern matching. Doing so, you receive a MatchError for undefined values.

val fn:Function1[String, Int] = s => s.length

val pf:PartialFunction[String, Int] = {
  case "one" => 3
}

def program(f:Function1[String, Int], s:String):(Boolean, Int) = (
  f.isInstanceOf[Function1[String, Int]], f(s)
)

program(fn, "one") == program(pf, "one")
program(fn, "two") == program(pf, "two")

fn: String => Int = <function1>

pf: PartialFunction[String,Int] = <function1>

program: program[](val f: String => Int,val s: String) => (Boolean, Int)

res0: Boolean = true

scala.MatchError: two (of class java.lang.String)

   at scala.PartialFunction$$anon$1.apply(delme.sc:249)

   at scala.PartialFunction$$anon$1.apply(delme.sc:247)

   at ...

Both fn and pf are subtypes of Function1, but I cannot substitute fn by pfwithout altering my program. So in my opinion it is a violation of LSP.

What do you think ?


Solution

  • fn and pf are not subtypes of Function1, since they aren't types at all. They are values, and LSP doesn't say you can take any object of type T and replace it with any object of type S: Int is certainly a subtype of Int, but replacing 1 with 2 can make a correct program incorrect.

    PartialFunction[String, Int] is a subtype of Function1[String, Int], but the contract of Function1 doesn't forbid apply from throwing exceptions and in fact explicitly allows it:

    Apply the body of this function to the argument. It may throw an exception.

    So if your program can handle objects of type Function1[A, B], it has to deal with apply throwing exceptions in some way already and PartialFunction throwing a MatchError is just a specific case.