5. CS2

Warning

This section is not ready yet. Working on it!

5.1. Collections

  • Just for Scala
  • Doesn’t make sense before loops in most languages.
  • One mutable, one immutable
  • Many standard methods
  • Many higher-order methods
  • Syntax
  • Use () for indexing
  • List also have ML style operations
  • Creation, pass-by-name

5.2. Case Classes

  • Immutable struct in simplest usage
  • Simple syntax for grouping data
  • Works as a pattern
  • Copy method

5.3. GUIs

  • scala.swing wraps javax.swing
  • Cleaner beginner syntax
  • No explicit inheritance
  • Reactions use partial functions
  • Drawbacks: Currently no JTree, Tables complex, Button syntax uses companion object
  • Full Java2D
  • Really using Java
  • Override paint method
  • Events for animations
  • Keyboard, Mouse, Timer

5.3.1. Simple GUI Example

This example shows a simple GUI where there is no inheritance. Clicking the button increments the number in the text field. If the user has changed it so that it is not an integer, it is set back to 0.

Those familiar with Java will notice a lot of similarities. This is because the scala.swing library is a wrapper around Java’s Swing library. The ideas are similar, but the way in which you interact with them has been changed to match the Scala style.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import swing._

val frame = new MainFrame
val field = new TextField("0")
val button = Button("Increment") {
    try {
        field.text = (field.text.toInt+1).toString
    } catch {
    case ex:
        NumberFormatException => field.text = "0"
    }
}

val bp = new BorderPanel
import BorderPanel.Position._
bp.layout += field -> North
bp.layout += button -> Center
frame.contents = bp
frame.centerOnScreen
frame.open

5.3.2. Simple GUI Example using scala.util.Try

Scala supports simplified exception handling through its scala.util.Try wrapper type. This is an important Scala idiom for representing a computation that either succeeds with a result value or fails with an exception.

For example, say you want to validate and convert a text field in your UI from string to integer. You could write this simple conversion function to do so:

scala> def toInteger(s: String) = scala.util.Try(s.toInt)
toInteger: (s: String)scala.util.Try[Int]

res0: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "blah")

scala> toInteger("35")
res1: scala.util.Try[Int] = Success(35)

Then you can use getOrElse to process the enclosed value and, if the Try value represents failure, return the given default value (as you see above when we tried to validate the string “blah”).

scala> toInteger("35").getOrElse(-1)
res2: Int = 35

scala> toInteger("blah").getOrElse(-1)
res3: Int = -1

It’s clear that being able to validate input efficiently is something that excites us. It certainly makes UI development more reliable and resilient to failures. (We’ve had more than our share of fun chasing down validation bugs in web and mobile app development. Most of the time it is caused by unnecessarily complex validation logic.)

You can see how this plays out in a slightly reworked version of the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import swing._
import scala.util.Try

val frame = new MainFrame
val field = new TextField("0")

val button = Button("Increment") {
  val attempt = Try(field.text.toInt) 
  field.text = (attempt.getOrElse(-1)+1).toString
}

val bp = new BorderPanel
import BorderPanel.Position._
bp.layout += field -> North
bp.layout += button -> Center
frame.contents = bp
frame.centerOnScreen
frame.open

5.3.3. Simple Paint Example

This example shows how you can override the paint method to make a custom drawing. It also shows interactions with the mouse.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import swing._
import event._
import java.awt.{Color,Shape}
import java.awt.geom._

var dots = List.empty[Shape]

val panel = new Panel {
  override def paint(g:Graphics2D) {
    g.setPaint(Color.white)
    g.fillRect(0,0,size.width,size.height)
    g.setPaint(Color.black)
    for(s <- dots) g.fill(s)
  }
  listenTo(mouse.clicks,mouse.moves)
  reactions += {
  case mc: MouseClicked =>
    dots ::= new Ellipse2D.Double(mc.point.x-2,mc.point.y-2,5,5)
    repaint
  case mc: MouseDragged =>
    dots ::= new Ellipse2D.Double(mc.point.x-2,mc.point.y-2,5,5)
    repaint
  }
}

val frame = new MainFrame {
  contents = panel
  size = new Dimension(600,600)
  centerOnScreen
}

frame.open

Here’s what the output looks like when you drag the mouse quasi-randomly on the blank canvas that first comes appears. (Your output may vary!)

_images/SimplePaint.png

5.3.4. A More Complex GUI Example

This is a large GUI example. There are two lists with a text fields and some buttons. The first list is populated by the text field and the buttons move things between lists or remove them from the second list.

The populating from the text field demonstrates how you listen to GUI elements and react to them. The behavior of the lists shows how collection methods can play a role in GUIs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import swing._
import event._

val list1 = new ListView[String]()
val list2 = new ListView[String]()
val buttons = new FlowPanel {
    contents += Button("<-") {
        list1.listData ++= list2.selection.items
        list2.listData = list2.listData.diff(list2.selection.items)
    }
    contents += Button("->") {
        list2.listData ++= list1.selection.items
        list1.listData = list1.listData.diff(list1.selection.items)
    }
    contents += Button("Remove") {
        list2.listData = list2.listData.diff(list2.selection.items)
    }
}
val field = new TextField() {
    listenTo(this)
    reactions += {
    case ed:
        EditDone =>
list1.listData :
        += text
        text = ""
    }
}

val frame = new MainFrame {
    contents = new BorderPanel {
        import BorderPanel.Position._
        layout += field -> North
        layout += new ScrollPane(list1) -> West
        layout += new ScrollPane(list2) -> East
        layout += buttons -> Center
    }
    size = new Dimension(600,500)
    centerOnScreen
}

frame.open

5.3.5. Asteroids

This program is a little implementation of asteroids. It shows keyboard events and the use of case classes to group data together.

We start by importing varius dependencies. This shows how you can take advantage of existing Java libraries.

Case classes are used to maintain information about key elements of the game, notably the asteroids and bullets. Although you see the word class here, we’re primarily using class to aggregate the data (think about C struct but even nicer). These are used to maintain two typesafe lists of asteroids and bullets, respectively, with type List[Asteroid] and List[Bullet].

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import swing._
import event._
import java.awt.{Color,Shape}
import java.awt.geom._
import javax.swing.Timer

case class Asteroid(x:Double,y:Double,vx:Double,vy:Double,size:Double)
case class Bullet(x:Double,y:Double,vx:Double,vy:Double,age:Int)

val windowSize = 600
val shipSize = 6

var asteroids = List.fill(5){
    val theta = math.random*math.Pi*2
    Asteroid(windowSize/2+math.cos(theta)*windowSize/4,
                   windowSize/2+math.sin(theta)*windowSize/4,
                     math.random-0.5,math.random-0.5,50)
}
var bullets = List[Bullet]()
var shipX = windowSize/2.0
var shipY = windowSize/2.0
var heading = 0.0
var shipVx = 0.0
var shipVy = 0.0
var leftDown = false
var rightDown = false

The wrap() method does what you might be thinking it does. It takes an x or y coordinate (even though you only see x in the parameter name) of a given asteroid and ensures it falls within the bounds of the window. Note that this references an external “global” value windowSize. Because this value is immutable, there is no risk of a side effect. (This could be called the revival of the const from C/C++ and Pascal but in a more modern formulation.)

1
2
3
4
5
6
def wrap(x:Double):Double = {
    var nx = x
    while(nx < 0) nx += windowSize
    while(nx > windowSize) nx -= windowSize
    nx
}

The definition of the panel is where the actual drawing (and redrawing) of the game takes place. It also shows how to clearly separate the drawing from the reactions to events of interest (keyboard and mouse). Notably, we can handle these events without having to use classes. This allows us to stay focused on design principles instead of the vagaries of event objects and interfaces (even though these details are still present, being able to match the event’s type allows us to avoid premature complexity from a student perspective.)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
val panel = new Panel {
    override def paint(g:Graphics2D) {
        g.setPaint(Color.black)
        g.fillRect(0,0,size.width,size.height)
        g.setPaint(Color.lightGray)
        for(a <- asteroids) g.fill(new Ellipse2D.Double(a.x-a.size/2,a.y-a.size/2,a.size,a.size))
        g.setPaint(Color.red)
        for(b <- bullets) g.fill(new Rectangle2D.Double(b.x,b.y,2,2))
        g.setPaint(Color.blue)
        g.fill(new Ellipse2D.Double(shipX-shipSize,shipY-shipSize,shipSize*2,shipSize*2))
        g.setPaint(Color.green)
        g.draw(new Ellipse2D.Double(shipX-shipSize,shipY-shipSize,shipSize*2,shipSize*2))
        g.fill(new Ellipse2D.Double(shipX+(shipSize+2)*math.cos(heading)-2,shipY+(shipSize+2)*math.sin(heading)-2,4,4))
    }
    listenTo(keys,mouse.clicks)
    reactions += {
        case kp:KeyPressed =>
            if(kp.key == Key.Left) leftDown = true
            else if(kp.key == Key.Right) rightDown = true
            else if(kp.key == Key.Up) {
                shipVx += math.cos(heading)*0.2
                shipVy += math.sin(heading)*0.2
            } else if(kp.key == Key.Down) {
                shipVx -= math.cos(heading)*0.2
                shipVy -= math.sin(heading)*0.2
            } else if(kp.key == Key.Space) {
                bullets ::= Bullet(shipX+(shipSize+2)*math.cos(heading),shipY+(shipSize+2)*math.sin(heading),shipVx+3*math.cos(heading),shipVy+3*math.sin(heading),0)
            }
        case kp:KeyReleased =>
            if(kp.key == Key.Left) leftDown = false
            else if(kp.key == Key.Right) rightDown = false
        case me:MouseEntered => requestFocus
    }
    preferredSize = new Dimension(windowSize,windowSize)
}

A timer is particularly useful in game design, where you want to have self-updating without user interaction. In the case of this game, whether or not the user is doing anything, asteroids continue moving, subject to their velocities. Same for bullets. There is also logic to determine collisions and whether the ship is destroyed (which ends the game).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
val timer:Timer = new Timer(10,Swing.ActionListener(e => {
    if(leftDown) heading -= math.Pi/40 
    if(rightDown) heading += math.Pi/40
    asteroids = asteroids.map(a => {
        a.copy(x = wrap(a.x+a.vx), y = wrap(a.y+a.vy))
    })
    shipX = wrap(shipX+shipVx)
    shipY = wrap(shipY+shipVy)
    var hit = List[Asteroid]()
    bullets = bullets.map(b => {
        b.copy(x = wrap(b.x+b.vx), y = wrap(b.y+b.vy), age = b.age+1)
    }).filter(b => {
        b.age<100 && asteroids.forall(a => {
            val dx = b.x-a.x
            val dy = b.y-a.y
            val dsqr = dx*dx+dy*dy
            val isHit = dsqr < a.size*a.size/4
            if(isHit) hit ::= a
            !isHit
        })
    })
    asteroids = asteroids.flatMap(a => {
        if(hit.contains(a)) {
            if(a.size <=10) List()
            else List.fill(4)(Asteroid(a.x+(math.random-0.5)*a.size,a.y+(math.random-0.5)*a.size,a.vx+math.random-0.5,a.vy+math.random-0.5,a.size/2))
        } else List(a)
    })
    if(asteroids.exists(a => {
            val dx = shipX-a.x
            val dy = shipY-a.y
            val dsqr = dx*dx+dy*dy
            dsqr < (shipSize+a.size/2)*(shipSize+a.size/2)
    })) timer.stop()
    panel.repaint
}))

Setup of the game is fairly concise. Create the main (Swing) frame and set the desired properties. In particular, we embed the created panel (a method that is familiar to anyone who has taught Java based UIs using AWT or Swing) and disallow frame resizing. We also center the frame on the screen (if supported). Then we display the frame and start the timer.

While there are a few details about Swing to know here, much of this code is common to all Swing application development, so it is eminently teachable–and you can always point students to the basic documentation for Java to learn the details.

1
2
3
4
5
6
7
8
9
val frame = new MainFrame {
    contents = panel
    resizable = false
    centerOnScreen
}

frame.open
panel.requestFocus
timer.start

Here’s a screenshot of the game:

_images/SimpleGame.png

5.4. CS2

  • Pure OO
  • Fewer quirks than Java
  • Powerful type system
  • Traits
  • Rich collections
  • Libraries again
  • Can make things interesting/relevant
  • Multithreading and networking
  • Eclipse (maybe) and IntelliJ (our favorite)
  • Scalable language
  • Libraries as language
  • Special methods