Chapter 10

Case Classes

One of the things that you should have noticed by this point is that there are times when it can be helpful to group pieces of data together. We have seen two ways that we can do this. If all the data is the same type, you could use something like an Array or a List. The downside of this is that the compiler can not check to make sure that you have the right number of elements. For example, if you want a point in 3-D with x, y, and z coordinates then you would need an Array[Double] that has three elements in it. Having more or less could cause the code to break and Scala would not be able to check for that until the program was running. The alternative, which also works if the types of the values are different, is to use a tuple. That same 3-D point could be represented as a (Double, Double, Double). While this works reasonably well for a 3-D point, it too has some significant limitations. The main limitation is that being a (Double, Double, Double) does not tell you anything about what those three Doubles mean or how it should be used.

To illustrate this, consider some different things that could be represented as three Doubles. One is a point in 3-D. Another closely related one is a vector in 3-D. Either the vector or the point could be represented in Cartesian coordinates, cylindrical coordinates, or polar coordinates and the tuple would not tell you which it is. The three Doubles though could also represent three subaverages for a student's grade in a class. For example, they might be the test, quiz, and assignment averages. The tuple does not tell you which it is and does not help you at all with keeping things straight.

What is more, the tuple lacks some flexibility and the syntax for getting things out of them is less than ideal. Calling the _1 and _2 methods all through your code can make it difficult to read. Imagine if instead you wanted to represent a full student in a class. Then the tuple might have a String and a large number of grades, all of the same type. Keeping track of what numbers are associated with what grades would very quickly become problematic. To get around these limitations, we will consider the use of case classes for grouping data.

10.1 User-Defined Types

What we really need to break away from the limitations of using tuples to deal with groupings of data is the ability to define our own types that have meaningful names. Tuples definitely have their place, and they are very useful in those places. However, there are many times when it would be handy to create a type specifically for a particular purpose. Then we could give the type a name that indicated its purpose and have the compiler check for us that we were using that type in the proper way.

User defined types are a common feature in modern programming languages and have been for decades. Scala provides three constructs for creating user-defined types: classes, traits, and singleton objects. For this chapter, in order to keep things simple, we will only consider a specific type of one of these, the case class. The others will be covered in detail when we consider object-orientation in full.

Back in section 7.5 we defined a type as a collection of values and the operations that can be performed on them. The user-defined types typically take the form of being collections of other types. This makes them fundamentally similar to just using a tuple. Where they prove to be more than just a tuple is the control they give you in determining what can be done with the types. We will remain somewhat limited in this regard for now, but even with those limitations you should find that our user-defined types provide a significant boost to our programming capabilities.

10.2 Case Classes

The simplest way to start with user-defined types in Scala is with case classes. Perhaps the best way to introduce them is to show some examples. We'll start with two that were mentioned above: a 3-D point and a student with some grades.

case class Point3D(x:Double,y:Double,z:Double)
case class Student(name:String,assignments:List[Double],tests:List[Double],
 quizzes:List[Double])

The first one declares a type called Point3D that stores inside of it three different Doubles that are named x, y, and z. The second declares a type called Student that has a name as a String and three different Lists of Double to use as grades.

There can be more to the declaration of a case class, but for now we will limit ourselves to this syntax that begins with the keywords "case class". After that is the name you want to give the type. This could be any valid Scala name, but it is customary to begin type names with uppercase letters and use camel naming so all subsequent words also begin with uppercase letters. After that is a list of name/type pairs in parentheses. The format of these is just like the arguments to a function. The elements of this list give the names and types of the values stored in this new type.

10.2.1 Making Objects

After you have declared a type, you need to be able to create objects of that type. With a case class, you can do this with an expression that has the name of the class followed by an argument list, just like a function call. The two classes listed above could be created and stored in variables with the following lines of code.

val p = Point3D(1,2,3)
val s = Student("Mark",Nil,Nil,List(89))

The first line makes a point that has x=1, y=2, and z=3 and stores a reference to it in a variable names p. The next line makes a student with the name "Mark" who has no assignment or test grades, but who has an 89 on one test and stores a reference to it in a variable named s.

You could insert the word new and a space after the equals signs so that these lines look like the following.

val p = new Point3D(1,2,3)
val s = new Student("Mark",Nil,Nil,List(89))

The result of this would be exactly the same. The first syntax is shorter and works for all case classes so we will stick with that in our sample code.1

10.2.2 Accessing Elements

In order to be able to use these objects, we must be able to access the different elements in them. This is very simple to do, just use the dot notation to access the elements. So if you want the x value in the Point3D, p that we made above, you would just do this.

scala> p.x
res1: Double = 1.0

To get the name of the Student you would do this.

scala> s.name
res2: String = Mark

The dot notation in Scala simply means that you are using something from inside of an object. It could be a method or a value that is stored in the object. For now we will only be concerning ourselves with the values that we store in our case classes.

We could put this to use by writing a function to find the distance between two Point3Ds. It might look something like this.

def distance(p1:Point3D,p2:Point3D):Double = {
 val dx=p1.x-p2.x
 val dy=p1.y-p2.y
 val dz=p1.z-p2.z
 math.sqrt(dx∗dx+dy∗dy+dz∗dz)
}

We could also use it to calculate and average for a Student with code like this.

def classAverage(s:Student):Double = {
  val assnAve=if(s.assignments.isEmpty) 0.0
  else s.assignments.sum/s.assignments.length
  val quizAve=if(s.quizzes.length>2) 0.0
 else (s.quizzes.sum-s.quizzes.min)/(s.quizzes.length-1)
  val testAve=if(s.tests.isEmpty) 0.0
 else s.tests.sum/s.tests.length
  0.5∗assnAve+0.3∗testAve+0.2∗quizAve
}

The if expressions here prevent us from doing division by zero.

One of the things to note about case classes is that the elements in them are vals. As such, you can not change what they refer to. If you try to make such a change you get something like the following.

scala> p.x=99
<console>:8: error: reassignment to val
 p.x=99
   ^

Whether you can change anything in an object created from a case class depends on whether the things in it are mutable or not. In our two examples, all of the contents are immutable. As a result, the case class as a whole is immutable. Once you create a Point3D or a Student, the object you create can not change its value in any way. However, if one or more of the fields in the case class were an Array, then the values in the Array would be mutable. You would not be able to change the size of the Array without making a new object, but you could change the values stored in it.

10.2.3 Named and Default Arguments (Advanced)

A few options were added to Scala in version 2.8 in regards to function arguments and calling functions. Normally, Scala figures out which of the arguments passed into a function is associated with which formal parameter by their order. Consider this function.

def evalQuadratic(a:Double,b:Double,c:Double,x:Double):Double = {
 val x2=x∗x
 a∗x2+b∗x+c
}

If you load this into the REPL you can execute it as follows.

scala> evalQuadratic(2,3,4,5)
res0: Double = 69.0

In this call, a=2, b=3, c=4, and x=5. This is because that is they order the arguments to appear in both the definition of the function and the call to it. For functions where there are a significant number of arguments that are all of the same type, this can lead to confusion. To get around this, Scala has named arguments. When you call the function you can specify the names you want the values associated with it. So the call above would be like the following:

scala> evalQuadratic(a=2,b=3,c=4,x=5)
res2: Double = 69.0

In this call it is now explicit what values are going to what parameters. One advantage of this is when you enter the arguments in a different order, the meaning will be what you want. For example, you might think that x was the first argument instead of the last. Without names arguments, this would lead to an error with no error message. You would simply get the wrong answer. However, if you use named arguments everything is fine because the names supersede the order.

scala> evalQuadratic(x=5,a=2,b=3,c=4)
res3: Double = 69.0

Here we see that even though x is first, the value we get is correct.

You can use named parameters without naming all the parameters. You can start the list with arguments that are based on position and then use names for the later ones. All the arguments after the first named one have to be named and they can not duplicate any that you gave using the position.

For some functions there are some arguments that will have a particular value a lot of the time. In that situation, it is nice to make it so that people calling the function do not have to provide them and they will use a default value. When you declare the function simply follow the type with an equals sign and the value you want to have for the default. If the caller is happy with the default value, then that argument can be left out. Default arguments at the end of the list can be simply omitted. If they are in the middle then you will have to use named arguments to specify any arguments after them in the list. Consider a function to add a grade to a Student.

def addGrade(name:String,grade:Int = 0):Student = ...

Here the default grade is a zero. So this function can be called in two ways.

addGrade("Jane",95)
addGrade("Joe")

The first call is like everything we have seen to this point. The second one leaves off the grade. As a result, Joe gets a 0 for whatever grade this was.

10.2.4 The copy Method

The fact that you can not mutate the values in a case class means that it would be helpful to have a way to make new case class objects that are only slightly changed from others. To see this, consider what happens when you want to add a new grade to a Student. The grades are in Lists, and it is easy to add to a List. The problem is, that does not mutate what is in the original List, it just gives us a new List that includes the new values as well as what was already there.

To help get around this problem, case classes come with a copy method. The copy method is intended to be used with the named arguments that were discussed in section 10.2.3. The arguments to copy have the same names as the fields in the class. Using named arguments, you only provide the ones you want to change. Anything you leave out will be copied straight over to the new object. So using the Student object we gave the name s above, we could use copy to do the following.

val ns = s.copy(tests = 99::s.tests)

This gives us back a new Student who is the same as the one we had in s, only it has a test grade of 99 in addition to the quiz grade of 89 it had originally.

You can specify as many or as few of the fields in the case class as you want. Whatever fields you give the names of will be changed to the value that you specify. If you leave the parentheses empty, you will simply get a copy of the object you have originally.

10.2.5 Case Class Patterns

Another capability that comes with case classes is that you can use them in patterns. This can be used as a simple way to pull values out of an instance or to select between objects in a match. As an example of pulling out values, consider the following code using Point3D.

for(Point3D(x,y,z) <- points) {
 // Do stuff with x, y, and z.
}

This is a for loop that runs through a collection of points. Instead of calling each point with a name like point, this pulls out the values in the point and gives them the names x, y, and z. That can make things shorter and more clear in the body of the loop.

As an example of limiting what is considered, we can use another for loop that goes through a course full of students.

for(Student(name,_,List(t1,t2,t3),_) <- courseStudents) {
  // Processing on the students with three test grades.
}

This does something with patterns that we have not seen before, it nests them. You can nest patterns in any way that you want. This is part of what makes them extremely powerful. In this case, the assignment and quiz grades have been ignored and the loop is limited to only considering students with exactly three test grades. Those grades are given the names t1, t2, and t3. That could also have been specified with the pattern t1::t2::t3::Nil. Students who have more or fewer test grades will be skipped over by this loop.

10.3 Mutable Classes

case classes are not the only option you have when creating classes. We will go into the full details of classes in chapter 16. There are some applications for which you do not need the full power classes can provide, but you would like to have fields in the class that are mutable. Remember that every field in a case class is like a val so the case class is generally immutable. The only way the value of something in a case class can change is if one of the fields references a mutable value like an Array.

When you want to make objects with mutable fields, you need to leave off the case keyword and just define a class. To be able to use the fields, given what you currently know, each one will need to be preceded by either val or var. If you put val before a field its value will not be allowed to change. If you use var, the field will be allowed to change. So if you have objects that truly need to be altered because they change very rapidly and modified copies are not efficient, you can make a class using var for each field that needs to change.

Note that by leaving off the case keyword, you do more than just allow for var fields. You also lose the copy method, the ability to leave off new when making a new instance of the class, and the ability to do pattern matching. In general, there are many benefits to immutability so you should not take this approach without good reason.

10.4 Putting it Together

Now we want to use a case class along with other things that we have learned to create a small, text-based application. The application that we will write will use the Student that we defined earlier along with the classAverage function and other functions that we will write to make a grade book. This program will be run from a text menu and give us various options similar to what was done in section 8.2.

The program will also use the file handling capabilities that we have learned so that the grades of the students in the course can be saved off and then be loaded back in when we restart the program. The menu for the program will have the following options:

  1. Add Test Grade
  2. Add Assignment Grade
  3. Add Quiz Grade
  4. Print Averages
  5. Save and Quit

The program will take a command-line argument for the file to load in. If none is given, the user will be asked how many students are in the section and their names along with the file name to save it under. When one of the first three menu options is selected, the program will list each student's name and ask for their grade. The "Print Averages" option will print out the names of each student along with their grades in each area, their average in that area, and their total average.

There is quite a bit to this program so it is worth breaking it up into different functions and then writing each of those. To do this we can outline what will happen when we run the program and use the outline to break things down then assign function names to things.

  • Startup
    • load a file (loadSection)
    • or create a section (createSection)
  • Main menu (mainMenu)
    • print the menu (printMenu)
    • act on the selection
      • add a test grade (addTest)
      • add an assignment grade (addAssignment)
      • add a quiz grade (addQuiz)
      • print the averages (printAverages)
  • Save when done (saveSection)

Now that we have figured out roughly what we need to do, we can write these functions in any order that we want. In general the process of writing functions like this can be very non-linear. You should not feel any reason why you would have to go through the functions in any particular order. Often in a real project you would do things in a certain order as you figure out how to do them.

The more experience you gain, the more comfortable you will be in writing code and then you might decide to pick certain functions because they will give you functionality that you can test. One of the advantages of having the REPL to fall back on is that we can load in our file and test functions one by one, seeing the results along the way. Without that, the printAverages function would prove to be extremely important to us as it would be the only way that we could see what was going on.

For our purposes we will start with createSection and saveSection. These two functions pair well together and are closely related because we have to decide how we are going to represent a section both in the memory of the computer and in the file. We will start with createSection and the way in which things are represented in the memory of the computer.

We have already created a case class called Student that can be used to represent one student in the section. We just need a bunch of them. We also need to realize that they will change over time as grades are added. It would probably be sufficient to just keep an Array[Student]. However, there are benefits to actually wrapping the array inside of a different case class like this.

case class Section(students:Array[Student])

One advantage for our purposes here is that this is a chapter on case classes and this provides yet another example of one. In general, this can also provide greater flexibility. We might decide at some point that we want to attach data for a course name, semester, instructor, etc., to each Section. Those things can not be added to a simple Array. However, they could easily be added to the case class. It also has the advantage of providing extra meaning. This is not just a random collection of Students, it represents a section of a class.

Now that we know this, we can write the createSection function. This function will prompt the user for the information that is needed to create the Section. For now that is a file name to save it to, the number of students, and the names of the students. The function will return the file name and the Section.

def createSection:(String,Section) = {
  println("What file would you like to save this as?")
  val fileName=readLine()
  println("How many students are in the class?")
  val numStudents=readInt()
  println("Enter the student names, one per line.")
  (fileName,Section(Array.
 fill(numStudents)(Student(readLine(),Nil,Nil,Nil))))
}

The first five lines of this function are fairly self-explanatory with prompts being printed and values being read. After that is the return tuple, which includes a call to Array.fill that has a readLine in the pass-by-name parameter. This means that it not only makes the return value, it also includes the input of the names.

Now that we have created a new Section, we can consider what it will look like in a file. There are many different ways that the file could be formatted. The manner that we will pick here starts with the number of students in the class on a line. After that there are four lines for each student. They are the student's name followed by a line each with assignment, test, and quiz grades. This function can be written as follows.

def saveSection(fileName:String,section:Section){
  val pw=new PrintWriter(fileName)
  pw.println(section.students.length)
  for(s <- section.students) {
 pw.println(s.name)
 pw.println(s.assignments.mkString(" "))
 pw.println(s.tests.mkString(" "))
 pw.println(s.quizzes.mkString(" "))
  }
 pw.close()
}

The function takes the file name and the Section. It then makes a PrintWriter with the fileName, which is closed at the end of the function, and prints the needed information. The use of mkString on the different Lists makes the code for doing this much shorter.

As you are writing these functions, you need to test them. One way to do that is to load them into the REPL and call them. Another way is to end the script with calls to them. At this point, the end of the script might look something like the following.

val (fileName,section)=createSection
saveSection(fileName,section)

This comes after the definition of both the case classes and the different functions. If you run the script with this in it, you should be prompted for the information on the Section and after you enter the the script should stop. You can then look at the file that you told it to save as and make sure it looks like what you would expect.

We will hold the loadSection function until the end and go into the main functionality with mainMenu and printMenu. You can write them in the following way.

def printMenu {
 println("""Select an option:
1. Add Test Grade
2. Add Assignment Grade
3. Add Quiz Grade
4. PrintAverages
5. Save and Quit""")
}
def mainMenu(section:Section) {
 var option=0
 do {
 printMenu
 option=readInt()
 option match {
  case 1 => addTest(section)
  case 2 => addAssignment(section)
  case 3 => addQuiz(section)
  case 4 => printAverages(section)
  case 5 => println("Goodbye!")
  case _ => println("Invalid option. Try again.")
  }
 } while(option!=5)
}

You can not test this code yet because mainMenu calls four other functions that have not been written yet. Once we have those written, we can put a call to mainMenu at the end of the script right before the call to saveSection.

The three different add functions will all look pretty much the same. We will only show the addTest function and let you figure out the others. It is worth thinking a bit about how that function will work. The Student type is immutable. All the fields in the case class are vals so they can not be changed. The String and the three different List[Int] values are all immutable so once a Student is created, it is set forever. Fortunately, the Section type stores the Students in an Array. This means we can change what Student objects are being referred to. We can use the copy capabilities of the case class to make new instances that are almost the same except for small variations. Using this, the addTest function could be written in the following way.

def addTest(section:Section) {
 for(i <- 0 until section.students.length) {
 println("Enter the grade for "+section.students(i).name+".")
 section.students(i)=section.students(i).
  copy(quizzes=readInt()::section.students(i).quizzes)
  }
}

This code works just fine, but it is a bit verbose because we have to type in section.students(i) so many times. We have to have the index because we need to be able to do the assignment to an element of the Array. The section.students(i) before the equals sign in the assignment is hard to get rid of because we have to mutate that value in the design of this code. The code could be shortened with appropriate use of imports, but there is another, more interesting solution.

def addTest(section:Section) {
 for((s,i) <- section.students.zipWithIndex) {
 println("Enter the grade for "+s.name+".")
 section.students(i)=s.copy(tests=readInt()::s.tests)
 }
}

This version uses zipWithIndex and a pattern on the tuple to give us both a short name for the student, s, and an index into the array, i. Both of these are equally correct so use the one that makes more sense to you and duplicate it for assignments and quizzes.

The next function in the menu is printAverages. A very basic implementation of this would just print student names and the course average. However, it could be helpful to see all the grades and the partial averages as well. That is what is done in this version.

def printAverages(section:Section) {
  for(s <- section.students) {
 println(s.name)
 val assnAve=if(s.assignments.isEmpty) 0.0
  else s.assignments.sum/s.assignments.length
 println(s.assignments.mkString("Assignments:",", "," = "+assnAve))
 val quizAve=if(s.quizzes.length<2) 0.0
  else (s.quizzes.sum-s.quizzes.min)/(s.quizzes.length-1)
 println(s.quizzes.mkString("Quizzes:",", "," = "+quizAve))
 val testAve=if(s.tests.isEmpty) 0.0
  else s.tests.sum/s.tests.length
 println(s.tests.mkString("Tests:",", "," = "+testAve))
 println("Average = "+(0.5∗assnAve+0.3∗testAve+0.2∗quizAve))
 }
}

This function uses the code from the earlier class average function and inserts some print statements. The only thing in here that might seem odd is the use of a mkString method that takes three arguments instead of just one. With this longer version, the first string goes before all the elements and the third one goes after all the elements. The argument in the middle is the delimiter as it has been in previous usage.

def loadSection(fileName:String):(String,Section) = {
  val src=Source.fromFile(fileName)
  val lines=src.getLines
  val section=Section(Array.fill(lines.next().toInt)(Student(
  lines.next(),
  lines.next().split(" ").filter(_.length>0).map(_.toDouble).toList,
  lines.next().split(" ").filter(_.length>0).map(_.toDouble).toList,
  lines.next().split(" ").filter(_.length>0).map(_.toDouble).toList
)))
src.close
 (fileName,section)
}

This function includes three lines for handling the file. The meat of the function is in the declaration of the section variable, which calls lines.next() anytime that it needs a new line from the input file. The first time is to read how many students are in the section for building the Array. Each student pulls in four lines for the name and three different grade types. The lines of grades are split, filtered, and them mapped to Doubles before they are converted to a List. The filter is required for the situation where you have not entered any grades of a particular type.

You might wonder why the return type of this function includes the fileName that was passed in. Technically this is not required, but it makes this function integrate much more nicely at the bottom of the script.

val (fileName,section)=if(args.length>1) createSection
 else loadSection(args(0))
mainMenu(section)
saveSection(fileName,section)

Having createSection and loadSection return the same information greatly simplifies this part of the code as they can be called together in a simple if expression.

That is everything. You now have a full little application that could be used to store a grade book for some course. Try putting this code in and playing with it a while.

Tuple Zipped Type (Advanced)

Something that you need to do fairly frequently is to run through two collections at the same time, pulling items from the same location of each. One way to do this is to use the zip method to zip the collections together into a new collection of tuples. While this works well if you have two collections, especially if you use a for loop, it does not work as well for three or more collections, and it is fundamentally inefficient because the zip method will go through the effort of creating a real collection with a bunch of tuples in it.

To get around these limitations, the types for tuples of length 2 and 3 have a type associated with them called Zipped. The sole purpose of the Zipped type is to let you get the benefits of running through a zipped collection without actually doing the zipping. To get an instance of the Zipped type, simply make a tuple that has all the collections you want in it and call the zipped method. The Zipped type has some of the main higher-order methods that you are used to using on collections: exists, filter, flatMap, forall, foreach, and map. The difference is that in the Zipped type they take multiple arguments. Specifically, they take as many arguments as there are elements in the tuple. This is significant because if you call a function like map that is mapping a collection of tuples the function has to take one argument and go through some effort to pull the elements out of the tuple. With the Zipped type you do not have to do that as the function literals are supposed to take multiple arguments instead of a single tuple with the multiple values.

A comparison of the two approaches is shown here.

val l1=List(1,2,3)
val l2=List(4,5,6)
l1.zip(l2).map(t => t._1∗t._2)
(l1,l2).zipped.map((v1,v2) => v1∗v2)

For this example the first one is a bit shorter, but that typically will not be the case. More importantly, the first one relies on the _1 and _2 methods which will make the code hard to read and understand for anything with more logic. To get the benefit of easy to read names using zip you would have to do the following.

l1.zip(l2).map(t => {
 val (v1,v2)=t
 v1∗v2
})

It remains an exercise for the reader to see what happens if you want to iterate over three collections using zip. Consider the Zipped type when you need to iterate over two or three collections at the same time.

10.5 End of Chapter Material

10.5.1 Summary of Concepts

  • The act of grouping together data is very useful in programming. We have been doing this with tuples. The problem with tuples is that they do not provide meaning and their syntax can make code difficult to read and understand.
  • User defined types let you create your own types that have meaning related to the problem you are solving.
  • One way of making user-defined types is with case classes. We will use these to group values together giving them useful, easy-to-read names.
    • To create a case class follow those keywords with an argument list like that for a function with names and types separated by commas. Names for types typically start with a capital letter.
    • You create an instance of a case class give the name of the type followed by an argument list of the values it should store.
    • When you want to access the members of a case class, use the dot notation we have been using for other objects.
    • The members of a case class are all vals. As a result, instances of case classes tend to be immutable. The only way that will not be true is of a member is itself mutable.
    • To make new instances of case classes that are slightly different from old ones, use the copy method. This method is called with named arguments for any members that you want to have changed in the copy.
    • Another useful capability of case classes is that they can be used as patterns.
  • If you need the ability to mutate values, you can make a normal class leaving off the case keyword and specifying whether the member is a val or a var.
    • Instances of these types will need to be created with new.
    • There is no copy method automatically defined.
    • These types will not implicitly work as patterns.

10.5.2 Self-Directed Study

Enter the following statements into the REPL and see what they do. Some will produce errors. You should try to figure out why. Try some variations to make sure you understand what is going on.

scala> case class Accident(dlNumber1:String,dlNumber2:String)
scala> case class
 Driver(name:String,dlNumber:String,dob:String,history:List[Accident])
scala> def wreck(d1:Driver,d2:Driver):(Driver,Driver,Accident) = {
 | val accident = Accident(d1.dlNumber,d2.dlNumber)
 | (d1.copy(history = accident::d1.history),
 |d2.copy(history = accident::d2.history),
 |accident)
 |}
scala> var me = Driver("Mark","12345","long ago",Nil)
scala> var otherPerson = Driver("John Doe","87654","01/01/1990",Nil)
scala> val (newMe,newOther,acc) = wreck(me,otherPerson)
scala> me = newMe
scala> otherPerson = newOther
scala> println(me.name)
scala> println(otherPerson.dlNumber)
scala> println(me.history.length)
scala> otherPerson.name = "Jane Doe"
scala> case class Vect2D(x:Double,y:Double)
scala> def magnitude(v:Vect2D):Double = {
 | math.sqrt(v.x∗v.x+v.y∗v.y)
 |}
scala> def dot(v1:Vect2D,v2:Vect2D):Double = v1.x∗v2.x+v1.y∗v2.y
scala> def makeUnit(angle:Double):Vect2D = {
 | Vect2D(math.cos(angle),math.sin(angle))
 |}
scala> def scale(v:Vect2D,s:Double):Vect2D = Vect2D(v.x∗s,v.y∗s)
scala> val a = makeUnit(math.Pi/4)
scala> val b = makeUnit(3∗math.Pi/4)
scala> dot(a,b)
scala> magnitude(a)
scala> magnitude(b)
scala> magnitude(scale(a,3))

10.5.3 Exercises

  1. Write a case class to represent a student transcript.
  2. Using your answer to the previous exercise, define a function that adds one semester of grades to the transcript.
  3. Using your answer to 1, write a function that will return the student's GPA.
  4. Write a case class to represent a recipe.
  5. Using your answer to 4, write a function that takes a recipe and the name of an ingredient and returns how much of that ingredient is needed.
  6. Write a case class to represent the information needed for a house in a Realtor posting.
  7. Re-write the grade book program in a completely functional way so that it has neither Arrays, vars or other mutable objects.
  8. Play with zip using 3 collections. Compared it to using zipped.
  9. Pick a favorite sport and make a case class that can be used to store player information.
  10. Extends what you did on the previous exercise so you have a case class that stores the information for a team.

10.5.4 Projects

  1. This is an extension on project 3 (p.205). You will use Arrays and classes to take the Keplerian simulator a bit further. In that program all you had was one body in motion about a "central mass" that was not moving at all. Now you can store and work with the positions and velocities of many bodies because you can store their component data in Arrays or case classes. That is to say you can have an Array for x positions as well as y, vx and vy , or an Array of some case class that stores those values. This allows you to simulate the motions of many particles at the same time which is much more fun. Earlier you only had to calculate the acceleration due to the central particle. Now you want to calculate accelerations due to all interactions between all the particles. You can also make the central particle one of the particles in the array or try not even having a central particle.

    With multiple particles, you need to have a nested loop (or a for loop with two generators) that calculates the accelerations on each particle from all the others and adds them all up. Keep in mind that if particle i pulls on particle j then particle j pulls back on i just as hard, but in the opposite direction. That does not mean the accelerations are the same though. The acceleration is proportional to the mass of the puller because the mass of the pullee is canceled out by its inertia. Earlier we had ax = xd3 and ay = xd3. When the particle doing the pulling is not at the origin, d is the distance between the particles and x and y are the distances between them in x and y directions. We also need a factor of m for the mass of the puller. You want to add up the accelerations from all other particles on each one and store that into arrays so

    ax(i) = j(xjxi)mjdij3

    There is a similar formula for y. The dij value is the distance between particle i and particle j. Also note that given a value, c, the best way to cube it is c∗c∗c, not math.pow(c,3).

    When you write your formula in the code, think a bit about it. This formula causes a problem if we really use d because particles can get too close to one another. It is suggested that you make d = dx2+dy2 +  where epsilon is a small value. You can play with how small you want it to be because that depends on the scale of your problem. It is also recommended that you have your integrator not use the normal Euler method which calculates accelerations, then updates positions, then velocities. Make sure that it does the accelerations on velocities before applying the velocities to the positions. Keep in mind that you want to break this up into functions that fit together nicely and are helpful. It will hurt your brain more if you do not.

    The input for the program will have a first line that is the number of bodies, the timestep, the stopping time, and the number of steps between outputs. This will be followed by lines with the x, y, vx, vy, and mass values for each particle. A sample input file can be found on the book website on the page for this chapter. Note that the central mass in that file has a mass of 1 and all the others are much smaller.

    As output, you should write out the positions of all the particles in your simulation to a file once for every n steps, where n is a value given on the first line of the input. If you do this then you can run a spreadsheet of gnuplot to plot it. If you use gnuplot and give it the command plot 'output' it will make a little scatter plot showing you the paths of the particles in the simulation.

  2. This project builds on top of project 8.1 (p.204). You have likely been using tuples or separate values to represent the geometry elements in your ray tracer. This is information that is much more naturally represented as a case class. For this project you should go through and edit your existing code so that it includes three different case classes, one for spheres, one for planes, and one for a scene which has a List[Sphere] and a List[Plane].
  3. This is the first installment for you building your own text adventure. Your program will read in from a map file that you will write by hand and let the user run around in that map by using commands like "north" to move from one room to another. The map file will have a fairly simple format right now and you will create your own map file using vi. Make sure when you turn in this program you turn in both the script and the map file so it can be tested with your map.

    The format of the map file should start with a line telling the number of rooms then have something like the following. You can change this if you want to use a slightly different format:

    room_number
    room_name
    long line of room description
    number_of_links
    direction1
    destination1
    direction2
    destination2
    ...

    This is repeated over an over. (The number of rooms at the top is helpful for storing things in an Array so that you know how big to make it.) Each room should have a unique room number and they should start at 0. The reason is that you will be putting all the room information into Arrays. There is a link on the book website to a sample map file, but you do not have to stick exactly to that format if you do not want to. You might deviate if you are thinking about other options you will add in later. Odds are good you will be refactoring your code for later projects.

    The interface for your program is quite simple. When you run the program it should read in the map file and keep all the map information stored in an Array[Room] where Room is a case class you have made to keep the significant information for each room.2 You will start the user in room 0 and print the description of that room and the different directions they can go as well as where those exits lead to, then follow that with a prompt. You could just use > as the prompt to start with. It might get more complex later on when you have real game functionality. So when the user starts the game it might look something like this if you read in the sample map file.

    Halsell 228
    You are standing in a room full of computers and comfy chairs with
    a giant air conditioning unit hanging from the ceiling. While the
    surroundings are serene enough, you can't help feeling a certain amount
    of dread. This isn't just a fear that the air conditioning unit is
    going to fall either. Something in you tells you that this room is
    regularly used for strange rituals involving torture. You can only
    wonder what happens here and why there isn't blood all over the place.
    Your uneasiness makes you want to leave quickly.
    The hallway is east.
    >

    The user must type in either a direction to move or "quit". If anything else is entered you should print an appropriate error message. The only goal of this project is to allow the user to move around the map. Collection methods such as find, indexWhere, filter, or partition can be extremely helpful for this project.

  4. Convert the work you did for project 3 (p.227) to use case classes to bind data. So you should have a case class for an item with an amount, one for a recipe, one for pantry contents, etc.
  5. Convert the work you did for project 4 (p.227) to use case classes. With this addition you can put more information into each course because you now have a better way to store it all together.
  6. Sports have a tendency to produce a lot of information in the form of statistics. A case class is a good way to represent this information. For this project you need to find a text format of box scores for the sport that interests you. Then make a case class to represent that data. Put multiple box scores into a text file and read them in. Have menu options for calculating averages for players and team in the different relevant statistics.

Additional exercises and projects, along with data files, are available on the book's website.

1In chapter 16 you will learn that normal classes require the use of new by default. To get around this requires writing some code in a companion object, a technique also covered in that chapter.

2Many implementations also make a case class to represent an Exit from a room.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset