스칼라 클래스 이해하기 의 3편이다.


1편 - [scala] class 예시 1 - 일반 클래스, 싱글톤 클래스(singleton class), 케이스 클래스(case class), main메소드/App 상속 클래스, 합성 타입(Compound), 제네릭(generic) 클래스

2편- [scala] class 예시 2 - 암시적 변환(implicit conversion - implicit converter/implicit class/implicit parameter)




믹스인(mixin) 클래스를 설명한다. 최근에 루비를 개발할 때 믹스인(mixin)을 보면서 엄청 방황(?)한 적이 있다. C++/java 개발에서 Ruby, Scala개발로 넘어가면서 재미있는 특징을 알게 된다. 

루비는 Include로 mixin을 구현한다. (자세한 내용은 http://blog.saltfactory.net/ruby/understanding-mixin-using-with-ruby.html을 참조한다).


Scala는 단일 상속을 지원하고, 다중 상속 같은 느낌을 믹스인으로 해결한다. 루비의 include  + 자바의 inline 개념으로 이해하면 될 것 같다.


Scala의 믹스인은 매우 간단하다. extends로 단일 상속으로 하게 하고, with로 믹스인을 구현한다. 

(처음에는 implements로 생각하는 바람에 다중 상속인 줄 알았다.)


abstract class Data {
def print1 = println("Data!!")
}

trait BigData {
def print2 = println("Big Data!!")
}

class InternalBigData extends Data with BigData {
}

object Main extends App {
val test = new InternalBigData
test.print1
test.print2
}


결과는 다음과 같이 간단하다.


Data!!

Big Data!!





이제, 내부 역어셈블링을 통해 내부 자바 코드를 살펴본다.



trait는 abstract class이며, 매개변수를 받지 않은 클래스이다. 

스칼라 컴파일러는 BigData trait를 interface로 바꾸고, 실제 구현 내용은 자식 클래스의 메소드로 inlining 시킨다. 




public abstract class Data

{


    public void print1()

    {

        Predef$.MODULE$.println("Data!!");

    }


    public Data()

    {

    }

}



public interface BigData

{


    public abstract void print2();

}




public class InternalBigData extends Data  implements BigData

{


    public void print2()

    {

        BigData.class.print2(this);

    }


    public InternalBigData()

    {

        BigData.class.$init$(this);

    }

}






다음은 AnyVal에 대한 설명이다. 


AnyVal 클래스는 모든 값 타입의 최상위 클래스이고, 내부 호스트 시스템에서 객체로 구현되지 않은 값을 생성한다.

AnyVal 클래스를 상속받은 클래스는 단일 매개변수가 하나의 public val인 주요 생성자가 단 하나가 있어야 하고, 해당 매개변수는 값 클래스가 될 수 없다. 또한 이것 외에도 제약이 좀 있다..




예시를 통해 공부해 본다. 간단한 테스트를 진행한다. 

case class Price(price: BigDecimal)
object Price {
def equals(a : Price, b : Price) : Boolean = {
a.price == b.price
}
}

object Main extends App {
val a = Price(2000)
val b = Price(2000)
println(Price.equals(a, b))
}

결과는 true이다. 


여기서 역어셈블링(jad)을 해서 내부 구조를 살펴본다.

Main 클래스를 살펴보면 다음과 같이 예상되는 코드가 생긴다. 



 public final void delayedEndpoint$Main$1()

    {

        a = new Price(BigDecimal$.MODULE$.int2bigDecimal(2000));

        b = new Price(BigDecimal$.MODULE$.int2bigDecimal(2000));

        Predef$.MODULE$.println(BoxesRunTime.boxToBoolean(Price$.MODULE$.equals(a(), b())));

    }


    public static final Main$ MODULE$ = this;

    private final Price a;

    private final Price b;

    private final long executionStart;

    private String scala$App$$_args[];

    private final ListBuffer scala$App$$initCode;



Price 클래스를 살펴본다.


public final class Price$

    implements Serializable

{


    public boolean equals(Price a, Price b)

    {

        BigDecimal bigdecimal;

        BigDecimal bigdecimal1 = a.price();

        bigdecimal1;

        bigdecimal1;

        bigdecimal = b.price();

        JVM INSTR ifnonnull 21;

           goto _L1 _L2

_L1:

        JVM INSTR pop ;

        if(bigdecimal == null) goto _L4; else goto _L3

_L2:

        bigdecimal;

        equals();

        JVM INSTR ifeq 32;

           goto _L4 _L3

_L4:

        true;

          goto _L5

_L3:

        false;

_L5:

        return;

    }




Price 같이 Wrapper class가 많아지면, 코드 관리는 편한데, 메모리를 많이 쓰고 GC에 영향을 줄 것이다. 스칼라 컴파일러에서 이런 메모리 압박을 줄이기 위해 2.10부터 AnyVal이라는 클래스를 만들었다. AnyVal 클래스는 값 클래스(value class)를 정의하고, 스칼라 컴파일러가 특별히 취급한다. 값 클래스는 인스턴 할당을 피하기 위해 컴파일 타임 때 최적화되고, 대신 래핑된 타입을 사용한다. 


이렇게 괜찮은 AnyVal을 상속받은 값 클래스는 제약이 당연히 있다. 

- 단일 매개변수가 하나의 public val인 주요 생성자가 단 하나가 있어야 한다. 

- 생성자의 매개변수는 값 클래스가 될 수 없다. 

- val 또는 var는 값 클래스 내부에서 사용할 수 없다.




위의 예시에서 class에 AnyVal 클래스를 상속한다.

case class Price(price: BigDecimal) extends AnyVal
object Price {
def equals(a : Price, b : Price) : Boolean = {
a.price == b.price
}
}

object Main extends App {
val a = Price(2000)
val b = Price(2000)
println(Price.equals(a, b))
}



Main 클래스를 역어셈블해보면, 다음과 같다. 



    public final void delayedEndpoint$Main$1()

    {

        a = BigDecimal$.MODULE$.int2bigDecimal(2000);

        b = BigDecimal$.MODULE$.int2bigDecimal(2000);

        Predef$.MODULE$.println(BoxesRunTime.boxToBoolean(Price$.MODULE$.equals(a(), b())));

    }


    private Main$()

    {

        scala.App.class.$init$(this);

        delayedInit(new Main.delayedInit.body(this));

    }


    public static final Main$ MODULE$ = this;

    private final BigDecimal a;

    private final BigDecimal b;

    private final long executionStart;

    private String scala$App$$_args[];

    private final ListBuffer scala$App$$initCode;



그리고, Price 클래스를 역어셈블링하면 다음과 같다.



public final class Price$

    implements Serializable

{


    public boolean equals(BigDecimal a, BigDecimal b)

    {

        a;

        BigDecimal bigdecimal = b;

        if(a != null) goto _L2; else goto _L1

_L1:

        JVM INSTR pop ;

        if(bigdecimal == null) goto _L4; else goto _L3

_L2:

        bigdecimal;

        equals();

        JVM INSTR ifeq 26;

           goto _L4 _L3

_L4:

        true;

          goto _L5

_L3:

        false;

_L5:

        return;

    }



스칼라가 컴파일러가 최적화를 이렇게 진행한다. 만약 Price의 타입이 만약 Int라면 어떻게 될까? 컴파일하면 자바 Primitivie type인 int로 변환된다. 이렇게 Primitive type이 존재하는 타입은 모두 변환되기 때문에 엄청난 성능 이득을 얻을 수 있다. 


public final class Price$

    implements Serializable

{


    public boolean equals(int a, int b)

    {

        return a == b;

    }





AnyVal이 너무 제약적이기 때문에, 유니버셜 트레이트로 좀 확장할 수 있다. 

유니버셜 트레이트는 Any를 상속한 트레이트이다.


역시 유니버셜 트레이트도 제약이 있다.

- 멤버는 def만 가지고 있으며, 초기화를 수행하지 않는다.

- 중첩 클래스 또는 객체 정의는 또한 불가능하다

- 다른 제한은 값 클래스가 유니버셜 트레이트(universal trait)말고 다른 것을 상속하지 않는다




trait Printable extends Any {
def print() : Unit = println(this)
}

case class Price(price: Int) extends AnyVal with Printable
object Price {
def equals(a : Price, b : Price) : Boolean = {
a.price == b.price
}
}

object Main extends App {
val a : Printable = Price(2000)
a.print()
}


스칼라 컴파일러가 문제 없이 잘 만들어줬다. 


public final class Price$

    implements Serializable

{


    public boolean equals(int a, int b)

    {

        return a == b;

    }




참고로 유니버셜 트레이트를 만드는 제약을 깨뜨리면, (다음과 같은 코드)

trait Printable extends Any {
val a = 1
def print() : Unit = println(this)
}


아래와 같은 컴파일러 에러가 발생한다. 

Error:(6, 7) field definition is not allowed in universal trait extending from class Any

  val a = 1



유니버설 트레이트가 스칼라에서 여기 저기 쓰인다. 대표적으로 Ordered trait가 있다.


trait Ordered[A] extends Any with java.lang.Comparable[A] {





다음은 type 키워드에 대한 설명이다. abstract type이라 지칭한다.

 type 키워드는 trait와 class에서 별명(alias)로 사용할 수 있다. trait에서 정한 타입을 추상화한다. Generic과 많이 유사하지만, 나름 영역이 있다고 하니. 많이 써보고 generic과 abstract type의 용도를 봐야 할 것 같다. 


trait Price {
type T
def method : T
}

class CarPrice extends Price {
type T = Int
def method: T = 1000
}

object Main extends App {
println((new CarPrice).method)
}

결과는 1000이다. 












Posted by '김용환'
,


scala 코드가 어떻게 jvm에서 인식될 수 있는 지 확인하려면 세 가지 방법이 있다.


1. jad

2. javap 

3. scalac -print




1.jad

jad는 scalac  자바 class 화일을 java로 역어셈블링한다. 간단한 파일은 scalac -print로 테스트해볼 수 있지만, scalac 파일이 많아지면 너무 많아진다. 필요한 것만 보기에 좋다.


$ scalac Main.java

$ jad *.class

$ls *

Main$.class                 Main.class                  Price$.class                Price.jad                   Printable.jad

Main$.jad                   Main.jad                    Price$.jad                  Printable$class.class

Main$delayedInit$body.class Main.scala                  Price.class                 Printable.class


클래스 파일을 에디터로 다음처럼 살펴보면 된다.


$ vi Price.jad


// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.

// Jad home page: http://www.kpdus.com/jad.html

// Decompiler options: packimports(3)

// Source File Name:   Main.scala


import scala.*;

import scala.collection.Iterator;


public final class Price

    implements Printable, Product, Serializable

{


    public static String toString$extension(int i)

    {

        return Price$.MODULE$.toString$extension(i);
...





2. javap

JDK 툴을 사용해서 jvm languague를 살펴본다. 


$ scalac Main.java

$ javap -c Price

(만약 private 메소드를 보려면, -private을 추가한다. $ javap -c -private Price)



Compiled from "Main.scala"

public final class Price implements Printable,scala.Product,scala.Serializable {

  public static java.lang.String toString$extension(int);

    Code:

       0: getstatic     #22                 // Field Price$.MODULE$:LPrice$;

       3: iload_0

       4: invokevirtual #24                 // Method Price$.toString$extension:(I)Ljava/lang/String;

       7: areturn





3. scalac -print

java 기반이 아닌 scala식 역어셈블링이다.


관련된 코드가 예쁘게 모두 나온다. 너무 많은 클래스를 컴파일 할 때는 너무 많이 나올 수 있다. 


$ scalac -print Main.scala

[[syntax trees at end of                   cleanup]] // Main.scala

package <empty> {

  abstract trait Printable extends Object {

    def print(): Unit

  };

  final case class Price extends Object with Printable with Product with Serializable {

    def print(): Unit = Printable$class.print(Price.this);

    <caseaccessor> <paramaccessor> private[this] val price: Int = _;

    <stable> <caseaccessor> <accessor> <paramaccessor> def price(): Int = Price.this.price;

    <synthetic> def copy(price: Int): Int = Price.copy$extension(Price.this.price(), price);

    <synthetic> def copy$default$1(): Int = Price.copy$default$1$extension(Price.this.price());

    override <synthetic> def productPrefix(): String = Price.productPrefix$extension(Price.this.price());

    <synthetic> def productArity(): Int = Price.productArity$extension(Price.this.price());

    <synthetic> def productElement(x$1: Int): Object = Price.productElement$extension(Price.this.price(), x$1);

    override <synthetic> def productIterator(): Iterator = Price.productIterator$extension(Price.this.price());

    <synthetic> def canEqual(x$1: Object): Boolean = Price.canEqual$extension(Price.this.price(), x$1);

    override <synthetic> def hashCode(): Int = Price.hashCode$extension(Price.this.price());

    override <synthetic> def equals(x$1: Object): Boolean = Price.equals$extension(Price.this.price(), x$1);

    override <synthetic> def toString(): String = Price.toString$extension(Price.this.price());

    def <init>(price: Int): Price = {

      Price.this.price = price;

      Price.super.<init>();

      Printable$class./*Printable$class*/$init$(Price.this);

      scala.Product$class./*Product$class*/$init$(Pr

Posted by '김용환'
,


신호와 소음의 저자를 알게 된 것은 미국의 한 데이터 사이언티스트가 방한 후 네이트 실버를 추천하면서, 신호와 소음이라는 책을 알려주었다.


이후에 나는 네이트 실버라는 사람에 대한 궁금증와 계속 고민해 오던 통계에 대한 통찰력을 얻고 싶었다. 단순히 통계는 수학이 아닐터인데, 관련된 대부분의 책은 '수학의 정석' 마냥 문제와 문제 풀이에만 집중하고 있는 것들이 왠지 모르게 불편했다.


그런 나의 불편을 알듯이 이 책은 그 부분을 잘 설명해준 책이다. 이 책을 읽고 마지막을 읽는 순간까지 즐거웠다. 이 사람이 아마추어지만 프로에 뒤지지 않는 그의 통찰력에 감탄했다. 


훌륭한 도박사와 과학자가 추구하는 방법이 같다라고 주장할 정도로, 어쩌면 plausible reasoning에 대한 생각을 내게 전달해주어서 개인적으로 네이트 실버에게 감사하다라고 말하고 싶다. 


또한, 베이지안 추론에서 제일 중요한 것은 사전 통계이다. 생각보다 많은 기술 서적에서 이런 내용을 사실 잘 얘기하지 못하고 있었는데, 신호와 소음에서는 아주 잘 다뤘다. 


그리고, 베이즈와 흄이 동시대 사람이었던 것,  짧지만 베이즈의 삶에 대해서 언급해줘서 왜 그렇게 노력했는지도 좀 알게 되었다. 



먼저 통계나 베이지안 추론에 관심이 있는 개발자는 먼저 이 책을 읽고 다른 기술 서적을 봐도 좋을 것 같다. 







Posted by '김용환'
,