Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행했다.

13장

패키지

  • 파일 전체를 패키지 안에 넣기
package bobsrockets.navigation
class Navigator
  • 패키징 문법
package bobsrockets.navigation {
  class Navigator
}
  • 여러 패키지
package bobsrockets {
  package navigation {
    class Navigator

    package tests {
      class NavigatorSuite
    }
}

관련 코드 접근

  • 클래스가 속한 패키지 안에서는 접두어 없이 해당 클래스에 접근 가능
  • 패키지를 포함하는 부모 패키지는 접두어 없이 해당 패키지 접근 가능
  • 중괄호 패키징 문법을 사용하면 패키징 스코프 밖에서 접근 가능한 모든 이름을 패키징 안에서 사용 가능
package bobsrockets {
  package navigation {
    class Navigator {
      val map = new StarMap // bobsrockets.navigation.StarMap
    }
    class StarMap
  }

  class Ship {
    val nav = new navigation.Navigator  // bobsrockets.navigation.Navigator
  }

  package fleets {
    class Fleet {
      def addShip() {
        new Ship        // bobsrockets.Ship
      }
    }
  }
}
  • 연쇄 패키지 절 : 괄호 없이 여러 패키지 절 사용
package bobsrockets // 중괄호 생략
package fleets      // 중괄호 생략
class Fleet {
  def addShip() { new Ship }
}
  • 숨겨진 패키지 이름 접근
package launch {
  class Booster3
}

package bobsrockets {
  package navigation {
    package launch {
      class Booster1
    }

    class MissionControl {
      val booster1 = new launch.Booster1                // 가장 좁은 패키지 스코프 안에 있음
      val booster2 = new bobsrockets.launch.Booster2    // 전체 패키지 지정
      val booster3 = new _root_.launch.Booster3         // 최상위 launch 패키지
    }
  }

  package launch {
    class Booster2
  }
}

임포트

  • 자바의 싱글 타입 임포트
// Fruit
import bobsdelights.Fruit
  • 자바의 주문식 임포트(*)
// bobsdelights의 모든 멤버
import bobsdelights._
  • 자바의 정적 클래스 필드 임포트
// Fruit의 모든 멤버
import bobsdelights.Fruits._
  • import 문은 코드 어디에나 위치 가능
def showFruit(fruit: Fruit) {
  import fruit._
  println(name +"s are"+ color) // fruit.name, fruit.color
}
  • 패키지, 객체 참조 가능
import java.util.regex
class AStarB {
  val pat = regex.Pattern.compile("a*b")    // 싱글톤 객체 java.util.regex.Pattern
}
  • 불러온 멤버 이름 중 일부를 숨기거나 다른 이름 지정 가능
    • x : x를 불러온다
    • x => y : x를 y라는 이름으로 불러온다
    • x => _ : x를 제외하고 불러온다
    • _ : 나머지를 모두 가져온다
// 일부 멤버만 불러온다
import Fruits.{Apple, Orange}

// 다른 이름 지정
import Fruits.{Apple => McIntosh, Orange}

// import Fruits._ 과 동일
import Fruits.{_}

// import Fruits._ 과 동일하나 Apple의 이름만 바꾼다
import Fruits.{Apple => McIntosh, _}

// Pear를 제외한 모든 멤버를 불러온다
import Fruits.{Pear => _, _}

암시적 임포트

  • 세가지 임포트 절을 모든 스칼라 소스 위에 추가하는 것과 같은 동작 있음
import java.lang._
import scala._
import.Predef._
  • 나중에 임포트한 패키지에 들어있는 이름이 앞에서 임포트한 이름을 가리게 됨

접근 수식자

비공개 멤버 private

자바와 달리 외부 클래스가 내부 클래스에 있는 비공개 멤버에 접근 불가

class Outer {
  class Inner {
    private def f() { ... }
    class InnerMost {
      f() // 문제 없음
    }
  }

  (new Inner).f()   // 오류
}

보호 멤버 protected

자바와 달리 보호 멤버를 정의한 클래스의 서브클래스에서만 접근 가능

package p {
  class Super {
    protected def f() { ... }
  }

  class Sub extends Super {
    f() // 문제 없음
  }

  class Other {
    (new Super).f() // 오류
  }
}

공개 멤버

privateprotected가 없는 멤버

보호 스코프

package bobsrockets

package navigation {
  private[bobsrockets] class Navigator {    // bobsrockets 내부에서 접근 가능
    protected[navigation] def useStarChart() {} // bobsrockets.navigation 내부와 bobsrockets.navigation.Navigator 서브 클래스에서 접근 가능
    
    class LegOfJourney {
      private[Navigator] val distance = 100 // bobsrockets.navigation.Navigator 내부에서 접근 가능
    }
    private[this] var speed = 200   // bobsrockets.navigation.Navigator 클래스의 객체 자신만 접근
  }
}

package launch {
  import navigation._
  object Vehicle {
    private[launch] val guide = new Navigator   // bobsrockets.launch 내부에서 접근 가능
  }
}
  • 수식자 없음 : 전체 접근 가능
  • private[bobsrockets] : bobsrockets 패키지 내부에서 접근 가능
  • private[navigation] : 자바의 패키지 접근
  • private[Navigator] : 자바의 private와 같음
  • private[LegOfJourney] : 스칼라의 private와 같음
  • private[this] : 어떤 객체 자신만 접근 가능

동반 객체

비공개, 보호 멤버에 대해서 동반 객체와 클래스는 접근 권리를 공유

패키지 객체

  • 패키지 전체 스코프에 도우미 메소드를 두는 경우, 패키지 최상위 수준에 넣을 수 있음
  • 패키지 객체는 package.class라는 이름의 클래스 파일로 컴파일되므로, 소스 파일도 동일하게 작성하는 것이 좋음
// bobsdelights/package.scala
package object bobsdelights {
  def showFruits(fruit : Fruit) {
    import fruit._
    println(name + "s are"+ color)
  }
}

// PrintMenu.scala
package printmenu
import bobsdelights.Fruits
import bobsdelights.showFruit
object PrintMenu {
  def main(args: Array[String]) {
    for (fruit <- Fruits.menu) {
      showFruit(fruit)
    }
  }
}


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행했다.

12장

트레이트 동작

  • 기본 부모 클래스는 AnyRef
trait Philosophical {
  def philosophize() {
    println("I consume memory, therefore I am!")
  }
}
  • 믹스인 : extends. 트레이트의 부모클래스를 암시적 상속
class Frog extends Philosophical {
  override def toString = "green"
}
  • 트레이트도 타입을 정의함
val phil: Philosophical = new Frog
  • 트레이트는 여러개를 믹스인 가능 : with
// 부모 클래스를 상속한 클래스에 믹스인
class Animal
class Frog extends Animal with Philosophical {
  override def toString = "green"
}

// 여러 트레이트 믹스인
trait HasLegs
class Frog extends Animal with Philosophical with HasLegs {
  override def toString = "green"
}
  • 트레이트 구현 오버라이드 가능 : override
class Animal
class Frog extends Animal with Philosophical {
  override def toString = "green"
  override def philosophize() {
    println("It ain't easy being "+ toString +"!")
  }
}

val phrog: Philosophical = new Frog
phrog.philosophize()    // 오버라이드 되었기 때문에 "It ain't easy being green!" 출력
  • 클래스 정의하면서 할 수 있는 모든 것 가능 : 트레이트에 필드 선언 등
  • '클래스' 파라미터를 가질 수 없음(생성자 파라미터 불가)
  • super 호출을 동적으로 바인딩 함(믹스인 하는 클래스에 따라 달라짐)

간결한 인터페이스 / 풍부한 인터페이스

  • 간결한 인터페이스 : 메소드 수가 적어 구현하는 측이 편리
  • 풍부한 인터페이스 : 많은 메소드 중 필요에 일치하는 메소드를 선택/사용하여 사용하는 측 편리
  • 간결한 인터페이스를 풍부한 인터페이스로 만들 때 트레이트를 사용할 수 있음
    1. 간결한 인터페이스 역할을 하는 추상 메소드 구현
    2. 추상 메소드를 활용해 풍부한 인터페이스 역할을 할 여러 메소드를 동일 트레이트 안에 구현

예제

class Point(val x: Int, val y: Int)

trait Rectangular {
  def topLeft: Point
  def bottomRight: Point

  def left = topLeft.x
  def right = bottomRight.x
  def width = right - left
  // ...
}

abstract class Component extends Rectangular {
  // ...
}

class Rectangle(val topLeft: Point, val bottomRight: Point) extends Rectangular {
  // ...
}

Orderd 트레이트

  • 순서를 표한하는 트레이트
  • 사용
    1. Ordered 트레이트를 타입 파라미터(19장) 명시하여 믹스인 Ordered[C]
    2. compare 메소드 정의
// Before
class Rational(n: Int, d: Int) {
  // ...
  def < (that: Rational) =
    this.numer * that.denom > that.numer * this.denom
  def > (that: Rational) = that < this
  def <= (that: Rational) = (this < that) || (this == that)
  def >= (that: Rational) = (this > that) || (this == that)
}

// After
class Rational(n: Int, d: Int) extends Ordered[Rational] {
  // ...
  def compare(that: Rational) =
    (this.numer * that.denom) - (that.numer * this.denom)
}
  • equals는 직접 정의해야함(30장 우회 방법)

변경 쌓기

  • 클래스의 메소드를 변경하고, 변경 위에 다른 변경을 계속 쌓을 수 있음
abstract class IntQueue {
  def get(): Int
  def put(x: Int)
}

import scala.collection.mutable.ArrayBuffer

class BasicIntQueue extends IntQueue {
  private val buf = new ArrayBuffer[Int]
  def get() = buf.remove(0)
  def put(x: Int) { buf += x }
}

trait Doubling extends IntQueue {
  abstract override def put(x: Int) { super.put(2 * x) }
}

// 믹스인
class MyQueue extends BasicIntQueue with Doubling
val queue = new MyQueue
queue.put(10)
queue.get()     // 20

// 인스턴스 생성시 믹스인
val queue2 = new BasicIntQueue with Doubling
queue2.put(10)
queue2.get()    // 20
  • 여러 트레이트로 변경을 여러 순서로 쌓을 수 있음
trait Incrementing extends IntQueue {
  abstract override def put(x: Int) { super.put(x + 1) }
}

trait Filtering extends IntQueue {
  abstract override def put(x: Int) {
    if (x >= 0) super.put(x)
  }
}

// 음수이면 제거 후에 1 증가
val queue = (new BasicIntQueue with Incrementing with Filtering)
queue.put(-1);queue.put(0);queue.put(1)
queue.get()     // 1
queue.get()     // 2

// 1 증가 후에 음수이면 제거
val queue2 = (new BasicIntQueue with Filtering with Incrementing)
queue.put(-1);queue.put(0);queue.put(1)
queue.get()     // 0
queue.get()     // 1
queue.get()     // 2

선형화

  • 상속의 경우 super를 호출할 때에 어떤 메소드를 부를지 컴파일 시점에 이루어진다.
  • 트레이트는 선형화로 부를 메소드를 결정한다.
class Animal
trait Furry extends Animal
trait HasLegs extends Animal
trait FourLegged extends HasLegs
class Cat extends Animal with Furry with FourLegged
  1. Animal -> AnyRef -> Any
  2. Furry -> (Animal -> AnyRef -> Any) // 이미 선형화한 부분은 제외. 중복 제외
  3. FourLegged -> HasLegs -> (Furry -> Animal -> AnyRef -> Any)
  4. Cat -> (FourLegged -> HasLegs -> Furry -> Animal -> AnyRef -> Any)

super 사용시, 선형화 순서상 자기보다 바로 오른쪽에 있는 첫 번째 구현을 호출

트레이트 사용 고려 사항

  • 재사용하지 않는 경우 -> 클래스
  • 관련 없는 클래스에서 여러번 재사용 -> 트레이트
  • 자바 코드에서 스칼라 내용을 상속 -> 추상 클래스
  • 컴파일한 바이트 코드 형태로 배포 -> 추상 클래스
  • 효율이 중요 -> 클래스
  • 잘 모르겠으면 -> 트레이트


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행했다.

11장

클래스 계층 구조

Any

최상위 클래스

  • final def ==(that: Any): Boolean : equals
  • final def !=(that: Any): Boolean : not equals
  • def equals(that: Any): Boolean
  • def ##: Int : hashCode
  • def hashCode: Int
  • def toString: String

자바(equals, hashCode)와 달리 스칼라(==, !=, ##)는 일관성을 위해 박싱 클래스를 특별 취급함

AnyVal, AnyRef

Any의 서브 클래스

AnyVal

모든 스칼라의 내장 값 클래스(Byte, Short, Char, Int, Long, Float, Double, Boolean, Unit)의 부모 클래스

  • Byte, Short, Char, Int, Long, Float, Double, Boolean
    • 자바의 원시 타입에 대응
    • 실행 시점에 자바 원시 타입 값으로 표현함
    • new 를 사용하여 인스턴스화할 수 없음(추상클래스이며 final 클래스임)
    • 암시적 변환 제공(widening)
      • Int -> Long -> Float -> Double
      • Char -> Int
    • 더 많은 기능 제공을 위한 암시적 변환(scala.runtime.RichInt 등)
42 max 43   // 43
42 min 43   // 42
1 until 5   // Range(1, 2, 3, 4)
1 to 5      // Range(1, 2, 3, 4, 5)
3.abs       // 3
(-3).abs    // 3
  • Unit
    • 자바의 void 와 유사
    • 값 : ()

AnyRef

모든 참조 클래스의 기반 클래스(java.lang.Object)

Nothing

모든 클래스의 서브클래스

기본 클래스 구현

  • 표준 연산은 자바 기본 연산을 사용하지만, 객체가 필요한 경우 대응하는 클래스를 사용
  • 값 타입의 경우, ==는 자연적인 동일성(값을 비교)
// 자바 코드
boolean isEqual(int x, int y) {
    return x == y;
}

isEqual(421, 421);  // true

boolean isEqual2(Integer x, Integer y) {
    return x == y;
}

isEqual2(421, 421); // false

// 스칼라 코드
def isEqual(x: Int, y: Int) = x == y

isEqual(421, 421)   // true

def isEqual2(x: Any, y: Any) = x == y

isEqual2(421, 421)  // true
  • 참조 동일성 확인 : eq, ne
val x = new String("abc")
val y = new String("abc")

x == y  // true
x eq y  // false
x ne y  // true

바닥에 있는 타입

scala.Null

  • AnyRef를 상속한 모든 참조 타입의 서브 클래스
  • 값 타입과는 호환되지 않음
val i: Int = null   // error

scala.Nothing

  • 모든 다른 타입의 서브 타입
def error(message: String): Nothing =
  throw new RuntimeException(message)

def divide(x: Int, y: Int): Int =
  if (y != 0) x / y
  else error("can't divide by zero")    // return Nothing for Int


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행하고 있다.


10장

추상 클래스

  • 추상 클래스 : 구현이 없는 추상 멤버가 있는 클래스
  • abstract
  • 인스턴스를 만들 수 없음
  • 추상 메소드 : 추상 클래스 내의 구현이 없는 메소드
  • 구체 메소드 : 구현이 있는 메소드
abstract class Element {
    def contents: Array[String]
}

파라미터 없는 메소드 정의

abstract class Element {
    def contents: Array[String]
    def height: Int = contents.length
    def width: Int = if (height == 0) 0 else contents(0).length
}
  • 파라미터 없는 메소드와 빈 괄호 메소드를 자유롭게 섞어쓸 수 있음
  • 서로 오버라이드 가능
  • 메소드 호출시 빈 괄호 생략 가능
  • 함수 호출시 프로퍼티 접근 이상의 작업이 있따면 괄호를 사용하는 것이 좋음

클래스 확장

class ArrayElement(conts: Array[String]) extends Element {
    def contents: Array[String] = conts
}
  • extends 절이 없으면 scala.AnyRef를 상속
  • 상속 : 슈퍼클래스의 모든 멤버는 서브클래스의 멤버
    • 비공개 멤버 제외
    • 이름과 파라미터가 동일한 멤버 정의가 존재하면 제외 = 오버라이드(추상 멤버의 경우는 구현)
val ae = new ArrayElement(Array("hello","world"))
ae.width // 슈퍼클래스 멤버 사용
  • 서브타입 : 슈퍼클래스의 값을 필요로 하는 곳이라면 어디나 서브클래스의 값 사용 가능
val e: Element = new ArrayElement(Array("hello")) // Element 대신 사용
  • 구성(composition) : ArrayElement 이진 클래스 코드에 배열을 가리킬 참조 필드가 들어감

메소드와 필드 오버라이드

  • 필드와 메소드가 동일한 네임스페이스에 속함
  • 파라미터 없는 메소드를 필드(val)로 오버라이드 가능
class ArrayElement(conts: Array[String]) extends Element {
    val contents: Array[String] = conts
}
  • 동일한 이름의 필드와 메소드를 동시에 정의 불가
class WontCompile {
    private var f = 0 // 필드와 메소드가 같은 이름이므로
    def f = 1         // 컴파일할 수 없음
}
  • 스칼라의 네임스페이스
    • 값 : 필드, 메소드, 패키지, 싱글톤 객체
    • 타입 : 클래스, 트레이트 이름

파라미터 필드

class ArrayElement (
    val contents: Array[String] // 파라미터와 필드를 결합한 파라미터 필드(parametric field)
) extends Element
  • val, var 사용 가능
  • private, protected, override 수식자 사용 가능

슈퍼클래스의 생성자 호출

class LineElement(s: String) extends ArrayElement(Array(s)) {
    override def width = s.length
    override def height = 1
}

override 수식자

  • 부모 클래스에 있는 구체적 멤버를 오버라이드하는 모든 멤버에 override 수식자를 붙여야 함
  • 추상 멤버 구현시에는 생략 가능
  • 우연한 오버라이드 방지

다형성(서브타입 다형성)

Element 타입 변수가 ArrayElement, LineElement 등의 객체 참조 가능

val e1: Element = new ArrayElement(Array("hello","world"))
val ae: ArrayElement = new LineElement("hello")
val e2: Element = ae

동적 바인딩

메소드는 실행 시점에 실제 그 객체가 어떤 타입인가를 따름

abstract class Element {
    def demo() {
        println("Element's implementation invoked")
    }
}

class ArrayElement extends Element {
    override def demo() {
        println("ArrayElement's implementation invoked")
    }
}

class LineElement extends Element {
    override def demo() {
        println("LineElement's implementation invoked")
    }
}

class UniformElement extends Element

def invokeDemo(e: Element) {
    e.demo()
}

invokeDemo(new ArrayElement) // ArrayElement's implementation invoked
invokeDemo(new LineElement) // LineElement's implementation invoked
invokeDemo(new UniformElement) // Element's implementation invoked

final

  • 서브클래스가 특정 멤버를 오버라이드 못하게 함
class ArrayElement extends Element {
    final override def demo() {  // 메소드 오버라이드 불가
        println("ArrayElement's implementation invoked")
    }
}
final class ArrayElement extends Element {  // 클래스 상속 불가
    override def demo() {
        println("ArrayElement's implementation invoked")
    }
}

상속과 구성 사용

  • 구성과 상속은 이미 존재하는 클래스를 이용해 새로운 클래스를 정의하는 방법
  • 구성 : 코드 재사용
  • 상속
    • is-a 관계 : 'ArrayElement는 Element이다'
    • 슈퍼클래스의 타입으로 서비클래스를 사용하는 경우

예제 구현

  • 메소드 구현
def above(that: Element): Element =
    new ArrayElement(this.contents ++ that.contents) // ++ 연산은 두 배열을 연결

def beside(that: Element): Element =
    new ArrayElement(
        for (
            (line1, line2) <- this.contents zip that.contents // zip 연산은 순서쌍으로 이뤄진 하나의 배열로 변환
        ) yield line1 + line2
    )

override def toString = contents mkString "\n"
  • 팩토리 구현
object Element {
    private class ArrayElement(
        val contents: Array[String]
    ) extends Element

    private class LineElement(s: String) extends Element {
        val contents = Array(s)
        override def width = s.length
        override def height = 1
    }

    private class UniformElement(
        ch: Char,
        override val width: Int,
        override val height: Int
    ) extends Element {
        private val line = ch.toString * width
        def contents = Array.fill(height)(line)
    }

    def elem(contents: Array[String]): Element =
        new ArrayElement(contents)
    def elem(chr: Char, width: Int, height: Int): Element =
        new UniformElement(chr, width, height)
    def elem(line: String): Element =
        new LineElement(line)
}

import Element.elem
abstract class Element {
    def contents: Array[String]
    
    def width: Int =
        if (height == 0) 0 else contents(0).length
    
    def height: Int = contents.length
    
    def above(that: Element): Element =
        elem(this.contents ++ that.contents)
    
    def beside(that: Element): Element =
        elem(
            for (
                (line1, line2) <- this.contents zip that.contents // zip 연산은 순서쌍으로 이뤄진 하나의 배열로 변환
            ) yield line1 + line2
        )

    override def toString = contents mkString "\n"
}
  • widen, heighten 메소드 추가
import Element.elem
abstract class Element {
    def contents: Array[String]
    
    def width: Int =
        if (height == 0) 0 else contents(0).length
    
    def height: Int = contents.length
    
    def above(that: Element): Element = {
        val this1 = this widen that.width
        val that1 = that widen this.width
        elem(this1.contents ++ that1.contents)
    }
    
    def beside(that: Element): Element = {
        val this1 = this heighten that.height
        val that1 = that heighten this.height
        elem(
            for (
                (line1, line2) <- this1.contents zip that1.contents // zip 연산은 순서쌍으로 이뤄진 하나의 배열로 변환
            ) yield line1 + line2
        )
    }
    
    def widen(w: Int): Element =
        if (w <= width) this
        else {
            val left = elem(' ', (w - width) / 2, height)
            val right = elem(' ', w - width - left.width, height)
            left beside this beside right
        }
    
    def heighten(h: Int): Element =
        if (h <= height) this
        else {
            val top = elem(' ', width, (h - height) / 2)
            val bot = elem(' ', width, h - height - top.height)
            top above this above bot
        }

    override def toString = contents mkString "\n"
}
  • Spiral 애플리케이션
import Element.elem
object Spiral {
    val space = elem(" ")
    val corner = elem("+")

    def spiral(nEdges: Int, direction: Int): Element = {
        if (nEdges == 1)
            elem("+")
        else {
            val sp = spiral(nEdges - 1, (direction + 3) % 4)
            def verticalBar = elem('|', 1, sp.height)
            def horizontalBar = elem('-', sp.width, 1)
            if (direction == 0)
                (corner beside horizontalBar) above (sp beside space)
            else if (direction == 1)
                (sp above space) beside (corner above verticalBar)
            else if (direction == 2)
                (space beside sp) above (horizontalBar beside corner)
            else
                (verticalBar above corner) beside (space above sp)
        }
    }

    def main(args: Array[String]) {
        val nSides = args(0).toInt
        println(spiral(nSides, 0))
    }
}


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행하고 있다.


9장

코드 중복 줄이기

  • 고차 함수 : 함수를 인자로 받는 함수
object FileMatcher {
    private def filesHere = (new java.io.File(".)).listFiles
    def fileEnding(query: String) =
        for (file <- filesHere; if file.getName.endsWith(query))
            yield file
    def fileContaining(query: String) =
        for (file <- filesHere; if file.getName.contains(query))
            yield file
    def fileRegex(query: String) =
        for (file <- filesHere; if file.getName.matches(query))
            yield file
    
}
  1. 함수값 사용
// 중복 코드를 함수로 작성
def filesMatching(query: String, matcher: (String, String) => Boolean) = {
    for (file <- filesHere; if matcher(file.getName, query))
        yield file
}

def fileEnding(query: String) =
    filesMatching(query, _.endsWith(_)) // 함수값
def fileContaining(query: String) =
    filesMatching(query, _.contains(_)) // 함수값
def fileRegex(query: String) =
    filesMatching(query, _.matches(_)) // 함수값
  1. 클로저 사용
// 클로저 사용하여 인자 제거
private def filesMatching(matcher: (String, String) => Boolean) = {
    for (file <- filesHere; if matcher(file.getName))
        yield file
}

def fileEnding(query: String) =
    filesMatching(_.endsWith(query))
def fileContaining(query: String) =
    filesMatching(_.contains(query))
def fileRegex(query: String) =
    filesMatching(_.matches(query))

클라이언트 코드 단순하게 만들기

def containsNeg(nums: List[Int]): Boolean = {
    var exists = false
    for (num <- nums)
        if (num < 0)
            exists = true
    exists
}

def containsNeg(nums: List[Int]) = nums.exists(_ < 0)
def containsOdd(nums: List[Int]): Boolean = {
    var exists = false
    for (num <- nums)
        if (num % 2 == 1)
            exists = true
    exists
}

def containsOdd(nums: List[Int]) = nums.exists(_ % 2 == 1)

커링

  • 커링 : n개의 인자를 받는 함수를 n개의 함수로 각각의 인자를 받도록 하는 것
def plainOldSum(x: Int, y: Int) = x + y
plainOldSum(1, 2)

def curriedSum(x: Int)(y: Int) = x + y  // 커링
curriedSum(1)(2)

val onePlus = curriedSum(1)_    // 인자 하나 받아서 1을 더하는 함수
onePlus(2)

새로운 제어구조 작성 - 중괄호

인자를 1개 전달하는 경우에 함수 인자를 감싸는 소괄호 대신 중괄호 사용 가능

def withPrintWriter(file: File, op: PrintWriter => Unit) {
    val writer = new PrintWriter(file)
    try {
        op(writer)
    } finally {
        writer.close()
    }
}

withWriter(
    new File("date.txt"),
    writer => writer.println(new java.util.Date)
) // 빌려주기 패턴. 자원 닫기가 보장됨
def withPrintWriter(file: File)(op: PrintWriter => Unit) {
    val writer = new PrintWriter(file)
    try {
        op(writer)
    } finally {
        writer.close()
    }
}

val file = new File("date.txt")
withPrintWriter(file) {
    writer => writer.println(new java.util.Date)
} // 중괄호로 변경

이름에 의한 호출

var assertionsEnabled = true
def myAssert(predicate: () => Boolean) =
    if (assertionsEnabled && !predicate())
        throw new AssertionError

myAssert(() => 5 > 3)

def byNameAssert(predicate: => Boolean) = // 이름에 의한 호출 파라미터
    if (assertionsEnabled && !predicate)
        throw new AssertionError

byNameAssert(5 > 3) // 빈 파라미터 타입 생략 가능

def boolAssert(predicate: Boolean) =
    if (assertionsEnabled && !predicate)
        throw new AssertionError

boolAssert(5 > 3)   // 괄호 안의 표현식이 바로 실행됨


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행하고 있다.


8장

메소드

  • 객체의 멤버인 함수
import scala.io.Source
object LongLines {
    def processFile(filename: String, width: Int) { // 메소드
        val source = Source.fromFile(filename)
        for (line <- source.getLines())
            processLine(filename, width, line)
    }

    private def processLine(filename: String, width: Int, line: String) { // 도우미 메소드
        if (line.length > width)
            println(filename +": "+ line.trim)
    }
}

지역 함수

  • 함수 안에서 정의한 함수
  • 정의를 감싸고 있는 블록 내에서만 접근 가능
def processFile(filename: String, width: Int) {
    def processLine(filename: String, width: Int, line: String) { // 지역 함수
        if (line.length > width)
            println(filename +": "+ line)
    }

    val source = Source.fromFile(filename)
    for (line <- source.getLines())
        processLine(line)
}
  • 바깥 함수의 파라미터를 사용 가능
def processFile(filename: String, width: Int) {
    def processLine(line: String) {
        if (line.length > width) // 바깥 함수의 파라미터 width를 사용
            println(filename +": "+ line) // 바깥 함수의 파라미터 filename을 사용
    }

    val source = Source.fromFile(filename)
    for (line <- source.getLines())
        processLine(filename, width, line)
}

1급 계층 함수

  • 함수를 정의, 호출, 리터럴로 표기해 값처럼 주고받을 수 있다.
  • 함수 리터럴 : 소스 코드 상의 표기로 존재
  • 함수 값(function value) : 실행 시점에 객체로 존재
(x: Int) => x + 1 // 함수 리터럴 예
scala> var increase = (x: Int) => x + 1
increase: Int => Int = <function1>

scala> increase(10)
res0: Int = 11

scala> increase = (x: Int) => x + 9999 // 재할당 가능
increase: Int => Int = <function1>

scala> increase(10)
res1: Int = 10009
val someNumbers = List(-11, -10, -5, 0, 5, 10)
someNumbers.foreach((x: Int) => println(x)) // 함수 리터럴 사용 예 - foreach
someNumbers.filter((x: Int) => x > 0) // 함수 리터럴 사용 예 - filter

간단한 형태의 함수 리터럴

someNumbers.filter((x: Int) => x > 0)
someNumbers.filter((x) => x > 0) // 타깃 타이핑
someNumbers.fileter(x => x > 0)

위치 표시자

  • 하나 이상의 파라미터에 대한 위치 표시자로 밑줄(_) 사용
  • '채워넣어야 할 빈칸'
someNumbers.fileter(x => x > 0)
someNumbers.filter(_ > 0)
val f = _ + _   // error
val f = (_: Int) + (_: Int) // 타입 명시

부분 적용한 함수

someNumbers.foreach(x => println(x))
someNumbers.foreach(println _)  // 전체 인자 목록에 대한 위치 표시자 사용 = 부분 적용한 함수
scala> def sum(a: Int, b: Int, c: Int) = a + b + c
sum: (a: Int, b: Int, c: Int)Int

scala> val a = sum _    // 인자 3개를 받는 함수 값을 인스턴스화
a: (Int, Int, Int) => Int = <function3>

scala> a(1,2,3) // == a.apply(1,2,3)
res0: Int = 6
  1. 변수 a : 함수 값 객체
  2. 함수 값 : 컴파일러가 sum _을 해석해 자동으로 만든 클래스의 인스턴스
  3. 클래스 : 인자 3개를 받는 apply 메소드 존재
  4. apply 메소드 : sum _에서 빠진 인자가 3개이기 때문에 3개의 인자를 받음
  • 밑줄 있는 함수 값으로 감싸면, 메소드나 중첩 함수를 변수에 할당하거나, 인자로 전달 가능
  • 인자 중 일부만 넘기는 것도 가능
val b = sum(1, _:Int, 3)
  • 명확하게 함수가 필요한 경우 밑줄 생략 가능
someNumbers.foreach(println _)
someNumbers.foreach(println)

클로저

  • 클로저(closure)
    • 함수 리터럴로부터 실행 시점에 만들어낸 객체인 함수 값
    • 함수 리터럴 본분에 있는 모든 자유 변수에 대한 바인딩을 포획해서 자유 변수가 없게 닫음
  • 자유 변수(free variable) : 함수에서 의미를 부여하지 않은 변수
  • 바운드 변수(bound variable) : 함수의 문맥 내에서만 의미가 있는 변수
  • 닫힌 코드 조각(closed term) : 자유 변수가 없는 함수 리터럴
  • 열린 코드 조각(open term) : 자유 변수가 있는 함수 리터럴 -> 클로저
(x: Int) => x + more
// x : 바운드 변수
// more : 자유 변수
  • 클로저가 변화를 감지함.
  • 표획한 인자는 힙에 재배치됨

특별한 형태의 함수 호출

반복 파라미터

def echo(args: String*) =   // args: Array[String]
    for (arg <- args) println(arg)
echo("hello", "world")

val arr = Array(...)
echo(arr: _*)   // 배열을 직접 전달하는 경우

이름 붙인 인자

def speed(distance: Float, time: Float): Float =
    distance / time

speed(100, 10)
speed(distance = 100, time = 10)    // 이름 붙인 인자 사용
speed(time = 10, distance = 100)    // 순서 변경 가능

디폴트 인자 값

  • 인자에 기본값 지정시 생략 가능
def printTime(out: java.io.PrintStream = Console.out) =
    out.println("time = "+ System.currentTimeMillis())

printTime() // 디폴트 값인 Console.out 사용
printTime(Console.err)  // 인자 지정
  • 일부 인자만 이름 붙여서 지정 가능
def printTime2(out: java.io.PrintStream = Console.out, divisor: Int = 1) =
    out.println("time = "+ System.currentTimeMillis()/divisor)

printTime2()
printTime2(out = Console.err)
printTime2(divisor = 1000)

꼬리 재귀

def approximate(guess: Double): Double =
    if (isGoodEnough(guess)) guess
    else approximate(improve(guess))    // 재귀 호출
  • 마지막에 자신을 재귀호출 하는 꼬리 재귀의 경우 최적화함
  • 단, 간접적 재귀(번갈아 호출하는 등), 함수 값 호출 등은 최적화되지 않음


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행하고 있다.


7장

if 표현식

var filename = "default.txt"
if (!args.isEmpty)
  filename = args(0)
val filename =
  if (!args.isEmpty) args(0)
  else "default.txt"

while 루프

  • 수행 결과는 Unit 타입
  • 할당의 결과는 Unit 값(())
(line == readLine()) != "" // () != "" 이므로 항상 참

while

while (a != 0) {
    val temp = a
    a = b%a
    b = temp
}

do-while

do {
    line = readLine()
    println("Read: " + line)
} while (line != "")

for 표현식

컬렉션 순회

  • 배열 순회
val filesHere = (new java.io.File(".")).listFiles
for (file <- filesHere)
  println(file)
  • Range 순회
for (i <- 1 to 4) // 1,2,3,4
  println("Iteration "+i)
for (i <- 1 until 4) // 1,2,3
  println("Iteration "+i)

필터링

val filesHere = (new java.io.File(".")).listFiles
for (file <- filesHere if file.getName.endsWith(".scala")) // '.scala'로 끝나는 파일만
  println(file)
for {
    file <- filesHere
    if file.isFile // 필터 추가
    if file.getName.endsWith(".scala")
} println(file)

중첩 순회

def fileLines(file: java.io.File) =
  scala.io.Source.fromFile(file).getLines().toList

def grep(pattern: String) =
  for {
    file <- fileHere
    if file.getName.endsWith(".scala")  // 바깥 루프
    line <- fileLines(file)
    if line.trim.matches(pattern)  // 안쪽 루프
   } println(file +": "+ line.trim)

grep(".*gcd.*")

변수 바인딩

def fileLines(file: java.io.File) =
  scala.io.Source.fromFile(file).getLines().toList

def grep(pattern: String) =
  for {
    file <- fileHere
    if file.getName.endsWith(".scala")
    line <- fileLines(file)
    trimmed = line.trim  // val 변수 처럼 선언하고 사용
    if trimmed.matches(pattern)
  } println(file +": "+ trimmed)

grep(".*gcd.*")

컬렉션 생성

def scalaFiles =
  for {
    file <- filesHere
    if file.getName.endsWith(".scala")
  } yield file
  • yield 는 전체 본문의 앞에 위치

try 표현식

예외 발생

throw new IllegalArgumentException // throw
  • throw는 Nothing이라는 타입을 결과값으로 갖는다.

예외 잡기

try {
  val f = new FileReader("input.txt")
} catch {
  case ex: FileNotFoundException => // ...
  case ex: IOExceptoin => // ...
}

finally 절

val file = new FileReader("input.txt")
try {
  // ...
} finally {
  file.close()
}

결과값

  • try, catch 값이 결과값으로 사용됨
  • finally는 결과값을 바꾸지 않음

match 표현식

  • case 내에는 어떤 종류의 상수라도 사용 가능
  • 모든 case마다 break문이 암묵적으로 존재
val firstArg = if (args.length > 0) args(0) else ""
firstArg match {
  case "salt" => println("pepper")
  case "chips" => println("salsa")
  case "eggs" => println("bacon")
  case _ => println("huh?")     // default case
}

break / continue

  • continue -> if
  • break -> 불리언 변수
  • 재귀 함수 사용(꼬리 재귀 호출)
  • scala.util.control.Breaks 클래스
import scala.util.control.Breaks._
import java.io._

val in = new BufferedReader(new InputStreamReader(System.in))

breakable {
    while (true) {
        println("? ")
        if (in.readLine() == "") break
    }
}

변수 스코프

  • 중괄호 사용시 새로운 스코프 생성(예외 존재 - for 등)
  • 안쪽 스코프에서 바깥 스코프의 동일 이름 변수를 선언하면 바깥 변수는 가려짐(shadow)
  • 인터프리터는 구문마다 새로운 스코프 생성


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행하고 있다.


6장

생성자

주 생성자

class Rational(n: Int, d: Int)
  • 클래스 파라미터 : n, d

주 생성자 코드

  • 클래스 내부에 있으면서 필드나 메소드 정의에 들어 있지 않은 코드는 주 생성자에 들어감
class Rational(n: Int, d: Int) {
    println("Created "+ n +"/"+d)
}

toString 메소드 오버라이드

class Rational(n: Int, d: Int) {
    override def toString = n +"/"+ d
}

전제 조건 확인

  • require 문 사용
  • 인자가 false 이면 IllegalArgumentException 예외 발생
class Rational(n: Int, d: Int) {
    require(d != 0)
    override def toString = n +"/"+ d
}

공개 필드

class Rational(n: Int, d: Int) {
    require(d != 0)
    val numer: Int = n
    val denom: Int = d
    override def toString = n +"/"+ d
    def add(that: Rational): Rational =
        new Rational(
            numer * that.denom + that.numer * denom,
            denom * that.denom
        )
}
  • 파라미터 필드(10.6절)를 사용하면 간단하게 작성 가능

자기 참조

  • this
def lessThan(that: Rational) =
    this.numer * that.denom < that.numer * this.denom // this 생략 가능
def max(that: Rational) =
    if (this.lessThan(that)) that else this // lessThan 앞의 this는 생략 가능

보조 생성자

  • def this(...) 으로 시작
class Rational(n: Int, d: Int) {
    require(d != 0)
    val numer: Int = n
    val denom: Int = d
    def this(n: Int) = this(n, 1) // 보조 생성자
    override def toString = n +"/"+ d
    def add(that: Rational): Rational =
        new Rational(
            numer * that.denom + that.numer * denom,
            denom * that.denom
        )
}

비공개 필드, 메소드

class Rational(n: Int, d: Int) {
    require(d != 0)
    private val g = gcd(n.abs, d.abs) // 비공개 필드
    val numer: Int = n / g // 초기화 코드
    val denom: Int = d / g // 초기화 코드
    def this(n: Int) = this(n, 1)
    override def toString = n +"/"+ d
    def add(that: Rational): Rational =
        new Rational(
            numer * that.denom + that.numer * denom,
            denom * that.denom
        )
    private def gcd(a: Int, b: Int): Int = // 비공개 메소드
        if (b == 0) a else gcd(b, a % b)
}

연산자 메소드

class Rational(n: Int, d: Int) {
    require(d != 0)
    private val g = gcd(n.abs, d.abs)
    val numer: Int = n / g
    val denom: Int = d / g
    def this(n: Int) = this(n, 1)
    override def toString = n +"/"+ d
    def + (that: Rational): Rational = // 덧셈 연산자 정의
        new Rational(
            numer * that.denom + that.numer * denom,
            denom * that.denom
        )
    def * (that: Rational): Rational = // 곱셈 연산자 정의
        new Rational(numer * that.numer, denom * that.denom)
    private def gcd(a: Int, b: Int): Int =
        if (b == 0) a else gcd(b, a % b)
}

식별자 규칙

  • 영숫자 식별자 : 문자나 밑줄(_)로 시작. 문자, 숫자, 밑줄 가능
  • 필드, 메소드 인자, 지역 변수, 함수 등 : camelCase 사용이 관례
  • 클래스, 트레이트, 상수 등 : CamelCase 사용이 관례
  • 연산자 식별자 : 하나 이상의 연산자 문자로 이루어져 있음. + ++ ::: <?> :->
  • 혼합 식별자 : unary_+ myvar_=
  • 리터럴 식별자 : 역따옴표로 둘러싼 문자열

메소드 오버로드

class Rational(n: Int, d: Int) {
    require(d != 0)
    private val g = gcd(n.abs, d.abs)
    val numer: Int = n / g
    val denom: Int = d / g
    def this(n: Int) = this(n, 1)
    override def toString = n +"/"+ d
    def + (that: Rational): Rational =
        new Rational(
            numer * that.denom + that.numer * denom,
            denom * that.denom
        )
    def + (i: Int): Rational = // 덧셈 연산자 메소드 오버로드
        new Rational(numer + i * denom, denom)
    def - (that: Rational): Rational =
        new Rational(
            numer * that.denom - that.numer * denom,
            denom * that.denom
        )
    def - (i: Int): Rational = // 뺄셈 연산자 메소드 오버로드
        new Rational(numer - i * denom, denom)
    def * (that: Rational): Rational =
        new Rational(numer * that.numer, denom * that.denom)
    def * (i: Int): Rational = // 곱셈 연산자 메소드 오버로드
        new Rational(numer * i, denom)
    def / (that: Rational): Rational =
        new Rational(numer * that.denom, denom * that.numer)
    def / (i: Int): Rational = // 나눗셈 연산자 메소드 오버로드
        new Rational(numer, denom * i)
    private def gcd(a: Int, b: Int): Int =
        if (b == 0) a else gcd(b, a % b)
}

암시적 타입 변환

  • 암시적 타입 변환 메소드 지정 가능
  • 해당 스코프 내에 메소드가 존재해야 동작함
implicit def intToRational(x: Int) = new Rational(x)


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행하고 있다.


5장

기본 타입

  • Byte / Short / Int / Long : 8 / 16 / 32 / 64 bits 부호 있는 정수(2의 보수)
  • Char : 16 bits 유니코드 문자
  • String : Char 시퀀스
  • Float / Double : 32 / 64 bits IEEE 754 단정도/배정도 부동소수점 수
  • Boolean : true / false

리터럴

정수

  • 10진 : 5, 7, ..
  • 16진 : 0x5, 0x00FF
  • Long : 35L, 31l

부동소수점

  • 1.2345
  • 1,2345e1
  • 123E45
  • Float : 1.2345F, 3e5f
  • Double(기본) : 3e5D

문자

  • 'A'
  • '\101'
  • '\u0041'

이스케이프 시퀀스(escape sequence)

  • \n : 줄바꿈(\u000A)
  • \b : 백스페이스(\u0008)
  • \t : 탭(\u0009)
  • \f : 페이지 넘김(\u000C)
  • \r : 줄 맨 앞으로(\u000D)
  • " : 큰따옴표(\u0022)
  • ' : 작은따옴표(\u0027)
  • \ : 역슬래스(\u005C)

문자열

  • "hello"
  • """Welcome ... """
  • stripMargin
println("""|Welcome to Ultamix 3000.
           |Type "HELP" for help.""".stripMargin)

심볼

  • 작은따옴표 뒤에 오는 식별자(알파벳,숫자) : 'ident
  • 'ident -> 내부에서 Symbol("ident") 호출됨
  • intern. 같은 심볼 리터럴은 동일한 객체를 참조.

불리언

  • true / false

연산자

  • 어떤 메소드든지 연산자가 될 수 있음
s.indexOf('o', 5)
s indexOf ('o', 5) // 동일함
  • 연산자는 메소드
    • 중위 연산자 : 1 + 2 == (1).+(2)
    • 전위 연산자 : -2.0 == (2.0).unary_-
    • 후위 연산자 : s toLowerCase == s.toLowerCase()

산술 연산자

  • +, -, *, /, %

관계, 논리 연산자

  • , <, >=, <=, !

  • &&, ||

비트 연산자

  • &, |, ^

객체 동일성

  • ==, != 사용하여 두 객체가 동일한지 여부를 확인
  • null을 포함한 어떤 객체라도 사용 가능
  • 내부적으로 null인지 확인하고, null 이 아닌 경우 equals 메소드를 수행함

연산자 우선순위

  • 연산자 시작하는 문자에 따라서 아래와 같은 순서로 우선 순위 판단
  1. (all other special characters)
  2. */%
  3. +-
  4. :
  5. =! <> &
  6. ˆ
  7. |
  8. (all letters)
  9. (all assignment operators)
  • 메소드가 ':'로 끝나면 오른쪽에서 왼쪽으로 연산되고, 다른 경우는 왼쪽에서 오른쪽으로 연산

리치 래퍼

  • 묵시적 변환(implicit conversion)으로 기본 타입에 더 많은 메소드를 실행 가능
0 max 5             // 5
0 min 5             // 0
-2.7 abs            // 2.7
-2.7 round          // -3L
1.5 isInfinity      // false
(1.0/0) isInfinity  // true
4 to 6              // Range(4,5,6)
"bob" capitalize    // "Bob"
"robert" drop 2     // "bert"
  • 위와 같은 메소드는 다음과 같은 레퍼 타입에 정의되어 있음
    • Byte - scala.runtime.RichByte
    • Short - scala.runtime.RichShort
    • Int - scala.runtime.RichInt
    • Char - scala.runtime.RichChar
    • Float - scala.runtime.RichFloat
    • Double - scala.runtime.RichDouble


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행하고 있다.


4장

클래스

  • 클래스 정의
class ChecksumAccumulator {
    // 클래스 정의
}
  • 객체 생성 : new
new ChecksumAccumulator
  • 필드, 메소드는 멤버

필드

  • 필드 정의 : val, var
  • 인스턴스 변수라고도 함
  • 기본 접근 수준은 전체 공개
  • 비공개 필드 : private
class ChecksumAccumulator {
    private var sum = 0
}

메소드

  • 메소드 정의 : def
  • 메소드 파라미터는 val
  • 권장 스타일
    • return을 명시적으로 사용하지 않는 것 -> 메소드는 한 값을 계산하는 표현식
    • 하나의 표현식만 있으면 중괄호 생략
    • 결과식이 짧으면 def 문 줄에 함께 쓰기
class ChecksumAccumulator {
    private var sum = 0
    def add(b: Byte): Unit = sum += b
    def checksum(): Int = ~(sum & 0xFF) + 1
}
  • 결과 타입이 Unit 인 경우
    • 부수 효과를 위해 사용
    • = 생략하고 {}로 감싸는 것으로 표현 가능
    def add(b:Byte) { sum += b }

세미콜론 추론

다음 세가지 경우가 아니면 줄의 끝은 세미콜론으로 취급된다.

  1. 줄이 명령을 끝낼 수 있는 단어로 끝나지 않는다. 마침표(.)나 중위 연산자 등의 줄의 맨 끝에 있는 경우
  2. 다음 줄의 맨 앞이 문장을 시작할 수 없는 단어로 시작한다.
  3. 줄이 () 사이나 [] 사이에서 끝난다.

싱글톤 객체

  • 클래스 정의와 비슷하나 object 키워드 사용
object ChecksumAccumulator {
    // ...
}
  • 싱글톤 객체는 1급 계층
  • 파라미터를 전달할 방법 없음
  • 클래스를 확장(extend)하거나 트레이트를 믹스인(mix in) 가능
  • 자체 타입을 정의하지는 않음

동반 객체와 동반 클래스

  • 동반 객체(companion object) : 어떤 클래스 이름과 동일한 이름의 싱글톤 객체
  • 동반 클래스(companion class) : 어떤 싱글톤 객체와 동일한 이름의 클래스
  • 독립 객체(standalone object) : 동반 클래스가 없는 싱글톤 객체
  • 클래스와 동반 객체는 서로의 비공개 멤버에 접근할 수 있다.

스칼라 애플리케이션

  • 다음과 같은 main 메소드를 가진 독립 싱글톤 객체가 애플리케이션의 시작점
    • 인자 : Array[String]
    • 반환값 : Unit
object Summer {
    def main(args: Array[String]) {
        // ...
    }
}
  • 클래스 이름과 파일이름을 동일하게 만드는 것이 강제 사항은 아님.
  • 컴파일 : scalac, fsc

Application 트레이트

object FallWinterSpringSummer extends Application {
    // main 메소드 내용
}
  • scala.Application -> scala.App


+ Recent posts