6
votes

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 ?

2
This is going to be primarily an opinion piece. Do you have a more specific question regarding application or use of the more general question you're asking?wheaties
Absolutely not. I was just asking for advice from other developers. Maybe should I have posted it on another community ?gervais.b
You could also define a Function1 that simply throws exceptions for all inputs other than "one". Your argument for violating LSP is that throwing an exception may be an undesirable alteration, but a Function1 can still have inputs that throw exceptions. e.g. BigDecimal("abc"). The main difference between PartialFunction and Function1 is that you have a built-in way of checking whether or not elements are defined first.Michael Zajac
No languages can prevent user mistakes. If someone want to break his program he can always do it. My concerns is more than in my opinion, Scala (the language itself) is violating the LSP. That will never make it less "cool" but I was just wondering.gervais.b

2 Answers

6
votes

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.

1
votes

According to wikipedia there is an additional clause required for the LSP to apply.

No new exceptions should be thrown by methods of the subtype, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the supertype.

So (assuming wikipedia is correct), no PartialFunction doesn't violate the LSP since it doesn't apply to this case.

EDIT: There is additional info in the scaladoc: (Scaladoc Function1)

Note that Function1 does not define a total function, as might be suggested by the existence of PartialFunction. The only distinction between Function1 and PartialFunction is that the latter can specify inputs which it will not handle.

So another way of thinking about it would be that pf is not a subtype of fn.

fn: String => Int

pf: Subset of String => Int

Further edit in response to Comment: No I'm arguing that the LSP doesn't apply at all. For the LSP to apply S must not throw new exceptions. S here does throw new exceptions so the LSP cannot be violated since it doesn't apply.