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이다.
정리하면, Empty
와 NonEmpty
는 둘다 IntSet
을 extend하고 있다. 따라서, IntSet
type을 conform하는 것이라고 볼 수 있다.
- an object of type
Empty
orNonEmpty
can be used wherever an object of typeIntSet
is required.
용어를 좀 더 정리하자면, IntSet
을 Empty
와 NonEmpty
의 superclass라고 이야기하고, 반대로 Empty
와 NonEmpty
를 IntSet
의 subclass라고 이야기한다. Scala에서는 다른 class를 extend할 수 있는데, 만약 superclass가 주어지지않으면 JAVA Package의 Object
class를 자동 extend하게 된다. (toString 등 Method를 가지고 있는...) 또한, direct 또는 indirect로 class C의 superclass를 접근한다면 그걸 base class라고 한다. 여기서는 NonEmpty
의 base class가 IntSet
와 Object
이다.
앞선 예제는 abstract function 인 IntSet
의 contains
및 incl
를 다른 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를 조회할 수 있는 사이트가 있다.
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의 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 listCons
- 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 ahead
elementx
and atail
listxs
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
라는 함수를 만들어보자. nth
는 n
과 list
를 입력받는데, 입력받은 list
의 n
번째 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