스칼라에서 implict 는 편하기도 하지만 코드가독성을 엄청 떨어뜨릴 수도 있기 때문에 논란이 되곤합니다.
왜 그런지 한번 살펴 볼까요?

0. 암시규칙 

 x + y 라는 표현식에서 타입 오류가 있다면 컴파일러는 convert(x) + y 를 시도 해봅니다. 
여기서 자동으로 가져다 사용되는 convert 는 무엇일까요? 

 convert 는 암시적으로 적용되는 변환을 목적으로 자동적으로 사용됩니다.  convert 가 아래의 규칙들을 갖는다면 말이지요.

 

1. 표시규칙 : implicit 로 표시한 정의만 검토 대상이 된다.

즉 implicit def intToString(x : Int) = x.toString 과 같이 implict 를 붙여주면 컴파일러가 암시적 변환에 사용할 후보에 넣는다. 변수,함수,객체정의에 implict 표시를 달 수 있다. 

2. 스코프 규칙 : 삽입할 implict 변환은 스코프 내에 단일 식별자로만 존재하거나, 변환의 결과나 원래 타입과 연관이 있어야 한다.

즉 someVariable.convert 같이는 안되며, 외부에서 가져올 경우 import Preamble._ 를 이용해서 단일 식별자로 가리킬 수 있게 한다. 그리고 원타입과 변환 결과 타입의 동반 객체에 있는 암시적 정의도 가능하다. 

Dollar 에서 Euro 로 변환 하고자 할때, 
object Dollar {

implict def dollarToEuro (x : Dollar) : Euro = ....

}

class Dollar { ... } 

이렇게 하면된다. implict 변환은 프로그램  전체에 영향을 미치지 않는다는 것을 기억하라. 그렇다면 가독성에 크게 문제가 생길 것이다.

3. 한번에 하나만 규칙 : 오직 하나의 암시적 선언만 사용한다.
convert1(convert2 (x) ) + y  이렇게 안됨.

4. 명시성 우선 규칙: 코드가 그 상태 그대로 타입 검사를 통과 한다면  암시를 통한 변환을 시도치 않음
 
 

1. 암시적 변환 (implicit conversion) 

개념 

버튼이라는 클래스를 가지고  생각해 보죠.

버튼이라는 클래스는 이미 라이브러리로 제공되고  있는 상황 (우리가 임의로 못고치는) 에서 버튼이 

눌려졌을때 어떤 행동이 일어날지는 개발자가 정해서 버튼에게 넘겨주게 됩니다. 

일단 우리는 왼쪽 버튼이 눌려지면 "삐약" 이라는 소리를 내게 하고 싶다고 해보죠.

 

val button = new OkkyButton 

버튼 클래스는  버튼이 눌려졌을때 어떤 행동을 할 지에 대해서  제공 받아야 합니다.

 

button.addLButtonDownAction( ...어떤 행동... ) 

보는바와 같이 버튼은 메소드를 제공하고 우리는 파라미터에   어떤 "행동" 을 제공합니다.

선이 그려질 수 도 있고, 파일이 저장될 수 도 있고 등등

 

이때 행동을 버튼 클래스에 추가 시키는 방법은 아래와 같을 수 있습니다.

button.addLButtonDownAction(

new ActionListener {

def actionPerformed ( event : ActionEvent ) {

println (" 삐약 ")

}

}

}

 "삐약" 이라는 행동을 넣었습니다.  완성!!

근데 좀 껄쩍지근한게  달랑 "삐약" 을 넣고 싶었는데 너무 많은 코드가 사용 되었네요. 

(이런 코드는 행사코드/얼개코드 등으로 불립니다. 해당 언어의 문법상 어쩔 수 없이 구현해야하는 코드지요.)

ActionListener  클래스도 사용되었으며 actionPerformed   메소드도 사용되었습니다.

그냥  println (" 삐약 ")  만 사용하면 안될까요?  왜 복잡한 코드를  나중에 또 봐야하는가요? 

(복잡함이라는게 코드가 길다고 복잡한건 아니긴 합니다. 즉 좀 길더라도 왜 그렇게 해야하는지에 대한 납득의 과정이 빠르면 문제되지 않으며, 코드가 짧더라도 그 납득의 과정이 불분명하면 오히려 더 코드가독성이 어려워 지곤 합니다. 이것이 바로 스칼라 언어의 딜레마 같습니다. 즉 웬만큼 익숙해 지기 전까지는 자바보다 분명 짧지만 코드가독성이 나아지지 않는? ) 

 

뭐시~ 중한디??

 

버튼의 addLButtonDownAction 메소드는 특정 인터페이스를 상속한 클래스를 파라미터로 강제하고 

있는거 같구요.. 그래서 어쩔 수 없이  저렇게 inner 클래스를 파라미터로 던져 주었습니다.

클래스 말고 그냥 함수를 던저 주면 안될까요? 외부 라이브러리라 바꿀 수 가 없습니다.

 

button.addLButtonDownAction { (_: ActionEvent) => printn(" 삐약 ") }

요로코롬 말이죠~   이렇게 해 보았지만  역시나 애초에~~ 

OkkyButton 버튼은  클래스를 매개변수로 받기 때문에 아쉽게도 요거는 꽝입니다. 

(여기서 java 8 스윙은 람다를 지원해서 어쩌구 저쩌구.. 입이 간질거리겠지만  잠시 잊어버리시고.. ) 

 

자 여기서 개념이 나오니깐 정신집중!!!!! 

 

저렇게 우아하게 처리해봤는데 아쉽게도 타입 불일치로 "꽝" 이 되버렸네요. 그냥 포기할까요?

우리 포기하지 말자구요 -.-V   스칼라는 암시적 변환이 있습니다.

자 다음 코드를 보세요.

implicit def functionToActionClass(f : ActionEvent => Unit ) = 

new ActionListener {

def actionPerformed (event : ActionEvent) = f (event)

}

 

implicit 로 처음에 시작하구요. 매개변수를 함수로 받아서 클래스를 리턴해 주고 있습니다.

이 코드를 사용해보죠.

button.addLButtonDownAction(

functionToActionClass( 

(_: ActionEvent) => println (" 삐약 ") 

}

이렇게 사용되었습니다. 애초에 내부 클래스 코드보다는 조금 간단해 졌습니다. !! (어떤 면으로는  더 복잡해졌습니다.) 

음.. 근데 이건 implicit 가 먼지모르겠지만 그거 상관없이 되는 거 잖아? 그렇죠.. --;; 

좀 만 기다려주세요.

이제 implicit 의 파워가 나옵니다.

 

button.addLButtonDownAction( (_: ActionEvent) => println (" 삐약 ") }

 

이렇게 functionToActionClass 를 빼도 잘 작동합니다~ 정말 너무나 간단해 졌습니다.

이게 어떻게 가능하냐면

일단 저 코드가 컴파일되면 처음엔 타입오류가 발생하긴 합니다.근데 여기서 그냥 나 못해~ 하고 끝나는게 

아니라 우리의 컴파일러군은 포기하지 안고서 "암시적 변환" 을 통해서 해결 할 수 있지 않나 살펴봅니다.

살펴보다가   implicit 로 선언된 함수 functionToActionClass  를 찾았습니다. 이걸 가지고 시도해보니 잘됩니다.

 

네 

암시적 변환이란 컴파일러가 열심히 일해서 개발자가 성가신 일들을 하지 않게 도와주는 녀석이었습니다.

이제 클래스를 넣어도 잘되고 저렇게 람다식으로 넣어도 잘 될 것입니다. 

 


2. 암시적 파라미터 (implicit parameter) 

개념

이것도 암시적 변환하고 매우 비슷합니다. 이름처럼 파라미터를 암시적으로 넣어 준다는것인데요.

즉 내가 넣지 않아도 자동으로 들어간다는 거겠죠? 

 

예를들어 

object Greeter {

def greet (name : String ) (implicit prompt : MyPrompt) {

println (name)

println (prompt.preference)

}

}

보시는 바와 같이 greet 는 두개의 파라미터를 갖습니다.  뒤에 것이 암시적 파라미터 인데요.

 

이렇게 호출 해 보겠습니다.

val prompt = new MyPrompt("ubuntu> ")

Greeter.greet("삐약") (prompt)

이건 직접적으로 prompt 객체를 만들어서 넣어주고 있습니다. 네 이건 명시적입니다. 당연히 작동하겠죠.  

 

그럼 암시적은 무엇인가??

 

Greeter.greet("삐약")   

이렇게 해도 잘 된다는 말인데요. 즉  뒤에 파라미터를 명시적으로 안 넣어 줘도 잘 된다는 거죠. 

이게 어떻게 되는거냐면요.
암시적으로 들어갈 파라미터는 다음과 같이  어딘가에 미리 만들어 놓습니다.

object HamaPrefs{

implicit val prompt = new MyPrompt (" ubuntu> ")

}


저게  있어야  컴파일러는 이 변수를 빠진 파라미터 목록에서 찾아서 적용해 주거든요.

정리하면

- 미리 변수 정의   : implicit val b = new B;
- 함수를 인자 없이 호출 :   test (a)
- 호출 당하는 함수쪽에 인자를 implicit 로 설정  : 
def  test(a :A , implicit b : B)  { }

입니다.  이렇게 DI 를 해놓으니깐 유연성이 커질거 같네요. 그쵸? 

 

마지막으로 implicit 키워드는 개별 파라미터가 아니라 전체 파라미터 목록을  범위로 합니다.

다음을 보시죠.

class MyTest1(val  tell : String)

class MyTest2(val tell : String)

 

object Greeter {

def greet (name : String) ( implicit  test  : MyTest1 ,  test2 : MyTest2 ) {

println(name)

println(test.tell)

println(test2.tell)  

    }

}

object HamaPrefs {

implicit val test = new MyTest1 ("삐약")

implicit val test2 = new MyTest2 ("개굴")

}

 

위 코드에서 test 와 test2 파라미터 모두 implicit 에 영향 받는다는 겁니다.  Greeter.greet ("HAMA")  이렇게 단일 파라미터를 던져도 나머지 암시적 파라미터를 찾아서 작동하지요. 

프레임워크등을 사용하다가 implicit 가 나오면 해당 프레임워크에서 그 인자에 대해서는 미리 만들어 뒀구나 라고 생각하면 편할거 같습니다.

 

3. implicit  와 폴리모피즘 

trait Factory[T] {
  def create: T
}

object Factory {
  implicit def stringFactory: Factory[String] = new Factory[String] {
    def create = "foo"
  }

  implicit def intFactory: Factory[Int] = new Factory[Int] {
    def create = 1
  }
}

object Main {
  def create[T](implicit factory: Factory[T]): T = factory.create
 
  // or using a context bound
 
  // def create[T : Factory]: T = implicitly[Factory[T]].create


  def main(args: Array[String]): Unit = {
    val s = create[String]
    val i = create[Int]

    println(s) // "foo"
    println(i) // 1
  }
}


4. 기존 클래스를 확장하기 위한 방법 (kotlin extension vs scala implict)

kotlin 

data class Person(val firstName: String, val lastName: String)

fun Person.greet() = "Hello, $firstName $lastName!"

Person("Matt", "Moore").greet()

Scala

case class Person(val firstName: String, val lastName: String)

implicit class ExtendedPerson(p: Person) {
  def greet = s"Hello, ${p.firstName} ${p.lastName}!"
}

Person("Matt", "Moore").greet
implicit class ExtendedInt(i: Int) {
  def squared = i * i
  def doubled = i * 2
}

2.squared
2.doubled



5. Play2 웹 프레임워크에서의 implicit  

 

웹어플리케이션을 빠르고 즐겁게 만들 수 있는 cool 한 Play2 에서 implicit 는 적극적으로 사용됩니다.

반복을 줄이기 위해서 사용되는데요 예를 보면서 알아 보시죠. 

예를들어 쇼핑몰 싸이트에서 방문자의 장바구니에 상품이 몇개가 담겨져 있는지 보여주도록 모든 페이지의 

 상단에 쇼핑 카트를 유지 하길 원한다고 해보죠.  예) Shoping Cart : 3 Items (Show Cart) 

 

 

그러려면 모든 웹페이지 마다 cart 정보를 넘겨주어야합니다. 다음 소스를 보시죠.

* Shop.scala 

object Shop extends Controller {

def catalog() = Action { implicit request =>
val products = ProductDAO.list
Ok(views.html.products.catalog(products, cart(request))

}

def cart(request: Request) = {

 

// Get Cart from session

}

 

Shop 컨트롤러의 catalog 호출을 하는 세션으로 부터 Cart 정보를 얻은후에 

catalog 뷰 템플릿의 인자로 products  리스트와 함께 넘겨주고 있습니다.

 

* catalog.scala.html

@

(products:

Seq

[Product], cart: Cart)

@

main(cart) {

<h2>

Catalog

</h2>
<ul>
@for

(product <- products) {

<li>
<h3>@

product.name

</h3>
<p class="description">@

product.description

</p>
</li>

}

</ul>

}

catalog 뷰 템플릿에서는 매개변수로 products  와 cart 를 받아서 , products 는 자신이 드로잉 하고 
cart 는 명시적으로  메인 뷰 템플릿으로 보내주네요.

 

* main.scala.html 

@(content: Html)(cart: Cart)
<!DOCTYPE html>
<html>
<head>
... 생략 ...
<span id="cartSummary" class="label label-info">
@cart.productCount match {

case 0 => {
Your shopping cart is empty.
}

case n => {
You have @n items in your shopping cart.
}
}
</span>

<div class="container">
  @content
</div>

마지막으로 메인뷰 템플릿에서는 넘어온  catalog 뷰 템플릿과 cart 를 이용하여  페이지를 완성합니다.
cart 에 아무것도 없을 경우와 있을 경우를 case 로 분리해서 처리 하고 있군요.

 

 

위에 보다시피 이런 페이지가 많으면 많을 수록 일일이 객체를 넘겨줘야하는데요.

implicit 를 활용하면 그러지 않아도 됩니다. implicit  가 적용된 다음  소스를 보시죠.

 

* Shop.scala 

object Shop extends Controller with WithCart {

  def catalog() = Action { implicit request =>
    val products = ProductDAO.list
    Ok(views.html.products.catalog(products))
  }

}

cart 를 넘겨주는 코드가 사라 졌습니다. 대신 with WithCart 가 생겼군요. WithCart 는 제일 마지막에 살펴보겠습니다.

 

* catalog.scala.html

@(products: Seq[Product])(implicit cart: Cart)

@main() {

<h2>Catalog</h2>
<ul>
@for(product <- products) {

<li>
<h3>@product.name</h3>
<p class="description">@product.description</p>
</li>
}

</ul>

}

cart 매개변수 앞에 implicit 가 생겼으며 , 역시 main 뷰 템플릿으로 cart 를 명시적으로 넘겨주지 않습니다.

 

* main.scala.html 

@(content: Html)(implicit cart: Cart)
<!DOCTYPE html>
<html>
... 생략 ...


<span id="cartSummary" class="label label-info">
@cart.productCount match {

case 0 => {
Your shopping cart is empty.
}

case n => {
You have @n items in your shopping cart.
}
}
</span>

<div class="container">
@content

</div>

역시 cart 매개변수 앞에 implicit 가 생겼습니다. 

이렇게 cart 를 모든 페이지에서 보여주기 원할때 컨트롤러에서 cart 를 매번 세션에서 직접가져오는 반복이 사라졌습니다.

 

그럼 cart 는 어디로 사라졌을까요? 매직인가요?  아닙니다. Play2 와 스칼라는 이해하기 불가능한 마술같은
일이 거의 없습니다.
개발자 친화적이라고 할까요?

 

trait WithCart {

  implicit def cart(implicit request: RequestHeader) = {
    // Get a fake cart. In a real app, you'd get it from the session here.
    Cart.demoCart()
  }

}

 이런 trait 가 만들어져 있습니다. trait 는  자바 인터페이스 + 추상클래스와 비슷하다고 일단 생각하시면 되는데요. 

 위에 Shop.scala 에서 이것을 믹싱하고 있습니다.

object Shop extends Controller with WithCart {

결국 WitdCart 만 믹싱해주고  매개변수들을 implicit 로 선언해주기만하면 컴파일러가 알아서 적용시켜 주는 것이죠.

 

웹개발경험+Play2+Scala에 대한 기초지식이 없다면 어려울 수 도 있을거 같네요. :-| 

trait


*  Scala 는 interface 가 없으며 대신  trait 을 사용한다. 

*  Scala 의 trait 는 자바의 interface 와 달리 구현 가능하다. (자바8 부터는 자바인터페이스도 디폴트메소드등 구현)

*  하나의 부모클래스를 갖는 클래스의 상속과 달리 트레이트는 몇개라도 조합해 사용 가능하다. 

 * Scala 의 trait 는 자바의  인터페이스와 추상클래스의 장점을 섞었다. 

 * 트레이트를 믹스인 할때는 extends 키워드를 이용한다. 

 * extends 를 사용하면 trait 의 슈퍼클래스를 암시적으로 상속하고 , 본래 trait 를 믹스인한다. 

*  trait 는 어떤 슈퍼클래스를 명시적으로 상속한 클래스에 혼합할 수 있다. 

   그때 슈퍼클래스는 extends 를 사용하고, trait 는 with 로 믹스인한다. 


예1) 


trait Car {
  val brand: String
}

trait Shiny {
  val shineRefraction: Int
}
class BMW extends Car {
  val brand = "BMW"
}

클래스는 여러 트레잇를 with 키워드를 사용해 확장할 수 있다.

class BMW extends Car with Shiny {
  val brand = "BMW"
  val shineRefraction = 12
}


예2)



 trait Philosophical {
      def philosophize() {
        println("I consume memory, therefore I am!")
      }
    }

Listing 12.1 - The definition of trait Philosophical.


 class Frog extends Philosophical {

    override def toString = "green"
  }

 Listing 12.2 - Mixing in a trait using extends.


 scala> val frog = new Frog
  frog: Frog = green
  
 scala> frog.philosophize()
  I consume memory, therefore I am!

 scala> val phil: Philosophical = frog  
  phil: Philosophical = green
  
 scala> phil.philosophize()
  I consume memory, therefore I am!

 class Animal
  
 class Frog extends Animal with Philosophical {
   override def toString = "green"
 }

Listing 12.3 - Mixing in a trait using with.


class Animal
trait HasLegs
  
class Frog extends Animal with Philosophical with HasLegs {
  override def toString = "green"
}

Listing 12.4 - Mixing in multiple traits.

class Animal
  
class Frog extends Animal with Philosophical {
  override def toString = "green"
  override def philosophize() {
    println("It ain't easy being "+ toString +"!")
  }
 }

 scala> val phrog: Philosophical = new Frog
  phrog: Philosophical = green
  
 scala> phrog.philosophize()
  It ain't easy being green!

trait 는 클래스를 정의하면서 할 수 있는 모든것을 할 수 있다. 
문법 경우 2가지를 제외하고 정확히 같다.

첫째. 트레이트는 '클래스' 파라미터를 가질 수 없다.
둘째 클래스는 super 호출을 정적으로 바인딩하지만, 트레이는 동적으로 바인딩한다.

class Point(x: Int, y: Int)
trait NoPoint(x: Int, y: Int) // trait 는 클래스 파라미터를 가질수 없다. 


trait   vs  추상클래스 


스택 오버플로우에서 아래를 참고 하라. 

트레잇과 추상 클래스의 비교
추상 클래스와 트레잇의 차이
스칼라 프로그래밍: 트레잇냐 아니냐 그것이 문제로다?  

자바8에서 인터페이스 기능이 대폭 증가한것을 기점으로  스칼라  trait vs 자바 interface 비교를 해야 할 듯싶다.


Orderd trait 


  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)
  }


  class Rational(n: Int, d: Intextends Ordered[Rational] {
    // ...
    def compare(that: Rational) =
      (this.numer * that.denom) - (that.numer * this.denom)
  }


  scala> val half = new Rational(12)
  half: Rational = 1/2
  
scala> val third = new Rational(13) third: Rational = 1/3
scala> half < third res5: Boolean = false
scala> half > third res6: Boolean = true


  trait Ordered[T] {
    def compare(that: T): Int
  
  def <(that: T): Boolean = (this compare that) < 0   def >(that: T): Boolean = (this compare that) > 0   def <=(that: T): Boolean = (this compare that) <= 0   def >=(that: T): Boolean = (this compare that) >= 0 }


case class


case 클래스 예제 

abstract class Expr

case class Var(Name: String) extends Expr

case class Number(num: Double) extends Expr 

case class BinOp(operator: String, left:Expr , right:Expr) extends Expr


* 스칼라에서 클래스 본문이 비어 있으면 중괄호를 생략할 수 있다. 


case 클래스 특징 


* 컴파일러는  클래스 이름과 같은 이름의 팩토리 메소드를 추가한다.

new Number(3.0) 대신해서 Number(3.0) 가능하다.

* 케이스 클래스의 파라미터의 목록을 val 접두사를 붙인다. 

val n = Number(3.0) 

n.num // 3.0 

* toString, hashCode,equals 메소드들의 자동으로 추가된다. 

val v = Var("x") 

val op = BindOp("+", Number(1), v) 

op.right == Var("x") // true 

* 컴파일러는 케이스 클래스에서 일부를 변경한 복사본을 생성하는 copy 메소드를 추가한다. 

    operator 만 바꾸고 op 같은 연산자를 만드는 법 

    op.copy(operator = "-")   // BindOp = BindOp(-, Number(1.0), Var(x)) 


패턴 매치

    * switch { case } 가 스칼라에서는 셀렉터 match { case }  와 같다.

    * 스칼라의 match 는 표현식이다.

    * 스칼라의 case 는 다음 case 로 fall through 하지 않는다.

    * 매치에 성공하지 않는 경우 MatchError 예외가 발생한다. 따라서 디폴트 케이스를 반드시 추가해야한다. 


    expr match {

case BinOp ( op, left, right) =>

println( ... ) 

case _ =>

    }


와일드 카드 패턴  
  expr match {
    case BinOp(op, left, right) =>
      println(expr +"is a binary operation")
    case _ =>
  }
 expr match {
    case BinOp(_, _, _) => println(expr +"is a binary operation")
    case _ => println("It's something else")
  }

상수  패턴  

 def describe(x: Any) = x match {
      case 5 => "five"
      case true => "truth"
      case "hello" => "hi!"
      case Nil => "the empty list"
      case _ => "something else"
    }
 scala> describe(5)
  res5: java.lang.String = five
  
scala> describe(true) res6: java.lang.String = truth
scala> describe("hello") res7: java.lang.String = hi!
scala> describe(Nil) res8: java.lang.String = the empty list
scala> describe(List(1,2,3)) res9: java.lang.String = something else

변수 패턴 

expr match {
      case 0 => "zero"
      case somethingElse => "not zero: "+ somethingElse
    }
 scala> import Math.{E, Pi}
  import Math.{E, Pi}
  
scala> E match {          case Pi => "strange math? Pi = "Pi          case _ => "OK"        } res10: java.lang.String = OK
 scala> val pi = Math.Pi
  pi: Double = 3.141592653589793
  
scala> E match {          case pi => "strange math? Pi = "+ pi        } res11: java.lang.String = strange math? Pi = 2.7182818...
 scala> E match {
           case pi => "strange math? Pi = "+ pi
           case _ => "OK"  
         }
  <console>:9: error: unreachable code
           case _ => "OK"  
                     ^
 scala> E match {
           case `pi` => "strange math? Pi = "+ pi
           case _ => "OK"
         }
  res13: java.lang.String = OK

생성자 패턴 

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 _ =>
    }

튜플 패턴 

def tupleDemo(expr: Any) =
      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
    }
Listing 15.11 - A pattern match with typed patterns.

Here are a few examples of using the generalSize method in the interpreter:

  scala> generalSize("abc")
  res14: Int = 3
  
scala> generalSize(Map(1 -> 'a'2 -> 'b')) res15: Int = 2
scala> generalSize(Math.Pi) res16: Int = -1

Sealed class

  sealed abstract class Expr
    case class Var(name: Stringextends Expr
    case class Number(num: Doubleextends Expr
    case class UnOp(operator: String, arg: Exprextends Expr
    case class BinOp(operator: String, 
        left: Expr, right: Exprextends Expr
Listing 15.16 - A sealed hierarchy of case classes.

Now define a pattern match where some of the possible cases are left out:

  def describe(e: Expr): String = e match {
    case Number(_) => "a number"
    case Var(_)    => "a variable"
  }

You will get a compiler warning like the following:

  warning: match is not exhaustive!
  missing combination           UnOp
  missing combination          BinOp




Scala 에서 Enumeration 사용하기

http://alvinalexander.com/scala/how-to-use-scala-enums-enumeration-examples 참조


문제 )  스칼라에서 enumeration 을 사용하고 싶습니다.(상수로서 사용되는 문자열 ) 

해결책 1 )  Enumeration 클래스를 사용해서 열거형 만들기 

scala.Enumeration 클래스를 확장하세요.

package com.acme.app {

    object Margin extends Enumeration {
        type Margin = Value
        val TOP, BOTTOM, LEFT, RIGHT = Value
    }

}

그리고 이것을 import 로 가져가서 사용하심 됩니다

object Main extends App {

    import com.acme.app.Margin._

    // use an enumeration value in a test
    var currentMargin = TOP
    
    // later in the code ...
    if (currentMargin == TOP) println("working on Top")

    // print all the enumeration values
    import com.acme.app.Margin
    Margin.values foreach println

}

해결책 2 )  traits / case 를 사용해서 열거형 만들기  

// a "heavier" approach
package com.acme.app {
    trait Margin
    case object TOP extends Margin
    case object RIGHT extends Margin
    case object BOTTOM extends Margin
    case object LEFT extends Margin
}

Enumeration  의 문제점 - 1


메소드 오버로딩에 문제가 생긴다. 아래를 보자.
/**
* Created by brad on 2016-09-30.
*/

import scala.collection._
import scala.util.parsing.json._


object Colours extends Enumeration {
val Red, Amber, Green = Value
}

object WeekDays extends Enumeration {
val Mon,Tue,Wed,Thu,Fri = Value
}

object Functions {
def f(x: Colours.Value) = println("That's a colour")
def f(x: WeekDays.Value) = println("That's a weekday")
}


object test {

import Colours._

def main(arg : Array[String]): Unit ={
Functions.f(Red)
}

}

Error:(19, 7) double definition:

method f:(x: WeekDays.Value)Unit and

method f:(x: Colours.Value)Unit at line 18

have same type after erasure: (x: Enumeration#Value)Unit

  def f(x: WeekDays.Value) = println("That's a weekday")

이 경우에 에러가 발생한다.  Red 라고 명시했지만 구분 할 수 없다.



Getter 와 Setter 


객체지향 프로그래밍에서 게터와 세터는 의도치 않게 이제 기본이 된  내용들 중 하나이지만 ( getter / setter 자체를 그냥 public 으로 변수 선언하는것과 마찬가지로 나쁘게 보는 시각도 있습니다. 객체지향은 외부 노출을 줄여야 한다고 보는데 게터,세터는 절차지향 마인드의 산물 ) 때로는 쓰기 귀찮아질 때 도 있긴합니다. 대부분의 게터와 세터는 매우 비슷하기 때문에 같은 기능을 하는 더 나은 방법이 있을거란 생각은 매우 타당 할 것이며 C# 에서는 그것을 위해 특별히 "프로퍼티"라는 것을 만들어서  아래와 같이 사용됩니다.

    private String strName;
 
    public String StrName
    {
        get { return this.strName; }
        set 
        {
               ... 제어 .. 
              this.strName = value; 
        }
    }

자바에서는 언어차원에서 편하게 쓰게 하기위한 장치는 없고(Java 1.7 에 @property  같은거 만들거란 말은 있었는데 안한듯?)  IDE 차원이나 Lombok 같은 툴의 도움을 받습니다. 

관련 읽을 거리:

왜 자바에서 getter / setter 는 악마인가  (영어)

* 왜 getter / setter 를 사용하는가  (영어)

* 정보은닉과 캡슐화의 차이   (한글)

* 홀럽 아저씨의 실용주의 디자인패턴 책에  왜 getter/setter 가 안 좋은지 나온다. ( 위 블로그의 한글버전)   

 게터,세터는 객체지향과 거리가 있으며  데이터의 흐름이 적을 수록 유지보수에 좋다라고 말 하고 있습니다. 즉 pull  해서 쓰지 말고  push  해서 쓰라는 지침. 즉 가져와서 하지 말고 맡겨라!  데이터베이스와 접점 부분등 몇몇 예외상황에 대해서도 말 합니다. 


스칼라에서의  Getter / Setter 

다음 첫번째 예를 보면 :  

class Person() {  
 var name = "" 
 var age = 0 
} 

첫눈에 보기에 이 클래스가 단지 사람 객체를 만들고 그 이상 아무것도 없어 보이겠지만 
사실 인스턴스화 되면 다음과 같이 get / set 을 할 수 있게 됩니다.

GET 

//person 객체 만들고 
person = new Person() 

// age 과 name 속성을 출력한다. 
println(person.age)  
println(person.name)  

SET

// Set the properties to different values 
person.age = 34  
person.name = "Dustin Martin"  

매우 간단한데  이 예를 보면 이렇게 생각 하실 듯 합니다.  이것은  getters 와 setters 가 아니잖아! 


자동으로 생성되는 Getter / Setter 

스칼라에서는 어떤 객체의 멤버 중 비공개가 아닌 모든 var 멤버에 게터와 세터 메소드를 자동으로 정의해 줍니다. 이때 자동으로 정의해 주는 이름은 자바의 관례와 다르며 age 의 게터는 그냥 age 이고 세터는 age_= 입니다. 이때 필드에는 자동적으로 private[this] 가 붙는데 그 필드를 포함하는 객체에서만 접근할 수 있다는 의미이고 자동으로 만들어진 게터와 세터는 원래의 age 와 같은 가시성이됩니다. 즉 age 가 공개이기 때문에 공개가 된다는 뜻 


 자신만의 Getter / Setter  정의 하기 - (1)

여기서 만약 age 속성에 0살~200살까지의 범위를 확인하는 절차가 필요할때 어떤 일이 발생 할까?
name 에 특정한 방식으로 포맷을 정하고 싶을때 어떻게 할까?  

스칼라에서 다루는 방법을 알아보겠습니다.

class Person() {  
 // age 속성을 private 로 바꾸고 , _age 으로 이름을 바꾸자.  
 private var _age = 0 
 var name = "" 

 // Getter 
 def age = _age 

 // Setter 
 def age_= (value:Int):Unit = _age = value 
} 

매우 간단한 코드인데. 첫번째로 "age" 를 "_age" 로 이름을 바꾸고 private 를 적용하였습니다. 

GET 

def age = _age  

변수를 리턴하는 age 메소드를 정의하였습니다.  스칼라는 return 키워드를 요구하지 않습니다. 

또한 Scala 는 메소드의 몸체에 괄호를 사용하는것이 의무가 아닙니다.

SET

다음엔 setter 를 살펴보죠.

def age_= (value:Int):Unit = _age = value  

-  메소드 이름은  "age_="  입니다.
- :Unit 는 void 를 리턴하다는 것과 동등하며 생략가능합니다.  
- priavate 변수인 _age 에 새 값인 value 를 대입한다는 뜻입니다.

아직까지 먼가 제한을 두는 코드가 들어 있지는 않았습니다. 이제 제한을 넣어 보죠. 


 자신만의 Getter / Setter  정의 하기 - (2)

class Time {

 private [this] var h = 12

 private [this] var m = 0

 def hour : Int = h  // getter

 def hour_= (x : Int) { //setter

   require (0 <= x && x < 24) 

   h = x

 }


 def minute = m  // getter

 def minute_= (x: Int) {  // setter 

   require(0 <= x && x < 60)

   m=x

 }
}
 이제 Set 을 할 때 값을 점검합니다. 여기서는 시간과 분이 특정 값 내에 있어야한다고 제한하고 있네요.

 

자신만의 Getter / Setter  정의 하기 - (3)

class Termometer {

   var celsius : Float = _   // celsius 의 getter/setter 는 디폴트구현을 사용

   def fahrenheit = celsius * 9 / 5 + 32  //fahrenheit 는 메소드만 존재해도 됨

   def fahrenheit_= (f : Float) {    

     celsius = ( f - 32 ) * 5 / 9

   }

}
--------------------------------------------

var t  = new Termometer

t.celsius = 100

t.fahrenheit = -40

 fahrenheit 같이 변수를 명시적으로 선언하지 않고도 getter / setter 로 사용할 수 있습니다.

celsius 를 var 로 정의하며 초기화 값으로 _ 를 주었는데 이것은 디폴트값으로 영을 할당합니다.
영은 숫자에서는 0 이고 불리언에서는 false 이며 레퍼런스에서는 null 입니다자바에서 초기화 하지 않았을때 할당되는 동일한 규칙 이며 스칼라에서는 초기화를 생략할 수 없습니다. 만약 초기화 하지 않는다면 그건 추상변수를 선언해버립니다.  추상변수는 다음에 알아보시죠.

Getter / Setter  애노테이션

위에 보다시피 보통 스칼라 코드에서는 명시적인 필드 get,set 메소드가 필요 없습니다. 그냥 디폴트 구현을 사용하다가 제한이 필요하거나 할때 재정의해서 사용할 수 가 있게 되죠. 그때 모든 코드들을 찾아다니며 변경할 필요가 없게 됩니다.  하지만 플랫폼에 따라 몇몇 프레임워크는 get,set 메소드를 꼭 필요로 하는 경우가 있기때문에 이를 위해 스칼라는 @scala.reflect.BeanProperty 애노테이션을 제공합니다.

필드에 이 애노데이션을 추가하면, 컴파일러가 자동으로 get,set 메소드를 만들어 줍니다.  예를 들어 crazy 필드를 애노테이션하면, 자바처럼  get메소드는 getCrazy가, set 메소드는 setCrazy 가 됩니다.



함수와 메소드

이번 포스트 내용은 지금까지 그리고 앞으로 나올 강좌중에서 가장 중요한 포스트라고 생각합니다.   
스칼라에서 함수/메소드는 그 만큼 중요합니다.  본 포스트의 내용이 불분명하거나 모자르면 반드시 
다른 곳에서 보충하시길 바랍니다.

 

스칼라에서 함수종류 

* 객체의 멤버로 있는 함수인 메소드
* 함수안에 정의한 내포 함수
* 함수 리터럴
* 함수 값 
 

메소드  

 

특성 

스칼라 메소드 파라미터에서 중요한 점은 이들이 val 이라는것이다.

  

  def add(b : Byte) : Unit = {

b = 1  // 파라미터가 val 이라 불가능 !

sum += b

   }

 

 * return 을 명시적으로 사용하지 말라. 

   스칼라는 메소드가 한 값을 계산하는 표현식인 것 처럼 생각하는걸 권장한다.

 * 메소드가 오직 하나의 표현식만 계산하는 경우 {} 중괄호를 없앨 수 있다.

   def add(b:Byte) : Unit = sum += b

 

 * 메소드의 결과 타입이 Unit 인 경우는 부수 효과를 위해 사용한다는 뜻이다. 

 

 * Unit 생략하고 = 를 없애서 {} 로 메소드를 나타낼 수 있는데, 이건 프로시저 처럼 보이게 하는데 
   프로시저란 오직 부수 효과를 얻기 위해서만 사용하는 메소드를 의미한다. 

 

   즉 def add(b: Byte) { sum += b}  처럼 사용한다는 뜻이데, 여기서 함정이 있는게 

 

   def add2 () { " test string " }  이건 Unit 를 리턴하며 
   def add3 () = { " test string " }  이건 String 을 리턴한다는 것이다. 

 

   즉 프로시저 처럼 작성하면 리턴이 무조건 Unit 이 된다는점~ 

 

  * 표현식 블럭 즉 {} 를 이용해서 호출도 가능하다. 블럭에서 실행된 값이 매개변수로 들어감
    (스칼라에서 이 문법 익숙해지기 전까지 매우 헤깔림) 

 

def formatEuro (amt : Double) = f"$amt%.2f" 

formatEuro { val rate = 1.32; 0.235 + 0.7123 + rate * 5.32 }   <-- 이런식으로 '호출'

 

예제 1

 

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) 
        } 
 }

 

두개의 메소드로 이루어져 있다.

 


지역 함수

 

특성 

함수안에 함수를 정의 할 수 있다.

 

예제 2 (예제 1 의 변형 ) 

 

import scala.io.Source  
object LongLines {  
	def processFile(filename : String, width : Int ) {     
    	def processLine(filename: String, width: Int, line: String)  {       
        	if (line.length > width)       
            	println(filename + ": " + line.trim)     
             }      
             val source = Source.fromFile(filename)     
             for (line <- source.getLines())       
             	processLine(filename, width, line)  
      }  
}

 

도우미 함수들은 프로그램의 네임스페이스를 오염 시킬 수 있기 때문에  자바에서는 주로 비공개 메소드를 이용해 이런 목적을 달성하지만 스칼라에서는 함수안에 함수를 정의해서 처리 하기도 한다. 

함수안의 함수는 그 외부 함수안에서만 접근 할 수 있다. 

 

예제 3 (예제 2 의 개선 ) 

 

import scala.io.Source  
object LongLines {    
	def processFile(filename : String, width : Int ) {     
    	def processLine(line: String)  {      
        	if (line.length > width)       
            	println(filename + ": " + line.trim)    
         }     
         val source = Source.fromFile(filename)    
         for (line <- source.getLines())      
         	processLine(filename, width, line)   
     }  
}

 

 

내부의 함수에서 바깥 함수의 인자를 사용한다.

 


1급 계층 함수

 

특성 

스칼라는 1급 함수 (first class function) 를 제공한다. 1급이란 말처럼 상위계층 즉 모든게 가능한.. 할당도 가능하고 , 인자로도 넘길 수 있고, 반환도 가능한 애들 말한다. 

 

이들은 이름 없이 리터럴로 표기해 값 처럼 주고 받을 수 있다.

함수 리터럴은 클래스로 컴파일하는데, 해당 클래스를 실행 시점에 인스턴스화하면 함수 값이 된다.  

모든 함수 값은 FunctionN 트레이트 중 하나를 확장한 클래스로부터 만든 인스턴스이다.

예를들어 Function() 은 인자가 없는 함수고, Function1 은 인자가 1개인 식이다. 

여기에는 함수 호출시 사용하는 apply 메소드가 들어 있다.

 

다음 예와 같다.

 

 

1
2
3
4
5
trait Function1[ T1, R] extends AnyRef {
    ...
    def apply( v1 :T1) : R
    ...
}

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Last( init : Int ) extends Function1[Int,Int]{
    var last = init
 
    def apply( x :Int ) : Int = {
        val retval = last
        last = x
        retval
    }
}
 
val last : Int => Int   =   new Last( 1 )
 
val x = last( 5 )     // x == 1
val y = last( 5 )     // y == 5
val z = last( 8 )     // z == 5
 ...
val a = last( 5 )     // a == 8

 

 

함수 리터럴  (간단 예제)  

 

(x: Int) => x + 1

 

정수 x 를 x + 1 로 매핑하는 함수이다. 

함수 값은 객체이기 때문에 원하면 변수에 저장 할 수 있다.

동시에 함수 값은 엄연히 함수이기도 하다. ( 괄호를 이용해 실행 할 수 있다는 야그) 

 

var increase = (x : Int) => x + 1     //  함수를 increase 변수에 저장가능!

increase(10)  // 호출 가능 !  내부적으론 apply 메소드가 사용됨. 

 

 

함수 리터럴 ( 둘 이상의 문장 예제) 

 

increate = (x: Int ) => {

println("...")

println("...")

x + 1

}

 

둘 이상의 문장이 필요하면 {} 중괄호로 감싸서 블록으로 만든다. 

 

 

함수 리터럴 ( 매개변수로 사용) 

 

val numbers = List (1,2,3,4,5)

numbers .foreach((x:Int) => println(x)) 

 

다음처럼 함수의 매개변수로 들어가서 사용된다. 

이제는 대부분의 언어에서 상식이 되버린 map, filter 함수에서도 사용된다. 

 

scala> numbers.map((i: Int) => i * 2) res0: List[Int] = List(2, 4, 6, 8, 10)
scala> numbers.filter((i: Int) => i % 2 == 0) res0: List[Int] = List(2, 4)

 

간단한 형태의 함수 리터럴 

scala> numbers.filter( i => i % 2 == 0) res0: List[Int] = List(2, 4)

 

애초에 컬렉션에서 i 는 정수라는 하실을 알기 때문에 굳이 Int 라고 하지 않아도 된다.

타깃 타이핑이라고 하며 () 도 생략 가능하다. 

 

더 간단한 형태의 함수 리터럴 

scala> numbers.filter( _ > 4) res0: List[Int] = List(5)

_ 는 함수가 호출 시 전달받은 인자로 하나씩 채워져서 계산될 것이다.  

 

 


기타 특성  

반복 파라미터 

def echo (args: String*) = for (arg <-args) println(arg)    //   매개변수에 * 를 표시함으로써 

echo("hello", "world")  // 요렇게 인자의 갯수를 자유롭게 할 수 있다. 

 

이름 있는 인자 

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

speed(100,10) // 일반적 사용

speed(time = 10, distance = 100) // 이름을 함께 사용, 순서를 바꾸어서 사용해도 된다.

 

디폴트 값

def printTime(out: java.io.PrintStream = Console.out) = out.println("time = " + System.currentTimeMillis()) 

printTime() //  이렇게 호출하면 자동적으로 디폴트로 out 은 Console.out 이 된다. 

 

클로저

 ( 나중에 따로 다루겠습니다 )

 

 


def 와 val 의 차이 

def even: Int => Boolean = _ % 2 == 0
val even: Int => Boolean = _ % 2 == 0

even(10) // 둘다 이렇게 호출 가능

 

위에서 배운 바와 같이 함수는 이렇게 표현 가능한데요. 둘의 차이는 멀까요? 

 

메소드 def even 은 호출 되었을때 평가되며 새로운 Function1 객체를 매 호출시마다 생성합니다.

반면 val 은 정의 되었을때 평가됩니다. 

val test: () => Int = {  // 이미 평가됨  (리턴값이 항상 같을 경우 사용 할 수 있다)    
	val r = util.Random.nextInt   () => r 
}  
test() // Int = -1049057402 
test() // Int = -1049057402 
- 동일한 값   

def test: () => Int = {   
	// 호출할때마다 평가가 달라짐    
    val r = util.Random.nextInt   () => r 
}  
test() // Int = -240885810 
test() // Int = -1002157461 - 값이 달라 졌다.

* lazy val 로 선언하면 이름에서 느껴지는것 처럼 정의 되었을대 평가되는게 아니라 처음 호출시 평가됩니다.

 

 

스칼라에서의 함수 호출

 

스칼라에서는 함수호출시 ()  대신 {} 중괄호를 사용 할 수 있다.  단 !!! 인자가 하나일 경우 !!
def formatEuro (amt : Double) = {.......}   <-- 와 같은 Double 형의 매개변수 하나를 받는 함수가 있을 시formatEuro { val rate = 1.32; 0.235 + 0.7123 + rate * 5.32 }   <-- 이렇게 호출 할 수 있는데, {} 블럭 안에서 평가된 rate 가 매개변수로 들어가게 된다.
더 간단하게 예를 들면
println ("Hello World") 대신해서 println { "Hello World" } 를 사용 할 수 있다는 의미이다.
스칼라에서 이렇게 한 이유는 프로그래머가 중괄호{} 내부에서 함수 리터럴을 직접 사용하도록 하기 위해서인데, 이렇게 작성한 메소드는 호출 시 좀 더 제어 추상화 구문과 비슷해진다. 좀 더 수준높은 예제를 들어 보면 
def withPrintWriter(file: File, op: PrintWriter => Unit ) {   
val writer = new PrintWriter(file)try {    op (writer)} finally {    writer.close()}
}
이런 함수를 호출 할 때는 
withPrintWriter ( new File ("date.txt") , writer => writer.println(new java.util.Date) ) 
이렇게 호출 하며, 여기서는 인자가 2개이기 때문에 { } 를 사용할 수 없다. 하지만 !!!!
def withPrintWriter(file: File) (op: PrintWriter => Unit ) { ...
}
이러한 함수라면 {} 를 사용 할 수 있다.
val file = new File("date.txt")withPrintWriter  (file) { writer => writer.println(new java.util.Date) }
왜 이게 되냐면  커링을 통해서 file 을 매개변수로 먼저 넣어진 새로운 함수가 생겨졌고, 그 새로운 함수는  매개변수 하나를 필요로 하는 함수이기 때문이다.!! 보통 (변수,함수) 매개변수 조합일 때 많이 사용된다.
def foldLeft[B](z: B)(op: (B, A) => B): B = {
  var result = z
  this foreach (x => result = op(result, x))
  result
}
Vector(1, 2, 3).foldLeft(Vector[Int]()) { (is, i) =>
  is :+ i * i
}
(curry vs partially applying 이라는 헥깔림 주제도 있다)

 

 


웹 MVC 컨트롤러에서의 예  (Play for Scala)  

 

스프링에서 컨트롤러 함수의 예가  다음과 같은 모습을 취하는 반면 

 @RequestMapping(method = RequestMethod.GET)    
 public String printHello(ModelMap model) {      
 	model.addAttribute("message", "Hello Spring MVC Framework!");       
    return "hello";    
 }
 

스칼라언어 기반 Play 프레임워크 웹개발에서는 (위의 자바 예와 동일한 내용의 예가 아닙니다) 

def doSomething = Action{                           Ok.apply(views.html.index("Hi there"))                   }

이런 모양새로 이루어지는데 여기서 Action 이 무엇일까요? 

 

위의 예는 다음과 같이 풀어지게 되는데요

def doSomething: Action[AnyContent] = Action.apply({   Ok.apply(views.html.index("Hi there")) })

위의 apply 메소드의 정의는  apply(block: => Result) 이라 익명의 함수가 들어가게 됩니다.  

Action.apply 함수의 () 는 생략가능이고 apply 함수내에는 인자가 없고 Result 를 리턴하는 함수가 들어가네요.
이 함수 말이죠. 

{   Ok.apply(views.html.index("Hi there")) }

그리고 doSomething 메소드는  Action[AnyContent]  클래스의 인스턴스를 리턴합니다.

항상 같은 값을 리턴하기때문에 def 을 빼도 됩니다.  즉 메소드가 아니라 val 이어도 된다는 말.

 

 

Action { request =>   Ok("Got request [" + request + "]") }

처럼 인자가 있는 함수가 들어가기도 합니다.

클래스와 객체 

   

 

 클래스,필드,메소드
  
 객체 생성
  클래스는 객체의 청사진이다.
  class ChecksumAccumulator {
  } 

  이런 클래스 가 있을때  

  new ChecksumAccumulator  // 이렇게 해주면 객체가 만들어 진다.
  클래스 안에는 필드와 메소드를 넣을 수 있다. 이 둘을 합쳐 멤버라고 한다.

  

  필드는 var 이나 val 로 정의하며

  메소드는 def 로 정의 한다.

  class ChecksumAccumulator {
       var sum = 0 

  } 

 위의 클래스를 가지고 객체를 2개 만들면 

 val a = new ChecksumAccumulator 

 val b = new ChecksumAccumulator

 

 해당 객체안의 sum 필드는 다른 메모리를 참조 할 것이고 0 을 바라 볼 것이다.

 sum 이 var 이기 때문에, 나중에 다른 Int 값을 재할당 할 수 있다.

 a.sum = 3 // 요렇게 

 객체가 val 인데도 불구하고 내부의 값을 바꿀 수 있다는 점을 체크하라. 

 다만 처음 생성한 그 객체를 바라보고 있다는 확신은 가질 수 있다. 

 

 필드 접근성  

* 필드를 비공개로 만들어서 직접 접근하지 못하게 하라. private 을 사용한다. 

    - 아무것도 안붙히면 전체 공개. (자바와 다르다) 

 

 메소드 특성 

스칼라 메소드 파라미터에서 중요한 점은 이들이 val 이라는것이다.

  def add(b : Byte) : Unit = {

b = 1  // 파라미터가 val 이라 불가능 !

sum += b

   }

 * return 을 명시적으로 사용하지 말라. 

   스칼라는 메소드가 한 값을 계산하는 표현식인 것 처럼 생각하는걸 권장한다.

 * 메소드가 오직 하나의 표현식만 계산하는 경우 {} 중괄호를 없앨 수 있다.

   def add(b:Byte) : Unit = sum += b

 * 메소드의 결과 타입이 Unit 인 경우는 부수 효과를 위해 사용한다는 뜻이다. 

 * Unit 생략하고 = 를 없애서 {} 로 메소드를 나타낼 수 있는데, 이건 프로시저 처럼 보이게 하는데 
   프로시저란 오직 부수 효과를 얻기 위해서만 사용하는 메소드를 의미한다. 

   즉 def add(b: Byte) { sum += b}  처럼 사용한다는 뜻이데, 여기서 함정이 있는게 

   def add2 () { " test string " }  이건 Unit 를 리턴하며 
   def add3 () = { " test string " }  이건 String 을 리턴한다는 것이다. 

   즉 프로시저 처럼 작성하면 리턴이 무조건 Unit 이 된다는점~ 

 

 세미콜론 추론 

  스칼라에서는 보통 문장 끝의 세미콜론(;) 을 생략 할 수 있다.

  다만 한 줄에 여러 문장이라면 사용해야한다. 

  가끔 헷갈리는것은

  x

  + y 

 스칼라는 위 문장을 x 와 +y로 파싱한다. 

 (x

 +y)  이렇게 하거나 

+ 를 줄의 끝에 넣으면 우리가 의도하는대로 된다. 

x + 

y  

 

싱글톤 객체

스칼라와 자바의 가장 큰 차이점 중 하나는 스칼라는 정적 멤버가 없다는 것이다.

대신 전용으로 사용할 수 있는 싱글톤 객체를 제공한다. 

class 대신 object 라는 키워드를 사용한다.

import scala.collection.mutable.Map 

object ChecksumAccumulator {

	private val cache = Map[String, Int) ()
	def calculate(s: String) : Int = 

		if (cache.contains(s))
			cache(s)
		else {

			val acc = new ChecksumAccumulator

		for (c <- s)
			acc.add(c.toByte)
		val cs = acc.checksum()
		cache += (s -> cs)    // s 키, cs 값 으로 연관관계를 만듬. 
		cs
}

}

이 싱글톤 객체는 이름이 저 위에 있는 class ChecksumAccumulator 와 같다.

이렇게 싱글톤 객체는 동일하게 만들 수 있는데 이걸 동반 객체라고 하며 

같은 파일안에 있어야한다. 

이런 클래스와 동반객체는 상대방의 비공개 멤버에 접근 할 수 있게 된다.

즉 자바의 정적멤버가 외부에 따로 만들어져 있다고 보면 된다.

ChecksumAccumulator.calculate("hello") 이렇게 호출 할 수 있다. 

* 팁:  코드에서 맵 경우 WeakHashMap 같은 걸 사용하면 메모리가 부족할때 가비지콜렉터가 캐시의 원소를 수집.

- 싱글턴 객체는 파라미터를 받을 수 없고 클래스는 받을 수 있다.

- 동반 클래스가 없는 싱글톤 클래스를 독립 객체라고 한다. 

  독립객체로는 유틸리티 메소드를 모아두는곳 이나 진입점으로 사용할 수 있다. 

 

스칼라 애플리케이션 

import ChecksumAccumulator.calculate

object Sumer {

	def main(args: Array[String]){

		for (arg <- args)

			println(arg : ": " + calculate(arg() )  // 메소드만 사용함!

	}

}

 * 스칼라는 항상 java.lang 과 scala 패키지의 멤버를 암시적으로 임포트한다.

또한 Predef 이라는 싱글톤 객체도 임포트하는데 여기엔 유용한 메소드가 많이 있다.

println 이라든지 assert 라든지..

 

 * 스칼라의 import 는 자바의 static import 로 생각 할 수 있다. 스칼라에서 다른 점은 싱글톤 뿐 아니라 어느 객체에서라도 멤버를 임포트 할 수 있다는 것이다.

* 자바는 공개  클래스 이름과 파일 이름이 같아야 하지만 스칼라에서는 상관없다. 

* scala.Application 이라는 트레이트를 통해 더 간단히 main 을 작성 할 수 있다.

import ChecksumAccumulator.calculate

object Sumer extends Application  {

	for (arg <- List("fall","winter"))

		println(arg : ": " + calculate(arg() )  // 메소드만 사용함!

}

 

클래스 예제

 

1) 간단한 클래스  

 

# 한줄 만으로 class 완성! 
class Person(var name: String, var age: Int) // 기본 생성자가 class 이름과 나란히~ 

# class 사용 
val al = new Person("Al", 42) 
al.name // "Al" 
al.age // 42

 

 

2) 자바와 다른 특이한 모습

 

// class 정의 
class Person(val firstName: String, val lastName: String) {
 println("the constructor begins") // 희안하죠? 객체로 만들어지는 동시에 호출됩니다.
 val fullName = firstName + " " + lastName 
 val HOME = System.getProperty("user.home"); 
 def foo { println("foo") } 
 def printFullName { println(fullName) } 

 printFullName // 희안하죠? 객체로 만들어지는 동시에 호출됩니다. 
 println("still in the constructor") // 희안하죠? 객체로 만들어지는 동시에 호출됩니다. } 

// 다음과 같이 나옵니다. 클래스 내부의 것들이 생성됨과 동시에 실행되네요. 
scala> val p = new Person("Alvin", "Alexander") 
the constructor begins 
Alvin Alexander 
still in the constructor 
p: Person = Person@68f507d2

 

3) 보조 생성자 

 this 를 사용하여  보조적 생성자를 만들수 있네요. (클래스 이름가지고 만들지 않음) 

 

class Pizza { // 기본 생성자에는 매개변수를 받는게 없군요. 
  var crustSize = 12 var crustType = "Thin" 
  def this(crustSize: Int) { // 매개변수 하나 받는 생성자~ 
     this() 
      this.crustSize = crustSize 
  } 
  def this(crustSize: Int, crustType: String) { // 매개변수 2개 받는 생성자 
       this(crustSize) 
       this.crustType = crustType 
  } 
  override def toString = { "A %s inch pizza with %s crust.".format(crustSize, crustType) } }

 

  println(new Pizza)
  println(new Pizza(14))
  println(new Pizza(16, "Thick"))

 

4) 생성자 매개변수에게 디폴트 값을~

 

class Socket (var timeout: Int = 10000) // 디폴트 값을 사용함 
val s = new Socket s.timeout // Int = 10000
val s = new Socket(5000) s.timeout // Int = 5000

 

5) 추상 클래스를 사용할 경우는 언제? 

 

추상클래스는 생성자 매개변수를 가질수 있지만, trait 는 그럴 수 없다. 
 * trait 는 자바의 인터페이스 비슷한 것 으로 추후에 설명 예정.

 

// 컴파일 안됨 trait Animal(name: String) 
// 추상 클래스는 가능 ! abstract class Animal(name: String)

 

abstract class BaseController(db: Database) {
  def save { db.save }
  def update { db.update }
  def delete { db.delete }
  def connect                             // abstract since it has no implementation
  def getStatus: String                   // an abstract method that returns a String
  def setServerName(serverName: String)   // an abstract method that takes a parameter
}

 

6) 스칼라 case 클래스 

 case 클래스는  유용한 행사코드를 자동으로  생성할 수 있다.

case class Person(var name: String, var age: Int) // class 앞에 case 를 붙여주는것으로

 

case class Person(var name: String, var age: Int) // class 앞에 case 를 붙여주는것으로

case class 를 사용하면 다양한 혜택과 자동으로 다음을 만들어 준다:

  • toString, equals, 와 hashCode  메소드를 만들어 준다.
  • 생성자 파라미터들에게 게터와 세터 속성이 부여된다.
  • 더이상 인스턴스로 만드는데 new 를 사용할 필요가 없다.
  • match/case 문에서 편리하게 이용할 수 있다.

 

객체의 동일성

 
두개의 객체가 동일한지 아닌지 구분하는 작업은 별거 아닌거 같지만 , 
꽤나 복잡하고 미묘한 일들이 도사리고 있습니다. 방심하다 망하죠. ;;
게다가 언어 마다 다릅니다. 테스트만이 살길~
 
* 2007년 상당량의 자바코드를 연구한 논문에서는 거의 대부분의 equals 메소드에 오류가 있다는
 결론을 내릴 정도로 상상 이상으로 실수가 많
답니다. OTL 
 
이 글 에서는 중요 포인트만 딱딱 집어서 설명 해 보겠습니다. ( 모든 걸 설명하지 않습니다. ) 
혹시 더 자세하게 파헤치고 싶은 분이라면  아래 서적을 참고 하시구요. (꼭 읽어보길 당부..)
 
자바 : Effective Java 2판 - 항목 8,9 
스칼라 :  Programming in Scala 2판 - 30장
 
먼저 익숙한 자바로 먼저 살펴보고, 그 후에 스칼라로 알아보겠습니다.  
객체의 동일성을 체크하는데 두 언어는 다른 생각을 가지고 있다는 점   체크하세요~
 
C++
* C++ 의 경우는 객체 비교시 포인터의 주소로 하거나 ,  == 연산자 오버로딩을 통해서 처리 하죠. equals 같은건 자체 제공하지 않습니다. string 클래스 경우 compare 함수와 == 연산자 오버로딩을 제공합니다.
 
Python 
* Python 의 경우  디폴트는 객체의 id 를 비교하며  문자열은 ==  연산자로 객체의 내용을 비교하여 True, False를 판정하며,   __eq__ , __ne__ , __hash__ 메소드를 오버라이딩 하여 사용자 정의 해줍니다.  특이한건 저 메소드를 정의하면 == , != 의 정의가 자동으로 바뀌어 집니다. (자바는 안그러죠? 스칼라는 파이썬과 비슷합니다) 동일한 참조를 비교할때  is  를 사용합니다.  

 

 

자바 
 == 와 equals

자바에서 동일성을 체크하는 방법에는 == 와 equals 가 사용 되는데요. 

값 타입에는 자연스러운 등호와 같지만 둘 다  참조 타입에 관해서는 객체의 주소가 같은지 비교합니다. 

 

즉  위 그림처럼  == 와  equals  (java.lang.Object) 둘 다  동일한 주소를 가르켜야 같다고 판정합니다. 

 

 

위와 같이 가르키는 객체는 달라도 각 객체가 담고있는 값(들) 이 같으면 같다고 만들 수 도 있는데..

equals 의  오버라이딩을 이용해서   동일성 체크방법을 바꿀 수  있습니다.

String a = new String("test")

String b = new String("test")

a.equals(b)

위의 a 와 b 를 비교하면 true 가 되는것은 equals 를 String 객체에서 오버라이딩 하고 있기 때문입니다.

a.equals(b)  // true

a == b      // false

입니다. 자바에서 == 는 무조건 동일한 객체를 바라보고 있어야 true 가 됩니다. 

결국 자신이 만든 객체를 String 처럼  객체가 가진 내용으로 동일성 여부를 판단하려면
반드시 equals 를 오버라이딩을  해 주고 equals 를 사용해야 합니다.

 

hashCode

 " equals 메소드를 오버라이드 하는 모든 클래스는 반드시 hashCode 메소드도 오버라이드 해야 한다"

  hashCode 을 오버라이드 하지 않으면 HashMap 과 HashSet 같은 모든 해쉬기반 컬렉션들을 사용할때
  문제가 생깁니다. 

 

eqauls 함수 작성법

중요하다고 생각되는  세가지만 살펴보겠습니다. 더 자세한 내용과 팁은 effective java item 8,9 를 참고하세요.

* 동치 관계로 정의하자 (반사성/대칭성/추이성/일관성 을 지키자) . ( 상세 내용 다음 포스트에 ) 

* equals 메소드를 오버라이드 할 때는 hashCode 를 항상 오버라이드한다.

* equals 메소드의 인자 타입을 Object 대신 다른 타입을 사용하지 말자. 

public boolean equals (MyClass c) {

     ...

}

이렇게 하지 말라는 겁니다.

public boolean equals (Object o) {

     if (! (o instanceof MyClass))

     return false;

MyClass c = (MyClass) o;

...

}

이게 정석입니다.

인자를 MyClass 로 하면 equals 를 오버라이딩 하는게 아니라 오버로딩하기때문에 상위의 기존 eqauls 메소드는 그대로 남아있게되고, 여러 상황에서 상위 equals 메소드를 사용하게 됩니다.

 

스칼라 

 

자바 프로그램을 작성할 때 초보자가 자주 빠지는 함정이 equals 를 오버라이드 한 후 비교해야 하는
두 객체를 == 로 비교 하는 것입니다. 

스칼라의 경우 객체가 같은지 비교하는 동일성 연산자가 eq 라고 있는데 자주 사용되지는 않습니다.

'x eq y'  경우  같은 객체를 가르킬 때 만 true 입니다.

스칼라에서는 == 동일성을 '자연스러운' 동일성을 위해 예약해 뒀습니다.  (코틀린도 마찬가지) 

값 타입의 경우는 자바와 마찬가지로 값을 비교하구요

참조 타입에 관해서 스칼라의 == 는 자바의 equals 와 같습니다. 

즉 새로운 타입에 관해 equals 메소드를 오버라이드하면 자바와는 다르게 == 의 의미도 재정의가 됩니다.

Any 클래스에 있는 equals 는 오버라이드 하지 않는 경우 자바의 == 와 같은  객체 동일성을 사용합니다.

결국 equals , == , eq 는 기본적으로 같습니다. (동일 주소인지 비교) 하지만 equals 를 오버라이드하면 자바는 그것만 재정의 되는 반면, 스칼라는 == 도 함께 재정의 된다가 중요 포인트입니다. 

 

그림으로 간단히 풀어 보겠습니다.

 eq

 

 

자바의 == 처럼 같은 객체(주소) 를 가르켜야 같다고 인정됩니다.

 

 == 와 equals

 

만약 equals 를 오버라이딩 하는 경우에는 위의 그림 처럼 객체의 내용으로 비교 가능합니다.

이때 == 도 자연스럽게 변경됩니다. 따라서 본능적(?) 으로 == 를 객체 비교하는데 사용해도 문제가 없어집니다. 

* equals 를 오버라이드 안하면  equals 와 == 는 둘 다 객체의 주소를 가르켜야 같다고 판정됩니다.

 

equals 의 함정 

1. equals 선언 시 잘못된 시그니처를 사용하는 경우 

class Point (val x : Int , val y: Int) { ... } 라는 클래스의 eqauls 를 정의해본다고 하자.

def equals(other : Point) : Boolean = this.x == other.x && this.y == other.y 

뭐 너무 단순해서 문제가 생길 여지가 없어보인다.

val p1,p2 = new Point(1,2) 

p1.equals(p2)  // true 

val q = new Point(2,2)

p1 equals q  // false

 

잘 작동한다~~

하지만 ~!! 컬렉션에 넣기 시작하면서 문제가 발생한다. 

 

val s = HashSet(p1)

s.contains(p2)  // false !!

엥~ 둘은 같은데 HashSet에 존재하지 않다네요..  아래 예를 일단 보시죠.

val p2a : Any = p2 

p1 equals p2a  // false

타입을 Point 에서 Any 로 바꾸어서 equals 를 사용하니 다르다고 하네요?

사실 앞에서 정의한 equals 는 표준 메소드인 equals 메소드를 오버라이드하지 않고 그냥 오버로딩합니다.

그 상태에서 HashSet 의 contains 메소드는 제네릭 집합에 작용하기 때문에, Object 에 있는 더 일반적인 equals 를 호출하기때문에 false 가 되었던 것입니다.

다음과 같이 수정해야죠.

override def equals(other Any) = other match {

case that : Point => this.x == other.x && this.y == other.y

case _ => false

}

근데 이것만으로 충분하지 않습니다.. . (-.-;;)  

 

2. equals 를 변경하면서 hashCode 는 그대로 놔두는 경우

val s = HashSet(p1)

s.contains(p2)  // false !!

다시 contains 를 해보면 그래도 false 가 나옵니다. (true 가 나올수도..)

이랬다 저랬다 하는 이유는

HashSet 은 원소들을 해시코드에 따른 '해시 버킷' 에 담게 됩니다. 

contains 는 먼저 '해시 버킷' 을 결정한다음에 , 그 버킷안에서 원소들을 찾게 되는데

hashCode를 재정의 안하면 객체 주소를 적당히 바꿔서 해시 코드로 만듭니다. 
결국  
p1 과 p2 는 객체 주소가 다르기 때문에  다른 해시코드를 만들고 다른 '해시버킷' 에 담길 가능성이
커지죠. 
그래서 못찾게 되는겁니다. 결국 hashCode 를 동일하게 만들어 주는 작업이 필요합니다.

hashCode 를 구현 해 보겠습니다.

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

override def hashCode = 41 * (41 + x) + y

...

}

이렇게 해주면 p1 과 p2 는 동일 해시코드를 가지고 있기때문에 동일 '해시버킷' 에 담기게되서 

contains 는 잘 작동하게 됩니다.  (hashCode 를 어떻게 만드느냐도 중요한 주제입니다만 .패스~) 

 

3. equals 를 변경 가능한 필드의 값을 기준으로 정의한 경우

 class Point (var x : Int , var y: Int) { ... } 라는 클래스의 eqauls 를 정의해본다고 하자. ( var 로 정의됨) 

val p  = new Point(1,2) 

val s = HashSet(p)

s.contains(p) // true 이다.

여기서 

p.x +=1 로 바꾸어보자.

s.contains(p) // false !

왜 이럴까요?

콜렉션안의 저장되있는 '해시버킷' 의 위치와 1 더 해진 후 계산되는 '해시버킷' 의 위치가 달라져서

시야에서 사라져 버렸기 때문입니다. 

equals 와 hashCode 가 변경 가능한 상태에 의존하면 잠재적으로 문제를 야기할 수 있음을 인지합시다.

 

4. equals 를 동치 관계로 정의 하지 않은 경우 

 동치 관계로 정의하자 (반사성/대칭성/추이성/일관성 을 지키자) . ( 상세 내용 다음 포스트에

 

좋은 equals 와 hashCode 를 만드는 방법 

class Rational(n : Int, d : Int ) {

	val numer = Iif (d<0) -n else n) / g

	val denom = d.abs / g 

	...

	override def equals (other : Any) : Boolean = 

	other match {

		case that: Rational => (that canEqual this) && numer == that.numer && denom == that.denum

		case _ => false

	}


    def canEqual(other : Any) : Boolean = other.isInstanceOf (Rational)

	override def hashCode: Int = 41 * (41 + number) + demon


...

}

 

가.  equals 를 final이 아닌 클래스에서 오버라이딩하면 , canEqual 메소드를 만들자.

나.  canEqual 메소드는 인자 객체가 현재 클래스라면 true 로 하자. (canEqual 내용은 다음 포스트에) 

다.  equals 메소드 정의에서 전달 받는 파라미터의 타입은 반드시Any 이다.

라.  equals 메소드의 본문을 match 표현식을 하나 사용해 작성하라. 

마.  equals 식에서 첫 번째 대안 부분에는 클래스의 타입과 같은 타입 패턴을 선언한다. case that: Rational =>

바.  이 case 문의 본문에, 객체들이 같기 위해 만족해야 하는 조건을 논리곱(&&) 을 사용해 작성하라.

     만약 오버라이드 하는 equals 가 AnyRef 에서 온것이 아니라면, 슈퍼클래스의 equals 를 호출하도록 한다.

     super.equals(that) && 

사. match 문의 두 번째 case 에는 와일드 카드 패턴을 사용해 false 반환 . case - => false

아. hashCode 의 경우 equals 메소드에서 동일성 계산에 사용했던 모든 필드('중요한필드') 를 포함시켜라. 

    전체 객체의 해시 코드를 계산하기 위해, 첫 필드의 해시 코드에 41을 더한 결과에 1 을 곱하고~~반복.

 

스칼라 요약 

 

* equals 를 오버라이딩하면 == 도 자동으로 적용

* hashCode 를 반드시 함께 오버라이딩 

* 타입 시그니처 주의

* 변경 가능한 상태를 가진 값으로 hashCode 를 계산해서는 안된다.

* 클래스가 final 이 아니라면 canEqual 메소드도 정의해야한다.

* 비교 가능한 클래스를 정의할 때 케이스 클래스로 만드는 것도 좋다.

 

 

+ Recent posts