케이스 클래스와 스칼라-XML의 불변성 다루기 Scala
2016.12.29 17:24 Edit
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>