From e8ccf783b6ccf3e1440cf09f0d4d8a0a0b5c966d Mon Sep 17 00:00:00 2001 From: jultty Date: Sun, 19 Nov 2023 17:44:51 -0300 Subject: [PATCH] Scala Book: A Taste of Scala Covers Domain Modeling through the section summary --- README.md | 384 +++++++++++++++++- build.sbt | 2 +- src/test/scala/.scala-build/ide-inputs.json | 1 + .../scala/.scala-build/ide-options-v2.json | 1 + .../1700421247-13054772439675899651.log | 32 ++ src/test/scala/Suite.scala | 128 +++++- 6 files changed, 542 insertions(+), 6 deletions(-) create mode 100644 src/test/scala/.scala-build/ide-inputs.json create mode 100644 src/test/scala/.scala-build/ide-options-v2.json create mode 100644 src/test/scala/.scala-build/stacktraces/1700421247-13054772439675899651.log diff --git a/README.md b/README.md index 2a8168b..cbd0ec1 100644 --- a/README.md +++ b/README.md @@ -38,15 +38,75 @@ val nums = List(1, 2, 3) val p = Person("Martin", "Odersky") // not a built-in ``` -### Lambdas +### Methods + +```scala +def methodName(param1: Type1, param2: Type2): ReturnType = + // body +``` + +```scala +def concatenate(s1: String, s2: String): String = s1 + s2 +def sum(a: Int, b: Int) = a + b // return types can be inferred +``` + +Parameters can have default values: + ```scala -nums.map(i => i * 2) // long form -nums.map(_ * 2) // short form +def makeConnection(url: String, timeout: Int = 5000): Unit = + println(s"url=$url, timeout=$timeout") + +makeConnection("https://localhost") // url=http://localhost, timeout=5000 +makeConnection("https://localhost", 2500) // url=http://localhost, timeout=2500 +``` + +Parameters can be set by their names: + +```scala +makeConnection( + url = "https://localhost", + timeout = 2500 +) +``` + +#### Extensions + +> The `extension` keyword declares that you’re about to define one or more extension methods on the parameter that’s put in parentheses. As shown with this example, the parameter `s` of type `String` can then be used in the body of your extension methods. +> This next example shows how to add a `makeInt` method to the `String` class. Here, `makeInt` takes a parameter named `radix`. The code doesn’t account for possible string-to-integer conversion errors, but skipping that detail, the examples show how it works:[^12] + +```scala +extension (s: String) + def makeInt(radix: Int): Int = Integer.parseInt(s, radix) + +"1".makeInt(2) // Int = 1 +"10".makeInt(2) // Int = 2 +"100".makeInt(2) // Int = 4 +``` + +### Lambdas +```scala +// long form +nums.map(i => i * 2) nums.filter(i => i > 1) + +// short form +nums.map(_ * 2) nums.filter(_ > 1) ``` +```scala +val a = List(1, 2, 3).map(i => i * 2) // List(2,4,6) +val b = List(1, 2, 3).map(_ * 2) // List(2,4,6) +``` + +```scala +def double(i: Int): Int = i * 2 + +val a = List(1, 2, 3).map(i => double(i)) // List(2,4,6) +val b = List(1, 2, 3).map(double) // List(2,4,6) +``` + ### Higher-order functions ```scala val xs = List(1, 2, 3, 4, 5) @@ -59,6 +119,19 @@ xs.takeWhile(_ < 3) // List(1, 2) > In those examples, the values in the list can’t be modified. The List class is immutable, so all of those methods return new values, as shown by the data in each comment.[^3] +> […] these functions don’t mutate the collection they’re called on; instead, they return a new collection with the updated data. As a result, it’s also common to chain them together in a “fluent” style to solve problems. + +```scala +val nums = (1 to 10).toList // List(1,2,3,4,5,6,7,8,9,10) + +// methods can be chained together as needed +val x = nums.filter(_ > 3) + .filter(_ < 7) + .map(_ * 10) + +// result: x == List(40, 50, 60) +``` + ### Traits and classes ```scala trait Animal: @@ -145,6 +218,305 @@ 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] +### 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] + +```scala +class Person(var firstName: String, var lastName: String): + + println("initialization begins") + val fullName = firstName + " " + lastName + + // a class method + def printFullName: Unit = + // access the `fullName` field, which is created above + println(fullName) + + printFullName + println("initialization ends") +``` + +> class constructor parameters can also have default values: + +```scala +class Socket(val timeout: Int = 5_000, val linger: Int = 5_000): + override def toString = s"timeout: $timeout, linger: $linger" + +val s = Socket() // timeout: 5000, linger: 5000 +val s = Socket(2_500) // timeout: 2500, linger: 5000 +val s = Socket(10_000, 10_000) // timeout: 10000, linger: 10000 +val s = Socket(timeout = 10_000) // timeout: 10000, linger: 5000 +val s = Socket(linger = 10_000) // timeout: 5000, linger: 10000 +``` + +#### Auxiliary constructors + +```scala +import java.time.* + +// [1] the primary constructor +class Student( + var name: String, + var govtId: String +): + private var _applicationDate: Option[LocalDate] = None + private var _studentId: Int = 0 + + // [2] a constructor for when the student has completed + // their application + def this( + name: String, + govtId: String, + applicationDate: LocalDate + ) = + this(name, govtId) + _applicationDate = Some(applicationDate) + + // [3] a constructor for when the student is approved + // and now has a student id + def this( + name: String, + govtId: String, + studentId: Int + ) = + this(name, govtId) + _studentId = studentId +``` + +> An object is a class that has exactly one instance. It’s initialized lazily when its members are referenced, similar to a `lazy val`. Objects in Scala allow grouping methods and fields under one namespace, similar to how you use `static` members on a class in Java, Javascript (ES6), or `@staticmethod` in Python. + +> Declaring an `object` is similar to declaring a `class`. Here’s an example of a “string utilities” object that contains a set of methods for working with strings:[^9] + +```scala +object StringUtils: + def truncate(s: String, length: Int): String = s.take(length) + def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*") + def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty + +StringUtils.truncate("Chuck Bartowski", 5) // "Chuck" + +import StringUtils.* // or `import StringUtils.{truncate, containsWhitespace, isNullOrEmpty}` +truncate("Chuck Bartowski", 5) // "Chuck" +containsWhitespace("Sarah Walker") // true +isNullOrEmpty("John Casey") // false +``` + +> Objects can also contain fields, which are also accessed like static members: + +```scala +object MathConstants: + val PI = 3.14159 + val E = 2.71828 + +println(MathConstants.PI) // 3.14159 +``` + +> An `object` that has the same name as a class, and is declared in the same file as the class, is called a _“companion object_.” Similarly, the corresponding class is called the object’s companion class. A companion class or object can access the private members of its companion. + +> Companion objects are used for methods and values that are not specific to instances of the companion class. For instance, in the following example the class `Circle` has a member named `area` which is specific to each instance, and its companion object has a method named `calculateArea` that’s (a) not specific to an instance, and (b) is available to every instance:[^10] + +```scala +import scala.math.* + +class Circle(val radius: Double): + def area: Double = Circle.calculateArea(radius) + +object Circle: + private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0) + +val circle1 = Circle(5.0) +circle1.area +``` + +**GPT:** _What is the advantage of defining a companion object in Scala instead of defining the companion object's attributes and methods in the class itself?_ + > The advantage of defining a companion object in Scala instead of defining + the companion object's attributes and methods in the class itself is that + the companion object can serve as a separate namespace for methods and + attributes that are not specific to any instance of the class. + + > 1. Namespace separation: By defining a companion object, you can separate + class-level methods and attributes from instance-level methods and attributes. + This helps in organizing the code and makes it clearer to understand which + methods are intended to be called on the class itself and which are meant to + be called on instances of the class. + > 2. Accessibility: Generally, the attributes and methods defined in a + companion object are accessible directly on the class itself, without the + need to create an instance of the class. This can be useful when you want to + provide utility methods or access some shared state that is not tied to any + specific instance. + > 3. Static-like behavior: In Scala, the companion object is a singleton + instance which allows you to define methods and attributes that can be + accessed without instantiating the class. This provides a similar behavior + to static methods and attributes in other languages. + > 4. Initialization code: Companion objects can be used to define code that + gets executed when the class is first accessed or when the companion object + itself is loaded. This can be useful for initializing shared resources or + performing some one-time setup. + +> Objects can also be used to implement traits to create modules. This technique takes two traits and combines them to create a concrete `object`: + +```scala +trait AddService: + def add(a: Int, b: Int) = a + b + +trait MultiplyService: + def multiply(a: Int, b: Int) = a * b + +// implement those traits as a concrete object +object MathService extends AddService, MultiplyService + +// use the object +import MathService.* +println(add(1,1)) // 2 +println(multiply(2,2)) // 4 +``` + +#### Collections + +##### Lists + +> Here are some examples that use the `List` class, which is an immutable, linked-list class. These examples show different ways to create a populated `List`:[^13] + +```scala +val a = List(1, 2, 3) // a: List[Int] = List(1, 2, 3) + +// Range methods +val b = (1 to 5).toList // b: List[Int] = List(1, 2, 3, 4, 5) +val c = (1 to 10 by 2).toList // c: List[Int] = List(1, 3, 5, 7, 9) +val e = (1 until 5).toList // e: List[Int] = List(1, 2, 3, 4) +val f = List.range(1, 5) // f: List[Int] = List(1, 2, 3, 4) +val g = List.range(1, 10, 3) // g: List[Int] = List(1, 4, 7) +``` + +List methods: + +```scala +val a = List(10, 20, 30, 40, 10) // List(10, 20, 30, 40, 10) + +a.drop(2) // List(30, 40, 10) +a.dropWhile(_ < 25) // List(30, 40, 10) +a.filter(_ < 25) // List(10, 20, 10) +a.slice(2,4) // List(30, 40) +a.tail // List(20, 30, 40, 10) +a.take(3) // List(10, 20, 30) +a.takeWhile(_ < 30) // List(10, 20) + +// flatten +val a = List(List(1,2), List(3,4)) +a.flatten // List(1, 2, 3, 4) + +// map, flatMap +val nums = List("one", "two") +nums.map(_.toUpperCase) // List("ONE", "TWO") +nums.flatMap(_.toUpperCase) // List('O', 'N', 'E', 'T', 'W', 'O') + +val firstTen = (1 to 10).toList // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) +firstTen.reduceLeft(_ + _) // 55 +firstTen.foldLeft(100)(_ + _) // 155 (100 is a “seed” value) +``` + +##### Tuples + +> […] a collection of different types in the same container. For example, + +```scala +val t = (11, "eleven", Person("Eleven")) +``` + +> […] access its values by binding them to variables, + +```scala +val (num, str, person) = t + +// result: +// val num: Int = 11 +// val str: String = eleven +// val person: Person = Person(Eleven) +``` + +> or access them by number: + +```scala +t(0) // 11 +t(1) // "eleven" +t(2) // Person("Eleven") +``` + +See also: + - + - + + + +#### Algebraic Data Types + +Algebraic data types are types composed of other types. **Sum types** combine different and exclusive alternatives or branches, typically implemented using enums or traits. **Product types** are formed by combining multiple types into a single type, for example a tuple defined as `(String, Int)`. + +See these minimal examples: + +```scala +// sum type +enum Natural: + case Zero + case Successor(predecessor: Natural) + +// product type +case class Person(name: String, age: Int) +``` + +In the examples above, a `Natural` type is one of either `Zero` or `Successor`. No two options, nor other types, will be accepted. In the product type, both a `String` and an `Int` type are combined. + +They are called sum and product types because of how you can obtain the total number of values they can contain by either multiplying the parameters (for product types) or adding up the alternatives (for sum types). + +In Scala, product types are typically defined using case classes: + +```scala +case class WeatherForecast(latitude: Double, Longitude) +``` + +Considered as a function, the class above could be written as `(Double, Double) => WeatherForecast`, meaning it takes two doubles and returns a `WeatherForecast`. + +We can also say that it is a product type, since it is not an exclusive choice between alternatives but a composition of two doubles. + +Finally, because for each pair of doubles you can define a different `WeatherForecast`, we can also say that `WeatherForecast` is a Cartesian product between its arguments, as in `type WeatherForecast = Double × Double`, since this is the number of possible unique `WeatherForecast` definitions.[^11] + +It is also possible to have a **hybrid type** when a sum type is comprised of many product types: + +```scala +sealed trait Response // sum type +case class Valid(code: Int, body: String) extends Response // product type +case class Invalid(error: String, description: String) extends Response // product type +``` + +One advantage of using ADTs is that you have more control over what types, ranges or values are accepted by leveraging type systems and type definitions, rather than ad-hoc validation that must be repeated every time a value is passed into a function. This reduces the complexity of code and increases testability. + +##### Product Types + +> A product type is an algebraic data type (ADT) that only has one shape, for example a singleton object, represented in Scala by a `case` object; or an immutable structure with accessible fields, represented by a `case` class. + +> A `case` class has all of the functionality of a `class`, and also has additional features baked in that make them useful for functional programming. When the compiler sees the `case` keyword in front of a `class` it has these effects and benefits: + +- > Case class constructor parameters are public `val` fields by default, so the fields are immutable, and accessor methods are generated for each parameter. +- > An `unapply` method is generated, which lets you use case classes in more ways in `match` expressions. +- > A `copy` method is generated in the class. This provides a way to create updated copies of the object without changing the original object. +- `equals` and `hashCode` methods are generated to implement structural equality. +- > A default `toString` method is generated, which is helpful for debugging. + +```scala +case class Person( + name: String, + vocation: String, +) + +val p = Person("Reginald Kenneth Dwight", "Singer") +p // p: Person = Person(Reginald Kenneth Dwight,Singer) +p.name // Reginald Kenneth Dwight +p.name = "Joe" // error: can't assign a val field +val p2 = p.copy(name = "Elton John") +p2 // p2: Person = Person(Elton John,Singer) +``` + ### Throw unimplemented ```scala // ??? // yes, ??? @@ -310,3 +682,9 @@ Suppress `info` level logging when running and watching runs: [^5]: [^6]: [^7]: +[^8]: +[^9]: +[^10]: +[^11]: +[^12]: +[^13]: diff --git a/build.sbt b/build.sbt index 6a923fb..6143b56 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,7 @@ lazy val root = project Wart.Var, Wart.Any ).contains), wartremoverErrors ++= Seq( - Wart.ArrayEquals, Wart.AnyVal, Wart.Equals, Wart.ExplicitImplicitTypes, + Wart.ArrayEquals, Wart.AnyVal, Wart.ExplicitImplicitTypes, Wart.FinalCaseClass, Wart.ImplicitConversion, Wart.JavaConversions, Wart.JavaSerializable, Wart.LeakingSealed, Wart.Nothing, Wart.Option2Iterable, Wart.PublicInference, diff --git a/src/test/scala/.scala-build/ide-inputs.json b/src/test/scala/.scala-build/ide-inputs.json new file mode 100644 index 0000000..8fb6eea --- /dev/null +++ b/src/test/scala/.scala-build/ide-inputs.json @@ -0,0 +1 @@ +{"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 new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/src/test/scala/.scala-build/ide-options-v2.json @@ -0,0 +1 @@ +{} \ 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 new file mode 100644 index 0000000..39a298b --- /dev/null +++ b/src/test/scala/.scala-build/stacktraces/1700421247-13054772439675899651.log @@ -0,0 +1,32 @@ +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 236be01..6b9841c 100644 --- a/src/test/scala/Suite.scala +++ b/src/test/scala/Suite.scala @@ -1,12 +1,13 @@ import scala.concurrent.duration.Duration import scala.collection.mutable.ArrayBuffer +import javax.lang.model.`type`.UnknownTypeException abstract class BaseSuite extends munit.FunSuite { override val munitTimeout = Duration(10, "sec") val on_base: String = "on base" } -class FunFixture extends BaseSuite { +class BaseFixture extends BaseSuite { var on_setup: String = "" var ints = ArrayBuffer.empty[Int] @@ -18,7 +19,7 @@ class FunFixture extends BaseSuite { on_setup } }, - teardown = { _ => on_setup = "" }, + teardown = { _ => on_setup = "this fixture was torn down" }, ) f.test("base suite is extended") { _ => @@ -33,6 +34,9 @@ class FunFixture extends BaseSuite { f.test(".fail test fails".fail) { _ => assert(false) } +} + +class ControlStructures extends BaseFixture { f.test("control structures are expressions") { _ => val is = if 1 > 0 then "greater" else "less" @@ -158,3 +162,123 @@ class FunFixture extends BaseSuite { } } +class DomainModelling extends BaseFixture { + + // Object-oriented + + f.test("traits are similar to interfaces") { _ => + trait Speaker: + def speak(): String + + trait TailWagger: + def startTail(): Unit = {} + def stopTail(): Unit = {} + + trait Runner: + def startRunning(): Unit = {} + def stopRunning(): Unit = {} + + class Dog(val name: String) extends Speaker, TailWagger, Runner: + def speak(): String = "Woof!" + + class Cat(val name: String) extends Speaker, TailWagger, Runner: + def speak(): String = "Meow!" + override def startRunning(): Unit = {} + override def stopRunning(): Unit = {} + + val d = Dog("Rover") + assertEquals(d.name, "Rover") + val c = Cat("Morris") + assertEquals(d.speak(), "Woof!") + assertNotEquals(d.speak(), "Meow!") + } + + f.test("class declarations create constructors") { _ => + class Person(var firstName: String, var lastName: String): + def getFullName() = s"$firstName $lastName" + + val p = Person("John", "Stephens") + assertEquals(p.firstName, "John") + p.lastName = "Legend" + assertEquals(p.getFullName(), "John Legend") + } + + f.test("parameters not assigned as val or var exist but are not accessible") { _ => + class Person(firstName: String, val lastName: String): + def getFullName() = s"$firstName $lastName" + + val p = Person("Jane", "Doe") + // p.firstName // won't compile: Reference Error + assertEquals(p.getFullName(), "Jane Doe") + } + + // Functional + + f.test("algebraic sum types can be modeled with enums") { _ => + enum CrustSize: + case Small, Medium, Large + + enum CrustType: + case Thin, Thick, Regular + + enum Topping: + case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions + + import CrustSize.* + val currentCrustSize = Small + + val size = currentCrustSize match + case Small => "Small crust size" + case Medium => "Medium crust size" + case Large => "Large crust size" + + assertEquals(size, "Small crust size") + } + + f.test("sum types can be defined using enum cases with parameters") { _ => + enum Nat: + case Zero + case Succ(pred: Nat) + + val number: Nat = Nat.Succ(Nat.Succ(Nat.Zero)) // represents 2 + + def increment(n: Nat): Nat = n match + case Nat.Zero => Nat.Succ(Nat.Zero) + case Nat.Succ(pred) => Nat.Succ(increment(pred)) + + // Nat.Succ(Nat.Succ(Nat.Succ(Nat.Zero))), representing 3 + val incrementedNum: Nat = increment(number) + + assertEquals( + incrementedNum, + Nat.Succ(Nat.Succ(Nat.Succ(Nat.Zero))), + ) + } + + f.test("product types can be defined using case classes") { _ => + case class Musician( + name: String, + vocation: String, + ) + + val m = Musician("Reginald", "Singer") + assertEquals(m.name, "Reginald") + assertEquals(m.vocation, "Singer") + + // m.name = "Joe" // won't compile: Reassignment to val + val m2 = m.copy(name = "나윤선") + assertEquals(m2.name, "나윤선") + } + + f.test("methods can be chained for less assignments and brevity") { _ => + val nums = (1 to 10).toList + + val result = nums.filter(_ > 3) + .filter(_ < 7) + .map(_ * 10) + + assertEquals(result, List(40, 50, 60)) + } + + + }