Unexpected behaviour for inline condition

Hey,

Today I’ve started to learn about the inline in scala, and figured out something that looks peculiar to me in regard to inline conditions.

Having the code below, I’ve got an error when tried the inline condition call on a variable anotherFalse of type Boolean, which was normal.
But when a variable of Int type is passed to the inline function with and inline pattern match I haven’t got a compilation error and, moreover, the result is strange.
Value 2 (Int) lead to the result “many” :frowning:

Is normal this behavior?

Thanks

inline def condition(x: Boolean): String = inline if x then "true" else "false"

inline val aFalse = false
val cFalse = condition(aFalse)
// val anotherFalse = false
// val cFalse = condition(anotherFalse) // this one gives a compile-time error as expected

inline def pMatch(x: Int): String = inline x match {
  case 0 => "zero"
  case 1 => "one"
  case 2 => "two"
  case _ => "many"
}

inline val aOne = 1
val pOne = pMatch(aOne) // works fine because aOne is inline

val aTwo = 2
val pTwo = pMatch(
  aTwo
) // I expected a compile-time error here as it is the case for Boolean, but it compiles fine and pTwo is "many" :frowning:

@main def main() = {
  println(pOne)
  println(pTwo) // "many"
}


There is some previous discussion at inline match breaks simple example, documentation? · Issue #13774 · scala/scala3 · GitHub

I’m not sure if there is a workaround other than switching to inline if

1 Like

Many thanks for pointing out @SethTisue

Still it is unclear to me how inline pattern match at compile time differs from the one at the runtime.

And, moreover, the following manifestation is quite weird to newcomers. :frowning:

Why when adding the _ match case all looks so weird in case of non constant being passed to inline function.

Wouldn’t be more straightforward to have a compile error when passing a non-constant to the inline function with an inline pattern match despite having the default case _ ?!

Many thanks in advance for putting me on the right path on understanding how inline pattern match works.

inline def conditionM3(x: Int): String = inline x match
  case 1 => "one"
  case 2 => "two"
  case 3 => "three"

inline def conditionM4(x: Int): String = inline x match
  case 1 => "one"
  case 2 => "two"
  case 3 => "three"
  case _ => "many" // adding a default case makes this compile :( but changes the behavior for the case when passed x is not a constant

val two = 2 // there is no problem if we have an constant here inline val aTwo = 2
// val d = conditionM3(two) // this one gives a compile-time error as expected
val f = conditionM4(two)
inline val aTwo = 2
val g = conditionM4(aTwo)

@main def main() = //
  println(f) // prints "many" :( I hardly believe a compile time error would be a better outcome here, as it is for the inline if
  println(g) // prints "two"

The key to understanding this is to write in the inferred type.

val two: Int = 2  // This is what two = 2 means
inline val aTwo: 2 = 2  // This is what inline val aTwo = 2 means

Why the difference? Well, the first one is ordinary assignment, which widens to the whole type, not the instance, because that is usually what you want. So, two is an Int. You happen to have chosen for it to be 2, but all the compiler knows when you say two is that it’s an Int.

However, an inline val is basically just saying, “Whenever you see this name on the left, copy in the literal thing I put on the right.” It’s equivalent to cut-and-paste.

Then for inline matches, the only thing to know is that it will match what it knows at compile-time, and if it’s an inline function you get the arguments copied in. (If the arguments themselves are inline, you get any expression that created them copied in–very useful for lambdas!)

So, conditionM3(two) asks the following: is our input, an Int with some name (happens to be two, but whatever), known statically to be the value 1? No. Is it surely 2? No. 3? No. Of course it’s not known to be any of those! All you know is that it’s an Int! So that’s a compile-time error.

conditionM4(aTwo) asks the following: is our input, which is aTwo, by which we simply mean 2, known statically to the the value 1? Of course not! Is it 2? Yes!! Okay, good.

conditionM4(two) asks the following: is our input, which is an Int, statically known to be 1? No, an Int is not a 1. Is it known to be 2? Nope, it’s an Int. Are we sure that it’s a 3? No! So we use the last case, which we confusingly called “many” even though it very well could be 1, just a 1 that we can’t tell is there statically.

If you want to check at runtime (which is your only choice once you’ve lost the precise type information), use a regular match statement.

3 Likes

Many thanks @Ichoran for the explanation.
It is clear now and makes sense (having a type 2 in the expression two: 2 = 2 is something new for a new comer).
Inline pattern match works only on types not values, as per my understanding.
On the other hand if I transform the inline pattern to inline if elseif else expression the type-like behavior vanishes, and the behavior looks more natural to a non-scala programmer.

Thanks!