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

15장

케이스 클래스

  • case라는 수식자가 붙은 클래스
abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String, left: Expr, right: Expr) extends Expr
  • 기능이 추가됨
    • 클래스 이름과 동일한 팩토리 메소드 추가
    • 클래스 파라미터 목록에 있는 모든 클래스 파라미터에 암시적으로 val 접두어 추가
    • toString, hashCode, equals 메소드의 자연스러운 구현 추가 (전체 구조를 비교함)
    • copy 메소드 추가 : 클래스에서 일부를 변경한 복사본 생성
// 팩토리 메소드
val v = Var("x")
val op = BinOp("+", Number(1), v)   // 중첩해서 객체 생성

// 파라미터가 클래스의 필드가 됨 (val)
v.name
op.left

// 전체를 구조적으로 서로 비교함
op.right == Var("x")    // true

// copy 메소드
op.copy(operator = "-") // BinOp(-,Number(1.0),Var(x))

패턴 매치

  • match 표현식 : 자바의 switch와 비슷. 셀렉터 match { 대안들 }
def simplifyTop(expr: Expr): Expr = expr match {
    case UnOp("-", UnOp("-", e)) => e   // 부호를 두번 반전
    case BinOp("+", e, Number(0)) => e  // 0을 더함
    case BinOp("*", e, Number(1)) => e  // 1을 곱합
}
  • Java switch VS Scala match
    • match는 표현식
    • 대안(case)은 다음으로 넘어가지 않는다
    • 매치 실패시 MatchError 발생

패턴의 종류

와일드카드 패턴

  • 모든 객체 매치
  • 매치한 값을 사용하지 않음
expr match {
  case BinOp(_, _, _) => println(expr +" is a binary operation")
  case _ => println("It's something else")
}

상수 패턴

  • 자신과 똑같은 값. 모든 리터럴, val, 싱글톤 객체
  • == 연산자를 적용하여 매치
def describe(x: Any) = x match {
  case 5 => "five"
  case true => "truth"
  case "hello" => "hi!"
  case Nil => "the empty list"
  case _ => "something else"
}

변수 패턴

  • 모든 객체 매치
  • 화살표 오른쪽에 있는 표현식에 변수가 나타나면 해당 변수의 값은 매치된 값을 참조
expr match {
  case 0 => "zero"
  case somethingElse => "not zero: "+ somethingElse
}

변수 / 상수 패턴 구분

  • 소문자로 시작하는 간단한 이름은 변수
  • 그 외에는 상수
    • 대문자로 시작 : Pi
    • 긴 이름 사용 : this.pi
    • 역따옴표 사용

생성자 패턴

  • UnOp("-", e) 과 같은 형태
  • 케이스 클래스에 속하는지 검사하고, 인자로 전달 받은 값을 검사
  • 깊은 매치 지원 : 최상위 객체만을 매치시킬 뿐 아니라, 내포 패턴과 객체의 내용에 대해서도 매치
expr match {
  case BinOp("+", e, Number(0)) => println("a deep match")
  case _ =>
}

시퀀스 패턴

  • 배열이나 리스트 등
// 길이가 정해진 경우
expr match {
  case List(0, _, _) => println("found it")
  case _ =>
}

// 길이 관계없이 매치
expr match {
  case List(0, _*) => println("found it")
  case _ =>
}

튜플 패턴

expr match {
  case (a, b, c) => println("matched "+ a + b + c)
  case _ =>
}

타입 지정 패턴

  • 타입 검사나 변환을 간편하게 대신함
def generalSize(x: Any) = x match {
  case s: String => s.length
  case m: Map[_, _] => m.size
  case _ => -1
}
  • 타입 검사 : isInstanceOf[T]
  • 타입 변환 : asInstanceOf[T]
if (x.isInstanceOf[String]) {
  val s = x.asInstanceOf[String]
  s.length
} else ...
  • 타입 소거 : Map 매치 시에 타입 인자에 대한 매치가 불가
def isIntIntMap(x: Any) = x match {
  case m: Map[Int, Int] => true     // 모든 Map에 대해서 매칭된다
  case _ => false
}

def isStringArray(x: Any) = x match {
  case a: Array[String] => "yes"    // Array는 타입 소거되지 않기때문에 Array[String]만 매칭된다
  case _ => "no"
}

변수 바인딩

  • 패턴에 변수를 추가
  • [변수 이름] @ [패턴]
expr match {
  case UpOp("abs", e @ UnOp("abs", _)) => e
  case _ =>
}

패턴 가드

  • 패턴 변수는 한 패턴 안에 오직 한 번만 나올 수 있음
expr match {
  case BinOp("+", x, x) => BinOp("*", x, Number(2)) // Wrong
  case _ => e
}
  • 패턴 가드 : [패턴] if [불리언 표현식]
expr match {
  case BinOp("+", x, y) if x == y => BinOp("*", x, Number(2))
  case _ => e
}

패턴 겹침

코드에 있는 순서대로 패턴을 매치시키므로, 구체적인 패턴일 수록 앞에 와야하며, 모든 경우를 처리하는 패턴은 가장 뒤에 있어야 함.

봉인한 클래스(sealed class)

  • 봉인한 클래스는 클래스와 같은 파일이 아닌 다른 곳에서 서브 클래스를 만들 수 없다.
  • 패턴 매치 시에 이미 알려진 서브 클래스만 고려하면 되므로, 놓친 패턴 조합을 찾기 쉽다.(컴파일러 경고)
  • sealed
sealed abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String, left: Expr, right: Expr) extends Expr
  • @unchecked : 문맥상 MatchError 발생하지 않는 경우, 패턴을 모두 다루는지 검사를 생략
def describe(e: Expr): String = (e: @unchecked) match {
  case Number(_) => "a number"
  case Var(_) => "a variable"
}

Option 타입

  • Some(x) : x가 실제값
  • None : 값이 없음

패턴 사용

변수 정의

val myTuple = (123, "abc")
val (number, string) = myTuple

val exp = new BinOp("*", Number(5), Number(1))
val BinOp(op, left, right) = exp

case 시퀀스로 부분 함수 만들기

  • PartialFunction[]
// 함수 타입
val second: List[Int] => Int = {
  case x :: y :: _ => y
}

// 부분 함수 타입
val second: PartialFunction[List[Int], Int] = {
  case x :: y :: _ => y
}
  • isDefinedAt : 부분 함수가 어떤 값에 대해 결과 값을 정의하고 있는지 확인
second.isDefinedAt(List(5, 6, 7)) // true
second.isDefinedAt(List())        // false

for 표현식

for ((country, city) <- capitals)   // 튜플 패턴 사용
  println("The capital of "+ country +" is "+ city)

val results = List(Some("apple"), None, Some("orange))
for (Some(fruit) <- results)
  println(fruit)      // apple과 orange만 출력

예제

import Element.elem

sealed abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String, left: Expr, right: Expr) extends Expr

class ExprFormatter {
  // 연산자 우선 순위
  private val opGroups =
    Array(
      Set("|", "||"),
      Set("&", "&&"),
      Set("^"),
      Set("==", "!="),
      Set("<", "<=", ">", ">="),
      Set("+", "-"),
      Set("*", "%")
    )
  private val precedence = {
    val assocs =
      for {
        i <- until opGroups.length
        op <- opGroups(i)
      } yield op -> i
    assocs.toMap
  }

  private val unaryPrecedence = opGroups.length // 단항 연산자
  private val fractionPrecedence = -1           // 나눗셈

  private def format(e: Expr, enclPrec: Int): Element =
    e match {
      case Var(name) =>
        elem(name)
      case Number(num) =>
        def stripDot(s: String) =
          if (s endsWith ".0") s.substring(0, s.length - 2)
          else s
        elem(stripDot(num.toString))
      case UnOp(op, arg) =>
        elem(op) beside format(arg, unaryPrecedence)
      case BinOp("/", left, right) =>
        val top = format(left, fractionPrecedence)
        val bot = format(right, fractionPrecedence)
        val line = elem("-", top.width max bot.width, 1)
        val frac = top above line above bot
        if (enclPrec != fractionPrecedence) frac
        else elem(" ") beside frac beside elem(" ")
      case BinOp(op, left, right) =>
        val opPrec = precedence(op)
        val l = format(left, opPrec)
        val r = format(right, opPrec + 1)
        val oper = l beside elem(" "+ op + " ") beside r
        if (enclPrec <= opPrec) oper
        else elem("(") beside oper beside elem(")")
    }

    def format(e: Expr): Element = format(e, 0)
}

object Express extends Application {
  val f = new ExprFormatter
  val e1 = BinOp("*", BinOp("/", Number(1), Number(2)),
                      BinOp("+", Var("x"), Number(1)))
  val e2 = BinOp("+", BinOp("/", Var("x"), Number(2)),
                      BinOp("/", Number(1,5), Var("x")))
  var e3 = BinOp("/", e1, e2)

  def show(e: Expr) = println(f.format+ "\n\n")
  for (e <- Array(e1, e2, e3)) show
}


+ Recent posts