Allow to match on value's type in interpolated string pattern matching

Hi!

Scala has this nice feature, where you can use interpolated string to match on some string and get some values from it:

case class Point(x: Int, y: Int)

val input = "3,5"

val patmat = 
  input match
    case s"${x},${y}" => println(Point(x, y))
    case _        => println("Nope")

However, this code would not compile, because Point need to get integers, not string, and type of x and y binding is a String.

Of course, one can just use x.toIntOption, but this would mean that the code above is less readable (and way more ugly).

So I propose either this:

  • make it so that s"${x : Int}, ${y: Int}" is treated differently in StringContext.unapply, then it would mean this x should be casted somehow to Int, if possible
    or that:
  • add unapply method for every type that has method .to<type>Option in String, this would the following would be valid then: s"${Int(x)}, ${Int(y)}"

The first proposal is kind of ugly and I don’t actually know how to implement that. The latter is much better IMO, as it would not break anything and it is kind of obvious approach (and even useful outside of this case).

Am I missing something here, perhaps a proper solution to this problem?
What do you think about it?

Thanks!

1 Like

What is missing with Regex matching?
https://www.scala-lang.org/api/current/scala/util/matching/Regex.html

EDIT: OK, I’ve misunderstood the problem. So you mean to add unapply to the Int object?

So you mean to add unapply to the Int object?

Yes, exactly.

I guess it should look like that:

object Int extends AnyValCompanion {
//...

  def unapply(raw: String): Option[Int] = raw.toIntOption
}
1 Like

The problem with that is that it would apply in every pattern match, not just in string interpolators. And that means that

def foo(s: Any, y: Int): Int = s match {
  case Int(x) => x + y
  case x: String => x.length() + y
}

foo("123", 5)

would return 128 and not 8.

That would be very surprising to me. If an Int(x) extractor existed, I would expect it to only match Ints; not Strings that happen to be parsed as Ints.

2 Likes

I wouldn’t find this behavior surprising. I guess I’d read Int(x) as "x can be interpreted as an Int".
For me, x: Int would be the pattern that only matches Ints.

3 Likes

I agree with charpov. I don’t find this extractor confusing. Anyway, we’re still able to use another name than Int.
Something like:

object AsInt {
  
  def unapply(x: String): Option[Int] = x.toIntOption
}

(Scastie)

1 Like

To be honest I’m big fan of:

implicit val intParser = ParseString[Int](_.toIntOption)

val patmat = 
  input match
    case s"${x:Int}, ${y:Int}" => println(Point(x, y))
    case _ => println("Nope")

Alternatively, sscanf style

def f(s: String): Int = s match { case f"$i%d" => i }
1 Like

Personally I also agree with @charpov and @Iltotore.
BTW you can already use Int as an extractor if you provide a proper extension method

extension (int: Int.type)
  def unapply(s: String): Option[Int] = s.toIntOption
3 Likes

Yep, I am aware of that. But creating those for every codebase for every type that I would like to match is kinda wrong. Creating it only when I need is overkill since I could just use .toIntOption.

Yet when you have a

case class Foo(x: Int)

you don’t read case Foo(x) as “x can be interpreted as a Foo”. You read it as "it is a Foo, and it contains an x which is actually an Int.

With this proposal, case Int(x) and case Foo(x) would be inconsistent. If anything, the proposed extractor should be called IntString (something that is a String and that contains an Int).

6 Likes

Yes, I see your point. But I wonder if it’s tied more to case classes than to pattern matching, which can rely on arbitrary extractors.

   val TwoNums = "(\\d+),(\\d+)".r
   val n: Int =
      "42,43" match
         case TwoNums(x, y) => x.toInt + y.toInt
         case _             => 0

In that case, I certainly don’t think of the string "42,43" as being an instance of TwoNums, or even as being a regular expression.

1 Like

For whatever it’s worth, we use Scala UUID at work, and it includes the appropriate extractor to do this:

str match {
  case UUID(uuid) => ...
  case notUUID => ...
}

I don’t recall this ever causing confusion. Nobody expects UUID to be a case class, so not behaving like one doesn’t raise any eyebrows. My intuition is that Int#unapply would be received in a very similar manner.

3 Likes

I personally really like s"${x : Int},${y: Int}", I think it’s clear what it does:
It’s an interpolated string, so you know x and y have to be converted from String anyways, and you don’t add matchers that could be used outside of a context where strings are involved, which could be confusing as @sjrd pointed out.
It could require something akin to Conversion[String, A], maybe a given Parser[A] ?
This would allow easy extensions to user-defined types
Without term inference, there’s also util.CommandLineParser.FromString, but to extend it, you need to subclass it, which is not ideal

6 Likes