├── README.md └── pattern-matching.txt /README.md: -------------------------------------------------------------------------------- 1 | Moved to https://codeberg.org/lutzh/pattern-matching-talk 2 | -------------------------------------------------------------------------------- /pattern-matching.txt: -------------------------------------------------------------------------------- 1 | \! 2 | | \gEverything You Always Wanted to Know About Pattern Matching* 3 | | \g(*But Were Afraid to Ask) 4 | 5 | | @lutzhuehnken 6 | 7 | 8 | | Lightbend 9 | --- 10 | | Pattern Matching 11 | 12 | ``` 13 | expr match { 14 | case pattern1 => result1 15 | case pattern2 => result2 16 | } 17 | ``` 18 | 19 | Differences to Java switch: 20 | * It’s an expression 21 | * No fall-through 22 | * MatchError if there’s no match 23 | 24 | 25 | --- 26 | 27 | 28 | ``` 29 | case pattern => result 30 | 31 | ``` 32 | 33 | * case is followed by a pattern 34 | * pattern must be one of the legal pattern types 35 | * result is an arbitrary expression 36 | * if pattern matches, result will be evaluated and returned 37 | 38 | --- 39 | 40 | ``` 41 | def matchAll(any: Any): String = any match { 42 | case _ => "It’s a match!" 43 | } 44 | 45 | ``` 46 | Wildcard Pattern 47 | * _ matches anything 48 | * use as „default“ (remember MatchError) 49 | 50 | --- 51 | 52 | ``` 53 | def isIt8(any: Any): String = any match { 54 | case "8:00" => "Yes" 55 | case 8 => "Yes" 56 | case _ => "No" 57 | } 58 | ``` 59 | 60 | Constant Pattern 61 | 62 | --- 63 | 64 | ``` 65 | def matchX(any: Any): String = any match { 66 | case x => s"He said $x!" 67 | } 68 | ``` 69 | 70 | Variable Pattern
 71 | * identifier - also matches anything! 72 | 73 | --- 74 | 75 | Variables vs. Constants 76 | 77 | ``` 78 | import math.Pi 79 | 80 | val pi = Pi 81 | 82 | def m1(x: Double) = x match { 83 | case Pi => "Pi!" 84 | case _ => "not Pi!" 85 | } 86 | 87 | def m2(x: Double) = x match { 88 | case pi => "Pi!" 89 | case _ => "not Pi!" 90 | } 91 | 92 | ``` 93 | 94 | 95 | --- 96 | 97 | | Attention! 98 | 99 | * uppercase - compiler will assume it's constant 100 | 101 | * lowercase - compiler will assume it's a new 102 | identifier (local to the match expression)! 103 | 104 | | This is a very common source of errors! 105 | --- 106 | 107 | 108 | Let's fix this! 109 | 110 | ``` 111 | import math.Pi 112 | 113 | val pi = Pi 114 | 115 | def m3(x: Double) = x match { 116 | case this.pi => "Pi!" 117 | case _ => "not Pi!" 118 | } 119 | 120 | def m4(x: Double) = x match { 121 | case `pi` => "Pi!" 122 | case _ => "not Pi!" 123 | } 124 | 125 | 126 | ``` 127 | 128 | --- 129 | 130 | To refer to lowercase values 131 | 132 | * use qualified name (e.g. this.pi) or 133 | 134 | * use backticks (e.g. `pi`) 135 | 136 | --- 137 | 138 | Constructor Pattern (Case Classes) 139 | 140 | ``` 141 | case class Time(hours: Int = 0, minutes: Int = 0) 142 | val (noon, morn, eve) = (Time(12), Time(9), Time(20)) 143 | 144 | def mt(t: Time) = t match { 145 | case Time(12,_) => "twelve something" 146 | case _ => "not twelve" 147 | } 148 | ``` 149 | 150 | --- 151 | 152 | Nest it like crazy 153 | 154 | ``` 155 | case class House(street: String, number: Int) 156 | case class Address(city: String, house: House) 157 | case class Person(name: String, age: Int, address: Address) 158 | 159 | val peter = Person("Peter", 33, Address("Hamburg", House("Reeperbahn", 45))) 160 | val paul = Person("Paul", 29, Address("Berlin", House("Oranienstrasse", 64))) 161 | 162 | def m45(p: Person) = p match { 163 | case Person(_, _, Address(_, House(_, 45))) => "Must be Peter!" 164 | case Person(_, _, Address(_, House(_, _))) => "Someone else" 165 | } 166 | ``` 167 | 168 | --- 169 | 170 | Sequence Pattern 171 | 172 | ``` 173 | val l1 = List(1,2,3,4) 174 | val l2 = List(5) 175 | val l3 = List(5,8,6,4,9,12) 176 | 177 | def ml(l: List[Int]) = l match { 178 | case List(1,_,_,_) => "starts with 1 and has 4 elements" 179 | case List(5, _*) => "starts with 5" 180 | } 181 | ``` 182 | 183 | --- 184 | Sequence Pattern (cont'd) 185 | 186 | ``` 187 | import annotation._ 188 | 189 | @tailrec 190 | def contains5(l: List[Int]): String = l match { 191 | case Nil => "No" 192 | case 5 +: _ => "Yes" 193 | case _ +: tail => contains5(tail) 194 | } 195 | ``` 196 | --- 197 | Sequence Pattern (cont'd) 198 | 199 | ``` 200 | @tailrec 201 | def contains5(l: List[Int]): String = l match { 202 | case Nil => "No" 203 | case 5 +: _ => "Yes" 204 | case _ +: tail => contains5(tail) 205 | } 206 | ``` 207 | 208 | Quiz: What is "+:"? 209 | 210 | --- 211 | It's an extractor! 212 | You can navigate to the source code in your IDE. 213 | Slightly simplified: 214 | 215 | ``` 216 | /** An extractor used to head/tail deconstruct sequences. */ 217 | object +: { 218 | def unapply[A](t: Seq[A]): Option[(A, Seq[A])] = 219 | if(t.isEmpty) None 220 | else Some(t.head -> t.tail) 221 | } 222 | ``` 223 | --- 224 | Extractor 225 | 226 | * An extractor is a Scala object with an unapply() method. 227 | * Think unapply() is "dual" of apply() 228 | * unapply takes the value you match on as parameter (if the type matches) 229 | * return something (we'll look into that) 230 | * the returned is matched with your pattern 231 | --- 232 | Extractor (cont'd) 233 | 234 | Let's write our own. 235 | 236 | ``` 237 | case class Time(hours: Int = 0, minutes: Int = 0) 238 | val (noon, morn, eve) = (Time(12), Time(9), Time(20)) 239 | 240 | object AM { 241 | def unapply(t: Time): Boolean = t.hours < 12 242 | } 243 | 244 | def greet(t:Any) = t match { 245 | case AM() => "Good Morning!" 246 | case _ => "Good Afternoon!" 247 | } 248 | ``` 249 | 250 | --- 251 | 252 | With variable binding. 253 | 254 | ``` 255 | 256 | object AM { 257 | def unapply(t: Time): Option[(Int,Int)] = 258 | if (t.hours < 12) Some(t.hours -> t.minutes) else None 259 | } 260 | 261 | def greet(t:Time) = t match { 262 | case AM(h,m) => f"Good Morning, it's $h%02d:$m%02d!" 263 | case _ => "Good Afternoon!" 264 | } 265 | ``` 266 | 267 | --- 268 | 269 | Just for demo purposes: 270 | if we return a pair, we can write the extractor inline.. 271 | 272 | ``` 273 | 274 | object AM { 275 | def unapply(t: Time): Option[(Int,Int)] = 276 | if (t.hours < 12) Some(t.hours -> t.minutes) else None 277 | } 278 | 279 | def greet(t:Time) = t match { 280 | case _ AM _ => "Good Morning!" 281 | case _ => "Good Afternoon!" 282 | } 283 | ``` 284 | 285 | --- 286 | 287 | In case you still have doubts.. 288 | 289 | ``` 290 | import annotation._ 291 | 292 | val l1 = List(1,2,3,4) 293 | val l2 = List(5) 294 | val l3 = List(5,8,6,4,9,12) 295 | 296 | @tailrec 297 | def contains5(l: List[Int]): String = l match { 298 | case Nil => "No" 299 | case +:(5, _) => "Yes" 300 | case +:(_, tail) => contains5(tail) 301 | } 302 | ``` 303 | 304 | Mystery of +: solved completely. It's simple ;) 305 | 306 | --- 307 | * yes/no - return Boolean 308 | * 2 or more variables - return Option[TupleN[..]] 309 | * 1 variable? There's no 1-tuple... 310 | 311 | ``` 312 | case class Time(hours: Int = 0, minutes: Int = 0) 313 | val (noon, morn, eve) = (Time(12), Time(9), Time(20)) 314 | 315 | object AM { 316 | def unapply(t: Time) = if (t.hours < 12) Some(t.hours) else None 317 | } 318 | 319 | def greet(t:Time) = t match { 320 | case AM(h) => s"Good Morning, the hour is $h!" 321 | case _ => "Good Afternoon!" 322 | } 323 | ``` 324 | 325 | --- 326 | 327 | Types.. 328 | 329 | ``` 330 | def greet(t:Any) = t match { 331 | case AM(h) => s"Good Morning, the hour is $h!" 332 | case _: Time => "Good Afternoon!" 333 | } 334 | ``` 335 | --- 336 | 337 | Extractors 338 | 339 | * What goes in? 340 | -- 341 | * Your match value 342 | * Where is it defined? 343 | -- 344 | * unapply() in your extractor object 345 | * what is returned? 346 | -- 347 | * No variables: Boolean 348 | * One variable: Option[A] 349 | * N variables: Option[TupleN[..]] 350 | -- 351 | * It gets even better! 352 | 353 | --- 354 | 355 | Say, I don't want to allocate an Option & Tuple every time I match. 356 | Let's just "pretend" we are an Option[TupleN[..]] 357 | 358 | ``` 359 | case class Time(hours: Int = 0, minutes: Int = 0) { 360 | def isEmpty = false 361 | def get = this 362 | def _1 = hours 363 | def _2 = minutes 364 | } 365 | 366 | val noTime = new Time { override def isEmpty = true } 367 | 368 | object AM { 369 | def unapply(t: Time): Time = if (t.hours < 12 ) t else noTime 370 | } 371 | 372 | def isAM(t:Time) = t match { 373 | case AM(h,m) => f"Good Morning, it's $h%02d:$m%02d!" 374 | case _ => "Good Afternoon!" 375 | } 376 | ``` 377 | 378 | --- 379 | 380 | * This concept is called name based extractors 381 | * It was introduced in Scala 2.11 382 | * It's an optimization, it might not make your code more readable 383 | * Remember: Premature optimization is the root of all evil! 384 | 385 | --- 386 | Do's and don'ts 387 | 388 | ``` 389 | import collection.immutable.Seq 390 | 391 | def contains5(l: Seq[Int]): String = l match { 392 | case Nil => "No" 393 | case 5 :: _ => "Yes" 394 | case _ :: tail => contains5(tail) 395 | } 396 | ``` 397 | 398 | Quiz: What's wrong with this? 399 | 400 | --- 401 | 402 | :: is a case class, the second class parameter is a List. 403 | You can navigate to the source code in your IDE. 404 | Slightly simplified: 405 | 406 | ``` 407 | case class ::[B](head: B, tl: List[B]) extends List[B] { 408 | override def tail : List[B] = tl 409 | override def isEmpty: Boolean = false 410 | } 411 | ``` 412 | --- 413 | 414 | Do's and don'ts 415 | 416 | * :: takes an element and a List 417 | * +: takes an element and a Seq 418 | 419 | * :: may look prettier than +:, but you might want to 420 | play it safe and use +: just in case. 421 | 422 | * by the way, what about :+ ? 423 | 424 | --- 425 | 426 | Do's and don'ts 427 | 428 | ``` 429 | def contains5(l: List[Int]): String = l match { 430 | case Nil => "No" 431 | case _ :+ 5 => "Yes" 432 | case init :+ _ => contains5(init) 433 | } 434 | ``` 435 | 436 | Although that may work, it may be very inefficient. 437 | 438 | --- 439 | 440 | Getting deep insights.. 441 | 442 | ``` 443 | case class Time(hours: Int = 0, minutes: Int = 0) 444 | val (noon, morn, eve) = (Time(12), Time(9), Time(20)) 445 | 446 | def checkTwice(x: Any): String = x match { 447 | case Time(h,m) if h > 11 => "Too late for breakfast" 448 | case Time(h,m) if h < 7 => "Too early" 449 | case _ => "undefined is not a function" 450 | } 451 | ``` 452 | 453 | --- 454 | 455 | $ scala -Xshow-phases 456 | phase name id description 457 | ---------- -- ----------- 458 | parser 1 parse source into ASTs, perform simple desugaring 459 | namer 2 resolve names, attach symbols to named trees 460 | packageobjects 3 load package objects 461 | typer 4 the meat and potatoes: type the trees 462 | \c patmat 5 translate match expressions 463 | superaccessors 6 add super accessors in traits and nested classes 464 | extmethods 7 add extension methods for inline classes 465 | pickler 8 serialize symbol tables 466 | refchecks 9 reference/override checking, translate nested objects 467 | uncurry 10 uncurry, translate function values to anonymous classes 468 | tailcalls 11 replace tail calls by jumps 469 | ... and so on until 25 470 | 471 | 472 | --- 473 | 474 | $ scala -Xprint:patmat 475 | 476 | * It's not super readable 477 | * But if you know what you look for, 478 | you might find it. 479 | * Play around with printing other phases, too! 480 | 481 | --- 482 | 483 | Case classes 484 | 485 | * While we still have the output on, let's check.. 486 | 487 | ``` 488 | case class T2(p1 : Int, p2: Int) 489 | 490 | case class T23(p1 : Int, p2: Int, p3: Int, p4: Int, p5: Int, 491 | p6: Int, p7: Int, p8: Int, p9: Int, p10: Int, 492 | p11: Int, p12: Int, p13: Int, p14: Int, 493 | p15: Int, p16: Int, p17: Int, p18: Int, 494 | p19: Int, p20: Int, p21 : Int, p22: Int, p23: Int) 495 | ``` 496 | 497 | --- 498 | 499 | ``` 500 | val t2 = T2(1,2) 501 | val t23 = T23(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23) 502 | 503 | case class T2(p1 : Int, p2: Int) 504 | 505 | def m(x: Any) = x match { 506 | case T2(_,_) => "T2" 507 | case T23(1,2,3,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_) 508 | => "What? How did this work?" 509 | case _ => "undefined is not a function" 510 | } 511 | 512 | --- 513 | 514 | Case Classes 515 | 516 | * For case classes, the unapply() method is not used! 517 | * Positive side effect: You can pattern match 518 | on case classes with > 22 fields. 519 | 520 | --- 521 | 522 | Still more on extractors.. 523 | 524 | What if you don't know the number of variables? 525 | 526 | 527 | --- 528 | 529 | Example.. 530 | 531 | ``` 532 | val s1 = "lightbend.com" 533 | val s2 = "www.scala-lang.org" 534 | 535 | object Domain { 536 | def unapplySeq(s: String) = Some(s.split("\\.").reverse) 537 | } 538 | 539 | def md(s: String) = s match { 540 | case Domain("com", _*) => "business" 541 | case Domain("org", _*) => "non-profit" 542 | } 543 | 544 | ``` 545 | 546 | --- 547 | 548 | Secret sauce: unapplySeq 549 | 550 | * Let's you return variable number of values 551 | * Think dual apply with varargs 552 | * Also: Some values and then sequence 553 | * apply where last argument is varargs 554 | 555 | --- 556 | 557 | By the way, scala.util.matching.Regex provides an unapplySeq method 558 | 559 | ``` 560 | val pattern = "a(b*)(c+)".r 561 | val s1 = "abbbcc" 562 | val s2 = "acc" 563 | val s3 = "abb" 564 | 565 | def mr(s: String) = s match { 566 | case pattern(a, bs) => s"""two groups "$a" "$bs"""" 567 | case pattern(a, bs, cs) => s"""three groups "$a" "$bs" "$cs"""" 568 | case _ => "no match" 569 | } 570 | 571 | ``` 572 | 573 | --- 574 | 575 | By the way, you can also use a string interpolator! 576 | 577 | Define a "t" interpolator .. 578 | 579 | ``` 580 | implicit class TimeStringContext (val sc : StringContext) { 581 | object t { 582 | def apply (args : Any*) : String = sc.s (args : _*) 583 | 584 | def unapplySeq (s : String) : Option[Seq[Int]] = { 585 | val regexp = """(\d{1,2}):(\d{1,2})""".r 586 | regexp.unapplySeq(s).map(_.map(s => s.toInt)) 587 | } 588 | } 589 | } 590 | ``` 591 | 592 | --- 593 | 594 | .. and use it in pattern match! 595 | 596 | 597 | ``` 598 | def isTime(s: String) = s match { 599 | case t"$hours:$minutes" => Time(hours, minutes) 600 | case _ => "Not a time!" 601 | } 602 | ``` 603 | 604 | --- 605 | 606 | @switch annotation 607 | 608 | ``` 609 | import annotation._ 610 | 611 | def wsw(x: Int): String = (x: @switch) match { 612 | case 8 => "Yes" 613 | case 9 => "No" 614 | case 10 => "No" 615 | } 616 | ``` 617 | --- 618 | 619 | @switch annotation (cont'd) 620 | 621 | ``` 622 | import annotation._ 623 | 624 | def wsw(x: Any): String = (x: @switch) match { 625 | case 8 => "Yes" 626 | case "9" => "No" 627 | case "10" => "No" 628 | } 629 | ``` 630 | 631 | Would give a warning (outside of presentation) 632 | 633 | --- 634 | 635 | @switch annotation (cont'd) 636 | 637 | Verifies that the match expression can be compiled 638 | to a tableswitch or lookupswitch 639 | and issues an error if it instead compiles 640 | into a series of conditional expressions. 641 | 642 | --- 643 | 644 | @switch annotation 645 | 646 | ``` 647 | import annotation._ 648 | 649 | def wsw(x: Any): String = (x: @switch) match { 650 | case 8 => "Yes" 651 | case 9 => "No" 652 | case 10 => "No" 653 | } 654 | 655 | 656 | def wosw(x: Int): String = x match { 657 | case 8 => "Yes" 658 | case 9 => "No" 659 | case 10 => "No" 660 | } 661 | 662 | ``` 663 | 664 | --- 665 | 666 | Pattern matching and type erasure 667 | 668 | ``` 669 | 670 | def print[A](xs: List[A]) = xs match { 671 | case _: List[String] => "list of strings" 672 | case _: List[Int] => "list of ints" 673 | } 674 | ``` 675 | 676 | --- 677 | 678 | Pattern matching and type erasure (cont'd) 679 | 680 | ``` 681 | import scala.reflect._ 682 | def print[A: ClassTag](xs: List[A]) = classTag[A].runtimeClass match { 683 | case c if c == classOf[String] => "List of strings" 684 | case c if c == classOf[Int] => "List of ints" 685 | } 686 | 687 | ``` 688 | --- 689 | 690 | Match by type 691 | ``` 692 | def t(x:Any) = x match { 693 | case _ : Int => "Integer" 694 | case _ : String => "String" 695 | } 696 | ``` 697 | 698 | --- 699 | 700 | Match alternatives 701 | 702 | ``` 703 | def alt(x:Any) = x match { 704 | case 1 | 2 | 3 | 4 | 5 | 6 => "little" 705 | case 100 | 200 => "big" 706 | } 707 | ``` 708 | 709 | --- 710 | 711 | Simulating union types? 712 | 713 | ``` 714 | 715 | def talt(x:Any) = x match { 716 | case stringOrInt @ (_ : Int | _ : String) => 717 | s"Union String | Int: $stringOrInt" 718 | case _ => "unknown" 719 | } 720 | ``` 721 | 722 | -- 723 | * Unfortunately the compile time type of stringOrInt is Any 724 | * No union types in Scala (yet ;) 725 | 726 | 727 | --- 728 | 729 | 730 | \! 731 | | \gThank You! 732 | 733 | | @lutzhuehnken 734 | 735 | 736 | | Lightbend 737 | --------------------------------------------------------------------------------