Functional Programming Principles in Scala - Data and Abstraction

3.1 Class Hierarchies

이전까지 single class에 대해 여러 각도로 확인해보았다면, 지금부터 좀 더 일반화하여 class 간 hierarchy구조를 보자.

Abstract Classes

abstract class IntSet {  
    def incl(x: Int): IntSet
    def contains(x: Int): Boolean
}

위 class를 보면 각 method에 body가 없는 것을 볼 수 있는데, 만약 abstract를 제거하면 에러를 발생한다. 또한, body가 없기 때문에 new를 통해 class를 instance할 수 없다. 이렇게 class에 대해 추상적(abstractive)으로 정하는 것을 abstract class라고 한다.

Class Extensions

set을 이용하여 binary tree를 구현해보자. tree에 가능한 type은 1. empty set을 위한 tree와 2. integer 값을 가지며 두개의 sub-tree를 가진 tree로 구분된다. 이를 구현하면 아래와 같다.

class Empty extends IntSet {  
    def contains(x: Int): Boolean = false
    def incl(x: Int): IntSet = new NonEmpty(x, new Empty, new Empty)
}

여기서 중요한 것은, Empty라고 생각하지만 실제로 left, right로 빈 node를 지신 NonEmpty의 작은 형태이다. 이해가 어렵다면 그림으로 이해를 해보자.

그럼 NonEmpty도 마저 구현해보자.

class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet {

    def contains(x: Int): Boolean =
        if (x < elem) left contains x
        else if (x > elem) right contains x
        else true

    def incl(x: Int): IntSet =
        if (x < elem) new NonEmpty(elem, left incl x, right)
        else if (x > elem) new NonEmpty(elem, left, right incl x)
        else this
}

이렇게 구현을 할 경우, 새로운 node를 생성할 경우 이전에 가지고 있던 tree를 유지하는 가운데 sub-tree를 공유하게 된다. 이런 식의 data structure를 persistent data structure라고 하며, 또하나의 functional programming cornerstone이다.

정리하면, EmptyNonEmpty는 둘다 IntSetextend하고 있다. 따라서, IntSet type을 conform하는 것이라고 볼 수 있다.

  • an object of type Empty or NonEmpty can be used wherever an object of type IntSet is required.

용어를 좀 더 정리하자면, IntSetEmptyNonEmptysuperclass라고 이야기하고, 반대로 EmptyNonEmptyIntSetsubclass라고 이야기한다. Scala에서는 다른 class를 extend할 수 있는데, 만약 superclass가 주어지지않으면 JAVA Package의 Object class를 자동 extend하게 된다. (toString 등 Method를 가지고 있는...) 또한, direct 또는 indirect로 class C의 superclass를 접근한다면 그걸 base class라고 한다. 여기서는 NonEmpty의 base class가 IntSetObject이다.

앞선 예제는 abstract function 인 IntSetcontainsincl를 다른 class들이 implement하고 있는데, non-abstract definition에 대해서도 동일하게 override를 이용하여 redefine할 수 있다.

abstract class Base {  
    def foo = 1
    def bar: Int
}

class Sub extends Base {  
    override def foo = 2
    def bar = 3
}

Object Definitions

IntSet 예를 보면, Empty의 경우 불필요하게 많은 instance를 생성해야 하는 것을 볼 수 있다. 따라서 이런 경우 사용자가 Empty를 Singleton으로 설계하고자 할때 단순히 object definition을 이용하여 적용이 가능하다.

object Empty extends IntSet {  
    def contains(x: Int): Boolean = false
    def incl(x: Int): IntSet = new NonEmpty(x, Empty, Empty)
}

incl body 내 NonEmpty 생성 시 new키워를 제외하고 Empty만 넣은 것을 확인할 수 있다. Singleton object는 value로 그 자신을 evaluation하게 되어있어, 별다른 evaluation과정이 필요없다.

Programs

지금까지 우리는 모든 Scala code를 REPL 또는 worksheet에서 실행해 왔다. 하지만, Scala에서도 standalone application을 생성할 수 있다. 이 경우 main method를 가진 object를 생성하면 되는데, 아래와 같이 만들 수 있다.

object Hello {  
    def main(args: Array[String]) = println("hello world!")
}

위 code를 compile 후 아래와 같이 실행하면 된다.

> scala Hello

Example

이번엔 abstract definition에 union(other: IntSet): IntSet 을 추가하여 정의해보자.

abstract class IntSet {  
    abstract class IntSet {
    def incl(x: Int): IntSet
    def contains(x: Int): Boolean
    def union(other: IntSet): IntSet
}

class Empty extends IntSet {  
    ...
    def union(other: IntSet): IntSet = other
}

class NonEmpty extends IntSet {  
    ...
    def union(other: IntSet): IntSet = 
        ((left union right) union other) incl elem
}

Dynamic Binding

Scala를 포함한 Object-oriented language에서는 dynamic method dispatch를 적용한다. 이것은 method에 의해 code가 호출 될때 그 method를 포함한 object의 runtime type에 의존한다는 것을 의미한다. Dynamic dispatch method는 higher-order function과 동일하게 보인다. 따라서 아래와 같이 질문 할 수 있다.

  • Objects in terms of higher-order functions?
  • Higher-order functions in terms of objects?

3.2 How Classes Are Organized

JAVA와 동일하게 Scala 또한, class와 object에 대해 package화 되어있다. 따라서 아래와 같이 package 명령어를 통해 package를 불러오거나 export할 수 있다.

package progfun.exmaples

object Hello { ... }  

위와 같은 예에서는 Hello는 package progfun.examples안에 포함이 된다. 따라서 Hello를 참조할 경우 progfun.examples.Hello로 접근해야한다. 예를 드어, Hello 프로그램을 돌릴 경우,

> scala progfun.examples.Hello

위와 같이 하여 실행할 수 있다.

Import하는 방법은 아래와 같이 몇가지 형태가 있다.

import week3.Rational            // imports just Rational  
import week3.{Rational, Hello}    // imports both Rational and Hello  
import week3._                    // imports everything in package week3  

마지막의 _는 wildcard로 week3에 포함된 모든 package 또는 object를 import하게 된다.

Automatic Imports

위와 달리 몇가지 package 및 object는 자동으로 import된다.

  • All members of package scala
  • All members of package java.lang
  • All members of the singleton object scala.Prefef

따라서 실제로 우리가 이전까지 봤던 예약어들은 실제 아래와 같이 위 package에서 import되어 사용되어 진 것이다.

Int                        scala.Int  
Boolean                    scala.Boolean  
Object                    java.lang.Object  
require                    scala.Predef.require  
assert                    scala.Predef.assert  

Scaladoc

Standard Scala library를 조회할 수 있는 사이트가 있다.

Scaladoc

Traits

Scaladoc을 탐색하다보면 trait가 나온다. Scala 뿐만 아니라 JAVA에서 class는 하나의 superclass만 가질 수 있다(Single Inheritance). 하지만, 만약 class가 여러 supertype이나 code를 상속받고 싶을 때는 어떻게 할까? 여기서 trait을 사용한다. abstract class와 동일하게 선언되어 abstract class 대신 trait를 사용하면 된다.

trait Planar {  
    def height: Int
    def width: Int
    def surface = height * width
}

모든 class와 object가 하나의 class만 상속(inherit)받을 수 있지만, trait는 여러개를 받을 수 있다.

class Square extends Shape with Planar with Movable ...  

Trait는 언뜻보기에 JAVA의 interface와 닮았지만, 실제로 훨씬 powerful하다. 왜냐하면 trait는 field와 구체적인 method를 가질 수 있기 때문이다. 하지만 trait는 parameter를 가질 수는 없다. parameter는 오지 class만 가능하다.

Scala's Class Hierarchy

Scala's Class Hierarchy

위는 Scala의 Hierarchy Diagram이다. scala.Any를 기점으로 둘로 나뉘는 등 Hierarchy 구조를 확인 할 수 있다.

여기서 우리는 top type으로 다음을 확인 할 수 있다.

  • Any - the base type of all types Methods: '==', '!=', 'equals', 'hashCode', 'toString'
  • AnyRef - The base type of all reference types; Alias of 'java.lang.Object'
  • AnyVal - The base type of all primitive types

The Nothing Type

Nothing 은 모든 Scala의 type hierarchy 중 바닥에 있다. 즉 다른 모든 type의 subtype이라고 말할 수 있다. Nothing은 값을 가지고 있지 않다. 그럼 왜 유용할까?

  • To signal abnormal termination
  • As an element type of empty collections (ex. Set[Nothing])

Exceptions

JAVA의 exception과 비슷하게 Scala도 exception을 다룬다.

throw Exc  

위에서 Exc는 Nothing을 리턴하게 된다. 결과적으로 아무것도 돌려주지 않기 때문이다.

The Null Type

또한, 모든 reference class type은 값으로 null을 가진다. 즉, Object를 상속받은 모든 class의 subtype이다.

val x = null  
val y: String = null  
val z: Int = null // error: type mismatch  

3.3 Polymorphism

Cons-Lists

Cons-Lists는 immutable linked list로 많은 functional language에서 기본 data structure로 소개된다. Cons-List는 두가지 block으로 구성되어 있다.

  • Nil - the empty list
  • Cons - a cell containing an element and the remainder of the list

그럼 이것을 scala로 구현해보자.

trait IntList ...  
class Cons(val head: Int, val tail: IntList) extends IntList ...  
class Nil extends IntList ...  

위와 같을 경우 list는 아래 중 하나 이다.

  • an empty list new Nil
  • a list new Cons(x, xs) consisting of a head element x and a tail list xs

Value Parameters

여기서 Cons에 요약되어 있는 (val head: Int, val tail: IntList)를 보자. 이것은 실제 아래와 동일하지만, 생략되어 있는 상태라고 보면된다. (Parameter 정의시 val 추가)

class Cons(_head: Int, _tail: IntList) extends IntList {  
    val head = _head
    val tail = _tail
}

동시에 _head와 _tail은 사용되지 않는 이름이다.

Type Parameters

하지만 Int만을 대상으로 list를 한정짓는 불편함이 있을 수 있다. 예를 들어 Double 또는 다른 type element에 대해서도 동일한 형태를 가질 수 있다. 따라서 아래와 같이 generalize할 수 있다.

trait List[T]  
calss Cons[T](val head: T, val tail: List[T]) extends List[T]  
class Nil[T] extends List[T]  

위와 같이 Type parameter 대괄호([])를 통해 정의할 수 있다.

위를 종합하여 아래와 같이 구현할 수 있다.

trait List[T] {  
    def isEmpty: Boolean
    def head: T
    def tail: List[T]
}

class Cons[T](val head: T, val tail: List[T]) extends List[T] {  
    def isEmpty = false
}

class Nil[T] extends List[T] {  
    def isEmpty = true
    def head = throw new NoSuchElementException("Nil.head")
    def tail = throw new NoSuchElementException("Nil.tail")
}

Generic Functions

class와 동일하게 function도 type parameter를 가질 수 있다. 예를들어 아래와 같이 단일 element를 가진 list를 생성하는 함수를 만들 수 있다.

def singleton[T](elem: T) = new Cons[T](elem, new Nil[T])

singleton[Int](1)  
singleton[Boolean](true)  

Type Inference

사실, Scala compiler는 위와 같은 상황에서 type을 추론(inference)할 수 있어 위 처럼 type을 명시할 필요없다. 따라서 아래와 같이 사용해도 무관하다. compiler는 가장 최적의 type을 자동 매칭하게된다.

singleton(1)  
singleton(true)  

이럴 수 있는 이유는 Scala에서는 Type parameter가 evaluation에 영향을 주지 않기 때문이다. 따라서 우리는 type parameter와 type arguments가 evaluation하기 이전에 지워진다고 가정 할 수 있다. 이걸 type erasure라고 한다. type erasure는 JAVA, Scala, Haskell, ML, Ocaml 등에서 적용된다. 반면, C++, C#, F# 등 언어에서는 run time까지도 type parameter가 유지된다.

Polymorphism

Polymorphism은 뜻 그대로 여러가지 형태를 의미하는데, function type이 "여러 형태"를 가진다는걸 의미한다.

  • the function can be applied to arguments of many types
  • the type can have instances of many types

여기서 polymorphism의 두가지 원칙 형태가 있는데 다음과 같다.

  • subtyping: instances of a subclass can be passed to a based class
  • generics: instances of a function or class are created by type parameterization

Example

nth라는 함수를 만들어보자. nthnlist를 입력받는데, 입력받은 listn번째 element를 돌려주면된다. (이전에 구현한 list를 이용한다.)

def nth[T](n: Int, xs: List[T]): T =  
    if (xs.isEmpty) throw new IndexOutOfBoundsException
    else if (n == 0) xs.head
    else nth(n - 1, xs.tail)

val list = new Cons(1, new Cons(2, new Cons(3, new Nil)))

nth(2, list) // 3  
nth(-1, list) // IndexOutOfBoundsException  
nth(4, list) // IndexOutOfBoundsException  

Reference

comments powered by Disqus