케이스 클래스와 스칼라-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>