케이스 클래스와 스칼라-XML의 불변성 다루기 Scala

Case Classes

보통 스칼라 커뮤니티에서는 불변성(Immutability)을 갖춘 코드를 선호한다. 변수 선언시 var 보다는 val 을 권장하며, 컬렉션 패키지의 클래스 사용시 기본으로 사용되는 컬렉션도 불변 컬렉션이고,  케이스 클래스(Case Class)의 생성자에서 생각없이 멤버 변수를 선언해도 불변 변수가 된다.

scala> val seq = Seq(1,2,3)
seq: Seq[Int] = List(1, 2, 3)

scala> seq(0) = 0
<console>:13: error: value update is not a member of Seq[Int]
       seq(0) = 0
       ^

scala> case class Person(name:String, id:String)
defined class Person

scala> val person = Person("Kim", "mmx900")
person: Person = Person(Kim,mmx900)

scala> person.name = "Lee"
<console>:14: error: reassignment to val
       person.name = "Lee"
                   ^

이런 유형들은 포함된 요소 혹은 멤버의 값을 변경할 수 없으므로 필요시 새로운 인스턴스를 만드는 식으로 대응한다.

scala> val (_::xs) = seq
xs: List[Int] = List(2, 3)

scala> val newSeq = Seq(0) ++ xs
newSeq: Seq[Int] = List(0, 2, 3)

scala> val Person(_, id) = person
id: String = mmx900

scala> val newPerson = Person("Lee", id)
newPerson: Person = Person(Lee,mmx900)

이런 과정은 퍽 번거롭기 때문에, 보통은 축약을 위한 도구들이 있다.

scala> val newSeq = 0 +: seq.drop(1)
newSeq: Seq[Int] = List(0, 2, 3)

scala> val newPerson = person.copy(name = "Lee")
newPerson: Person = Person(Lee,mmx900)

하지만 불변 객체가 중첩되는 경우에는 이 마저도 매우 번거로워진다.

scala> val superGroup = SuperGroup("super", Group("nested", Person("Kim", "mmx900")))
superGroup: SuperGroup = SuperGroup(super,Group(nested,Person(Kim,mmx900)))

scala> val modified = superGroup.copy(
     | group = superGroup.group.copy(
     | person = superGroup.group.person.copy(name = "Lee")
     | )
     | )
modified: SuperGroup = SuperGroup(super,Group(nested,Person(Lee,mmx900)))

이 경우 사용하는 것이 '렌즈(lens)' 라이브러리 들이다. Monocle, Quicklens 등이 있다.

// 프로젝트 생성이 귀찮은 관계로, 실행 안해본 코드임

scala> import com.softwaremill.quicklens._

scala> val modified = superGroup.modify(_.group.person.name).setTo("Lee")

Scala-xml

한동안 스칼라에 기본 포함되었던 scala-xml 역시 불변성을 갖추고 있다. HTML 코드를 하나 예로 들어보자.

<html>
    <head>
        <title>Test</title>
    </head>
    <body>
        <h3>Hello, World!</h3>
    </body>
</html>

여기서 본문 내용을 기존의 "Hello, World!"에서 "Bye"로 바꾼다면, 다음과 같은 자바스크립트 코드를 상상할 것이다.

document.querySelector("h3").innerHTML = "Bye"
// 혹은
$("h3").html("Bye")

이제 scala-xml 에서 이것을 해보자. 앞서 말했다시피 자료구조는 불변이므로, 값을 변경한 새 xml을 만든다고 생각하면 된다.
일단 문서에서 h3 요소를 찾아서 값을 치환한 새 h3 요소를 반환하는 것은 간단하다.

scala> val sample = <html><head><title>Test</title></head><body><h3>Hello, World!</h3></body></html>
sample: scala.xml.Elem = <html><head><title>Test</title></head><body><h3>Hello, World!</h3></body></html>

scala> (sample \\ "h3").head match {
     | case <h3>{_}</h3> => <h3>Bye!</h3>
     | }
res19: scala.xml.Elem = <h3>Bye!</h3>

그렇지만 문서 전체에서 찾아 바꾸는 데는 재귀호출이 필요하다.
(http://stackoverflow.com/questions/970675/scala-modifying-nested-elements-in-xml 참고)

scala> def updateNodes(ns: Seq[Node]): Seq[Node] =
     |   for(subnode <- ns) yield subnode match {
     |     case <h3>{_}</h3> => <h3>Bye!</h3>
     |     case elem: Elem => elem.copy(child = updateNodes(elem.child))
     |     case other => other  // preserve text
     |   }
updateNodes: (ns: Seq[scala.xml.Node])Seq[scala.xml.Node]

scala> updateNodes(sample.theSeq)(0)
res23: scala.xml.Node = <html><head><title>Test</title></head><body><h3>Bye!</h3></body></html>

scala.xml.Elem은 케이스 클래스는 아니지만 추출자(extractor)와 copy 메서드를 제공하기 때문에 익숙한 방식으로 쓸 수 있다. 하지만 위 코드에서는 노드를 포함하는 상위 노드까지 신경써야 하는 불편이 있는데, scala.xml.transform 패키지를 활용하면 불편을 더는 한편 재사용성까지 높일 수 있다..

scala> import scala.xml.transform._
import scala.xml.transform._

scala> object HelloRule extends RewriteRule {
     |   override def transform(n: Node): Seq[Node] = n match {
     |     case <h3>{_}</h3> => <h3>Bye!</h3>
     |     case other => other
     |   }
     | }
defined object HelloRule

scala> object HelloTransformer extends RuleTransformer(HelloRule)
defined object HelloTransformer

scala> HelloTransformer(sample)
res2: scala.xml.Node = <html><head><title>Test</title></head><body><h3>Bye!</h3></body></html>
Tag :
,

Leave Comments