diff --git a/.gitignore b/.gitignore index 9e79245..1935c30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,4 @@ -# macOS -.DS_Store - -# sbt specific +# build dist/* target/ lib_managed/ @@ -14,9 +11,12 @@ project/local-plugins.sbt .ensime_cache/ .sbt-scripted/ local.sbt +.scala-build -# Bloop -.bsp +# IDEA +.idea +.idea_modules +/.worksheet/ # VS Code .vscode/ @@ -26,7 +26,9 @@ local.sbt .metals/ metals.sbt -# IDEA -.idea -.idea_modules -/.worksheet/ +# Bloop +.bsp + + +# macOS +.DS_Store diff --git a/README.md b/README.md index deb9b54..d8d6e74 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ # scala notes +Notes on studying Scala, mainly from reading documentation such as: +- The [Scala Book](https://docs.scala-lang.org/scala3/book/introduction.html) +- The [Scala 3 Reference](https://docs.scala-lang.org/scala3/reference/) +- The [Munit docs](https://scalameta.org/munit/docs/getting-started.html) +- A few blogs and videos, see the [footnotes](#References) for all references + +Aside from this README, the [tests](src/test/scala/Suite.scala) contain many runnable examples of some concepts explored here and many only explored there. + ## Functional programming > **Functional Programming** is programming with functions. When we say functions, we mean mathematical functions, which are supposed to have the following properties: @@ -46,7 +54,7 @@ The last these high-level, broader types explained in the "A First Look at Types From these two come the more specific types: -`AnyVal` represents **non-nullable value types** such as `Double`, `Float`, `Long`, `Int`, `Short`, `Byte`, `Char`, `Unit`, and `Boolean`. +`Int | AnyVal` represents **non-nullable value types** such as `Double`, `Float`, `Long`, `Int`, `Short`, `Byte`, `Char`, `Unit`, and `Boolean`. `AnyRef` represents **reference types**, which includes all non-value types and all user-defined types. It corresponds to Java's `java.lang.Object`.[^14] This includes strings, classes, objects, functions and compound types like lists and arrays. @@ -165,7 +173,7 @@ scalacOptions ++= Seq( ), ``` -Now for `ǹull` to work a value type must use a union type:[^15] +Now for `null` to work a value type must use a union type:[^15] ```scala val x: String = null // error: found `Null`, but required `String` @@ -282,16 +290,27 @@ class Dog extends Animal, HasTail: ``` ### Case class + +Using a case class has the benefit that the compiler will generate an`unapply` method for pattern matching, a `copy` method to create modified copies of an instance, `equals` and `hashCode` methods, a `toString` method.[^21] + ```scala -final case class Person( - name: String, - surname: String, -) +// Case classes can be used as patterns +christina match + case Person(n, r) => println("name is " + n) + +// `equals` and `hashCode` methods generated for you +val hannah = Person("Hannah", "niece") +christina == hannah // false -def p(name: String, surname: String): Person = - Person(name, surname) +// `toString` method +println(christina) // Person(Christina,niece) -val jane = p("Jane", "Doe") +// built-in `copy` method +case class BaseballTeam(name: String, lastWorldSeriesWin: Int) +val cubs1908 = BaseballTeam("Chicago Cubs", 1908) +val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016) +// result: +// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016) ``` ### Pattern matching @@ -324,6 +343,28 @@ p match > There’s much more to pattern matching in Scala. Patterns can be nested, results of patterns can be bound, and pattern matching can even be user-defined. See the pattern matching examples in the [Control Structures chapter](https://docs.scala-lang.org/scala3/book/control-structures.html) for more details.[^7] +When matching a catch-all, it's possible to use the matched value in the body of the match case: + +```scala +i match + case 0 => println("1") + case 1 => println("2") + case what => println(s"You gave me: $what") +``` + +For this to work, the variable name on the left must be lowercase. If uppercase, a corresponding variable will be used from the scope:[^20] + +```scala +val N = 42 +val r = i match + case 0 => println("1") + case 1 => println("2") + case N => println("42") + case n => println(s"You gave me: $n" ) + +r // "42" +``` + ### String interpolation ```scala println(s"2 + 2 = ${2 + 2}") // "2 + 2 = 4" @@ -354,6 +395,76 @@ val x = if a < b then a else b > An expression returns a result, while a statement does not. Statements are typically used for their side-effects, such as using `println` to print to the console.[^5] +#### Expression-oriented programming + +> […] lines of code that don’t return values are called statements, and they’re used for their side-effects. For example, these [last two] lines of code don’t return values, so they’re used for their side effects: + +```scala +val minValue = if a < b then a else b // expression +if a == b then action() // statement +println("Hello") // statement +``` + +> The first example runs the `action` method as a side effect when `a` is equal to `b`. The second example is used for the side effect of printing a string to STDOUT. As you learn more about Scala you’ll find yourself writing more _expressions_ and fewer _statements_.[^18] + +#### Pattern matching + +```scala +import scala.annotation.switch + +@switch val i = 2 + +val day = i match + case 0 => "Sunday" + case 1 => "Monday" + case 2 => "Tuesday" + case 3 => "Wednesday" + case 4 => "Thursday" + case 5 => "Friday" + case 6 => "Saturday" + case _ => "invalid day" // the default, catch-all +``` + +> When writing simple `match` expressions like this, it’s recommended to use the `@switch` annotation on the variable `i`. This annotation provides a compile-time warning if the switch can’t be compiled to a `tableswitch` or `lookupswitch`, which are better for performance.[^19] + +There are many different forms of patterns that can be used to write match expressions. Examples include:[^22] + +```scala +def pattern(x: Matchable): String = x match + + // constant patterns + case 0 => "zero" + case true => "true" + case "hello" => "you said 'hello'" + case Nil => "an empty List" + + // sequence patterns + case List(0, _, _) => "a 3-element list with 0 as the first element" + case List(1, _*) => "list, starts with 1, has any number of elements" + case Vector(1, _*) => "vector, starts w/ 1, has any number of elements" + + // tuple patterns + case (a, b) => s"got $a and $b" + case (a, b, c) => s"got $a, $b, and $c" + + // constructor patterns + case Person(first, "Alexander") => s"Alexander, first name = $first" + case Dog("Zeus") => "found a dog named Zeus" + + // type test patterns + case s: String => s"got a string: $s" + case i: Int => s"got an int: $i" + case f: Float => s"got a float: $f" + case a: Array[Int] => s"array of int: ${a.mkString(",")}" + case as: Array[String] => s"string array: ${as.mkString(",")}" + case d: Dog => s"dog: ${d.name}" + case list: List[?] => s"got a List: $list" + case m: Map[?, ?] => m.toString + + // the default wildcard pattern + case _ => "Unknown" +``` + ### Domain Modelling > Classes can also have methods and additional fields that are not part of constructors. They are defined in the body of the class. The body is initialized as part of the default constructor:[^8] @@ -828,3 +939,8 @@ Suppress `info` level logging when running and watching runs: [^15]: [^16]: [^17]: +[^18]: +[^19]: +[^20]: +[^21]: +[^22]: diff --git a/src/test/scala/.scala-build/ide-inputs.json b/src/test/scala/.scala-build/ide-inputs.json deleted file mode 100644 index 8fb6eea..0000000 --- a/src/test/scala/.scala-build/ide-inputs.json +++ /dev/null @@ -1 +0,0 @@ -{"args":["/home/juno/git/hub/scala-notes/src/test/scala/Suite.scala"]} \ No newline at end of file diff --git a/src/test/scala/.scala-build/ide-options-v2.json b/src/test/scala/.scala-build/ide-options-v2.json deleted file mode 100644 index 9e26dfe..0000000 --- a/src/test/scala/.scala-build/ide-options-v2.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/src/test/scala/.scala-build/stacktraces/1700421247-13054772439675899651.log b/src/test/scala/.scala-build/stacktraces/1700421247-13054772439675899651.log deleted file mode 100644 index 39a298b..0000000 --- a/src/test/scala/.scala-build/stacktraces/1700421247-13054772439675899651.log +++ /dev/null @@ -1,32 +0,0 @@ -java.util.concurrent.TimeoutException: Future timed out after [30 seconds] - scala.concurrent.impl.Promise$DefaultPromise.tryAwait0(Promise.scala:248) - scala.concurrent.impl.Promise$DefaultPromise.result(Promise.scala:261) - scala.concurrent.Await$.$anonfun$result$1(package.scala:201) - scala.concurrent.BlockContext$DefaultBlockContext$.blockOn(BlockContext.scala:62) - scala.concurrent.Await$.result(package.scala:124) - bloop.rifle.internal.Operations$.timeout(Operations.scala:531) - bloop.rifle.internal.Operations$.about(Operations.scala:499) - bloop.rifle.BloopRifle$.getCurrentBloopVersion(BloopRifle.scala:156) - bloop.rifle.BloopServer$.ensureBloopRunning(BloopServer.scala:111) - bloop.rifle.BloopServer$.bsp(BloopServer.scala:155) - bloop.rifle.BloopServer$.buildServer(BloopServer.scala:185) - scala.build.compiler.BloopCompilerMaker.$anonfun$1(BloopCompilerMaker.scala:35) - scala.build.compiler.BloopCompiler.(BloopCompiler.scala:15) - scala.build.compiler.BloopCompilerMaker.create$$anonfun$1(BloopCompilerMaker.scala:38) - scala.util.Try$.apply(Try.scala:210) - scala.build.compiler.BloopCompilerMaker.create(BloopCompilerMaker.scala:38) - scala.build.compiler.ScalaCompilerMaker.withCompiler(ScalaCompilerMaker.scala:29) - scala.build.compiler.ScalaCompilerMaker.withCompiler$(ScalaCompilerMaker.scala:7) - scala.build.compiler.BloopCompilerMaker.withCompiler(BloopCompilerMaker.scala:13) - scala.build.Build$.build(Build.scala:608) - scala.cli.commands.test.Test$.runCommand(Test.scala:180) - scala.cli.commands.test.Test$.runCommand(Test.scala:58) - scala.cli.commands.ScalaCommand.run(ScalaCommand.scala:368) - scala.cli.commands.ScalaCommand.run(ScalaCommand.scala:350) - caseapp.core.app.CaseApp.main(CaseApp.scala:157) - scala.cli.commands.ScalaCommand.main(ScalaCommand.scala:335) - caseapp.core.app.CommandsEntryPoint.main(CommandsEntryPoint.scala:166) - scala.cli.ScalaCliCommands.main(ScalaCliCommands.scala:125) - scala.cli.ScalaCli$.main0(ScalaCli.scala:269) - scala.cli.ScalaCli$.main(ScalaCli.scala:108) - scala.cli.ScalaCli.main(ScalaCli.scala) diff --git a/src/test/scala/Suite.scala b/src/test/scala/Suite.scala index 49adfce..3fabc36 100644 --- a/src/test/scala/Suite.scala +++ b/src/test/scala/Suite.scala @@ -1,6 +1,8 @@ import scala.concurrent.duration.Duration import scala.collection.mutable.ArrayBuffer import javax.lang.model.`type`.UnknownTypeException +import scala.collection.mutable.ListBuffer +import scala.annotation.switch abstract class BaseSuite extends munit.FunSuite { override val munitTimeout = Duration(10, "sec") @@ -23,12 +25,12 @@ class BaseFixture extends BaseSuite { ) f.test("base suite is extended") { _ => - assertEquals(clue(on_base), "on base") + assertEquals(on_base, "on base") } f.test("fixture setup context is set") { _ => - assertEquals(clue(on_base), "on base") - assertEquals(clue(on_setup), "on setup") + assertEquals(on_base, "on base") + assertEquals(on_setup, "on setup") } f.test(".fail test fails".fail) { _ => @@ -40,13 +42,85 @@ class ControlStructures extends BaseFixture { f.test("control structures are expressions") { _ => val is = if 1 > 0 then "greater" else "less" - assertEquals(clue(is), clue("greater")) + assertEquals(is, "greater") + } + + f.test("scala 3 ifs don't need braces") { _ => + + val x = + if 0 == 1 then + val a = 2 + a + 3 + else if 2 == 2 then + val b = 3 + b - 1 + else + val c = 4 + c / 2 + end if // optional + + assertEquals(x, 2) } f.test("for loop generator") { _ => var ints2 = ArrayBuffer.empty[Int] + var ints3 = ArrayBuffer.empty[Int] + + // one-liner for i <- ints do ints2 += i - assertEquals(clue(ints2), clue(ArrayBuffer(1, 2, 3, 4, 5))) + assertEquals(ints2, ArrayBuffer(1, 2, 3, 4, 5)) + + // multi-line + for i <- ints + do + val j = i * -1 + ints3 += j + + assertEquals(ints3, ArrayBuffer(-1, -2, -3, -4, -5)) + } + + f.test("for loops can have multiple generators") { _ => + + var list = ListBuffer.empty[Char] + + for + i <- 'a' to 'b' + j <- 'c' to 'd' + k <- 'e' to 'f' + do + list += i + list += j + list += k + + assertEquals(list, ListBuffer( + 'a', 'c', 'e', + 'a', 'c', 'f', + 'a', 'd', 'e', + 'a', 'd', 'f', + 'b', 'c', 'e', + 'b', 'c', 'f', + 'b', 'd', 'e', + 'b', 'd', 'f', + )) + } + + f.test("for loops can iterate maps") { _ => + + var list = ListBuffer.empty[String] + + val states = Map( + "AP" -> "Amapá", + "MT" -> "Mato Grosso", + "CE" -> "Ceará", + ) + + for (abbrev, full_name) <- states do list += s"$abbrev: $full_name" + + assertEquals(list, ListBuffer( + "AP: Amapá", + "MT: Mato Grosso", + "CE: Ceará", + )) } f.test("for loop guards") { _ => @@ -55,7 +129,7 @@ class ControlStructures extends BaseFixture { if i > 3 do greater += i - assertEquals(clue(greater), clue(ArrayBuffer(4, 5))) + assertEquals(greater, ArrayBuffer(4, 5)) } f.test("for with multiple guards and generators") { _ => @@ -72,8 +146,8 @@ class ControlStructures extends BaseFixture { last_i = i last_j = j - assertEquals(clue(last_i), clue(2)) - assertEquals(clue(last_j), clue('b')) + assertEquals(last_i, 2) + assertEquals(last_j, 'b') } f.test("for yield returns the same data structure with results") { _ => @@ -81,24 +155,58 @@ class ControlStructures extends BaseFixture { assertEquals(doubles, ArrayBuffer(2, 4, 6, 8, 10)) } - f.test("for can have multiple yield expressions") { _ => + f.test("yield expressions can have a block body") { _ => + val names = List("_olivia", "_walter", "_peter") + + val cap_names = for name <- names yield + val name_without_underscore = name.drop(1) + name_without_underscore.capitalize + + assertEquals(cap_names, List("Olivia", "Walter", "Peter")) + } + + f.test("for yield is equivalent to a map expression") { _ => + val map_list = (10 to 22).map(_ * 2) + val for_list = for i <- 10 to 22 yield i * 2 + assertEquals(map_list, for_list) + } + + f.test("for can yield multiple values") { _ => val results = ArrayBuffer.empty[Int] - val result = for { + val result = for int <- ints if int % 2 == 0 - } yield { + yield results += int val square = int * int results += square results += square * 2 - } - assertEquals(clue(results), clue(ArrayBuffer(2, 4, 8, 4, 16, 32))) + assertEquals(results, ArrayBuffer(2, 4, 8, 4, 16, 32)) + } + + f.test("while loop syntax") { _ => + var i = 0 + + while i < 9 do + i -= 1 + i += 2 + + assertEquals(i, 9) + } + + f.test("while loop with block return") { _ => + val result = { + var x = 0 + while x < 13 do x += 3 + x + } + assertEquals(result, 15) } f.test("basic match expression") { _ => - val i = 3 + @switch val i = 3 var number = "" i match @@ -107,11 +215,12 @@ class ControlStructures extends BaseFixture { case 3 => number = "three" case _ => number = "other" - assertEquals(clue(number), "three") + assertEquals(number, "three") } - f.test("match expressions return values") { _ => - val i = 2 + f.test("match structures are expressions") { _ => + + @switch val i = 2 val number = i match case 1 => "one" @@ -119,7 +228,7 @@ class ControlStructures extends BaseFixture { case 3 => "three" case _ => "other" - assertEquals(clue(number), "two") + assertEquals(number, "two") } @@ -131,37 +240,110 @@ class ControlStructures extends BaseFixture { case l: List[?] => "List" case _ => "Unexpected type" - assertEquals(clue(matchType("a string")), clue("String containing: a string")) - assertEquals(clue(matchType(4.92)), clue("Double")) - assertEquals(clue(matchType(List(3, 2, 1))), clue("List")) + assertEquals(matchType("a string"), "String containing: a string") + assertEquals(matchType(4.92), "Double") + assertEquals(matchType(List(3, 2, 1)), "List") } - f.test("division by zero throws ArithmeticException") { _ => + f.test("a lowercase variable on match left captures a default") { _ => + val i = 3 + val r = i match + case 0 => '0' + case 1 => '1' + case 2 => '2' + case x => x + + assertEquals(r, 3) + } + + f.test("an uppercase variable on match left uses scope") { _ => + val i, Y = 3 + val r = i match + case 0 => '0' + case 1 => '1' + case 2 => '2' + case Y => '3' + case x => 'x' // unreachable + + assertEquals(r, '3') + } + + f.test("match can handle multiple values in a single line") { _ => + def even_or_odd(n: Int): String = + n match + case 1 | 3 | 5 | 7 | 9 => "odd" + case 2 | 4 | 6 | 8 | 10 => "even" + case n => s"$n is out of bounds" + + assertEquals(even_or_odd(4), "even") + assertEquals(even_or_odd(7), "odd") + assertEquals(even_or_odd(31), "31 is out of bounds") + } + + f.test("match cases can have guards") { _ => + + def assert(f: Int => String) = + val pairs = Map( + 1 -> "one", + 5 -> "between 2 and 5", + 10 -> "10 or greater", + 31 -> "10 or greater", + -8 -> "0 or less", + ) + + for (number, range) <- pairs do assertEquals(f(number), range) + + def get_range(i: Int) = + i match + case 1 => "one" + case a if a > 1 && a < 6 => "between 2 and 5" + case b if b > 5 && b < 10 => "between 6 and 9" + case c if c >= 10 => "10 or greater" + case _ => "0 or less" + + assert(get_range) + + // same example using a more idiomatic, readable syntax: + def get_range_2(i: Int) = + i match + case 1 => "one" + case a if 1 to 5 contains a => "between 2 and 5" + case b if 6 to 9 contains b => "between 6 and 9" + case c if c >= 10 => "10 or greater" + case _ => "0 or less" + + assert(get_range_2) + } + + f.test("fields from classes can be extracted") { _ => + + case class Person(name: String) + + def speak(p: Person) = p match + case Person(name) if name == "Fred" => s"$name says, Yubba dubba doo" + case Person(name) if name == "Bam Bam" => s"$name says, Bam bam!" + + assertEquals(speak(Person("Fred")), "Fred says, Yubba dubba doo") + } + + f.test("try structures are expressions") { _ => var always: String = "" val exception = try 2/0 catch - case nfe: NumberFormatException => "Got a NumberFormatException" - case nfe: ArithmeticException => "Got an ArithmeticException" + case e: NumberFormatException => "Got a NumberFormatException" + case e: ArithmeticException => "Got an ArithmeticException" finally always = "This always executes" - assertEquals(clue(exception), clue("Got an ArithmeticException")) - assertEquals(clue(always), clue("This always executes")) + assertEquals(exception, "Got an ArithmeticException") + assertEquals(always, "This always executes") } - f.test("while loop with block return") { _ => - val result = { - var x = 0 - while x < 13 do x += 3 - x - } - assertEquals(clue(result), 15) - } } - + class DomainModelling extends BaseFixture { // Object-oriented @@ -279,7 +461,4 @@ class DomainModelling extends BaseFixture { assertEquals(result, List(40, 50, 60)) } - - - - } +}