Surjectivity check when return type is sealed
Asked Answered
H

2

6

Scala can warn when pattern match on a sealed type is not exhaustive, however can we check that a function returns all cases when the return type is sealed? For example, consider the following ADT

sealed trait Foo
case object Bar extends Foo
case object Qux extends Foo

Then function f: Foo => String on the algebraic data type Foo

def f(x: Foo): String = x match {
  case Bar => "bar"
}

raises warning

match may not be exhaustive.
It would fail on the following input: Qux
def f(x: Foo) = x match {

Is it possible to raise a similar non-exhaustion warning when return type is an ADT such as in the following implementation of f: String => Foo:

def f(x: String): Foo = x match {
  case "bar" => Bar
  // warn because we never return Qux 
}
Hamford answered 15/5, 2019 at 14:48 Comment(2)
I do not know if such warning may exists, but I doubt it exists, since there is nothing wrong about not having a case for every possible implementation. Probably the only place where this would be useful would be for a factory from name to instance (which looks like what you showed). - Not exactly an answer, but you may be interested that Enumeratum will create such method for you, and since it is a macro it will contain all cases. Maybe, you may just write a similar macro to ensure there is at least one case for each implementation?Ironbound
Also, you may try proposing this feature on the Scala contributors channel.Ironbound
E
5

Maybe this is not really an answer, but it's too long for a comment anyway.

Pattern matching and function return values are two different things. Former operates on the type level, latter on the value level. When you pattern match on Bar, you're pattern matching on the type (just like e.g. Int). But when you return Bar, you're returning the case object value (just like e.g. 42).

Surjective function is defined as:

For every member y of the codomain, there exists at least one member x of the domain, such that f(x) = y.

Now it's easy to see why this check is not feasible / possible. What if your Bar wasn't a case object, but a class? E.g.

final case class Bar(name: String, surname: String, age: Int)

You would need to expect every possible value of Bar to be used (e.g. name = "John", surname = "Smith", age = 42).

Of course, this is not what you intended; what you described is a scenario where each subtype has exactly one inhabitant, because Bar and Qux are basically enums, and I can see why such a check might make sense to you. But it would have to be implemented for the general case of arbitrary number of inhabitants per (sub)type - it would need to verify that codomain contains at least one value of type Bar, at least one value of type Qux etc. which doesn't sound very useful.

As I said, this is not really an answer, but I wanted to give you an insight into what exactly it is that you're asking. :) Perhaps someone wrote something with reflection and/or macros which could provide such a check, but not to my knowledge. Hopefully with Scala 3 enums you will never need to write such a function anyway.

Ebb answered 15/5, 2019 at 15:59 Comment(0)
H
2

Here are enumeration examples suggested by @LuisMiguelMejíaSuárez and @slouc which do provide case exhaustion:

enumeratum

import enumeratum._

sealed trait Foo extends EnumEntry
object Foo extends Enum[Foo] {
  val values = findValues
  case object Bar extends Foo
  case object Qux extends Foo  
}

Foo.withName("Qux")

Scala 3 Enumerations

enum Foo {
  case Bar
  case Qux
}

Foo.enumValueNamed("Qux"))

Both methods worked fine even with parameterised sealed type.

Hamford answered 15/5, 2019 at 21:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.