Programming in scala 정리 (1)

Scala 3를 익히기 위해선, 먼저 Scala 2를 알아야 할 것 같다. 관련 책 한권을 정리해본다.

A Scalable Language

scala를 써야하는 7가지 이유

  1. Scala is scalable
    • 확장이 용이, 커스텀 타입을 빌트인 처럼 쓸 수 있다.
    • 연산자 함수 등을 커스텀해, DSL을 만들기에도 용이하다.
  2. Scala is OOP
    • Pure OOP = 모든 요소는 객체(값, 함수, 타입 포함)
    • Powerful composition feature = trait
  3. Scala is FP
    • 함수을 first-class value로 취급한다.
    • 값의 변경 대신, 입출력의 매핑에 집중한다.
    • 사이드 이펙트 없음 = referentially transparent
  4. Scala is compatible
    • interoperability with Java
    • implicit conversion을 통한 Java 라이브러리 강화
  5. Scala is concise
    • less boilerplate, easy to read and understand
    • FP, 확장 가능한 라이브러리로 더 짧게 줄일 수 있음
  6. Scala is high-level
    • 메소드를 전달함으로서 제어-추상화 가능
  7. Scala is staticically typed
    • 정적 타입의 장점은 살리고, 타입추정으로 verbosity를 줄임

First Steps in Scala

  • 변수 선언
    • 변수 선언시 val, var 사용, 타입 추정됨
    • val = immutable
    • var = mutable
    • def = lazy (사실은 함수임)
  • 함수 선언
    • def 로 시작
    • 변수, 파라미터와 타입, 리턴타입 모두 Kotlin과 동일
    • 단 body와 function 선언 사이에 =이 들어가야함
    • 한 줄 일 경우 블록 필요 없음
    • 여러 줄 일 경우 {} 필요, scala3에서는 indent로 대체됨
    • return은 생략 가능, 명식적으로 쓸 수도 있음
    • return을 쓸 경우는 리턴 타입을 명시해야함
  • 반복, 조건
    • while은 타 언어와 사용법 동일
    • if 역시 동일, 추가로 expression으로 사용 가능
    • forgenerator 포맷으로 사용, immutable yield만 가능
1
2
3
4
5
def max(x: Int, y: Int): Int = {
  if (x > y) x else y
}

for (arg <- args) println(arg)

Next Steps in Scala

Generics, Array, apply, update

  • 제너릭은 대괄호로 표현
    • 타입은 명시하지 않아도 자동으로 추정됨
  • apply
    • 객체에 대해서 메소드 이름 없이 호출하면 apply가 호출됨
    • 인스턴스에 대해서 호출하면, 멤버 apply가 호출됨
    • 클래스에 대해서 호출하면, companion object의 apply가 호출됨
  • update
    • 특정 객체에 대한 대입은 update로 치환됨
    • apply와 마찬가지로 클래스에 대해서 호출하면, 컴패니언의 update가 호출됨
  • infix 호출
    • 인자가 하나인 메소드는 별다른 처리 없이 infix로 쓸 수 있음
1
2
3
4
5
6
7
8
9
10
11
// generic type inference
Array(1,2) == Array[Int](1,2)
Array[Int].apply(1,2,3) // invalid

// calling with non-function is equal to call apply
notFunction(123)
notFunction.apply(123)

// equal can be replaced by update
greetStrings(0) = "Hello"
greetStrings.update(0, "Hello")

List

  • Array와 다른점
    • immutable (Array는 mutable)
    • 값 비교 가능 (Array는 reference 비교함)
    • 연결 리스트이므로, random access에 선형시간이 걸림
  • 각 노드가 head와 tail을 가지고 이어진 연결리스트
    • headtail은 상수시간에 찾을 수 있다.
  • 연산자 메서드 존재
    • elem :: List 리스트 가장 앞에 원소를 추가, $O(1)$
    • List :+ elem 리스트 뒤에 원소를 추가, $O(N)$
    • List ::: List 두 리스트를 이어붙임, $O(N)$
  • Nil은 빈 리스트

Tuple

  • Array처럼 특별취급 되는 자료구조
  • 괄호로 묶어서 선언 가능
  • 실제로는 TupleN 클래스의 인스턴스
  • decomposed binding 가능
  • _1, _2 등으로 접근 가능

Set, Map

  • mutable, immutable 패키지가 나눠져있음, 양자택일 해야함
    • scala.collection.immutable.HashSet
    • scala.collection.mutable.HashSet
  • 기본은 immutable임
  • 원소 추가
    • +=는 mutable에서만 쓸 수 있음
    • +는 둘다 쓸수 있음, immutable에서는 새 Set을 만들어서 리턴
  • Map의 경우는 -> 메서드로 튜플을 만들어 선언, 그냥 튜플 넣어도 됨
    • Map((1,'a'), (2,'b')) == Map(1->'a', 2->'b')
  • Map, Set 모두 List 처럼 동일성 비교 가능

Scala style

  • 기본적으로는 FP와 OOP 모두 가능하다.
  • 필요에 따라서 적합한걸 선택한다.
  • 다만 양립 가능한 상황에서는, 최대한 FP를 선택한다.
  • FP를 선택한다는 것은, 최대한 var없이 코드를 작성하는 것이다.

Classes and Objects

Class and members

  • 클래스 기본
    • class 선언은 자바의 그것과 동일
    • scala3 부터는 파이썬 스타일로 :과 indent로 블럭 지정 가능
    • 인스턴스 화 할때는 new 사용
    • 멤버로 val, var, def를 가질 수 있음
  • newapply 비교
    • new와 함께 호출시 명시적 생성자 호출
    • 그냥 호출시 companion object의 apply가 호출됨
    • scala3 apply가 없으면, 자동으로 applynew 호출됨
  • Unit 타입
    • 모든 타입객체는 Unit으로 변환 가능
    • Any타입 변수에 값을 대입하는것과는 다름
    • Unit으로 변환되면서 데이터가 사라짐
  • 세미콜로는 생략해도 됨

Singleton Objects

  • object 로 선언, 클래스랑 같은 문법
    • 단 파라미터는 사용 불가능, 정적 속성을 가지므로 당연함
    • 다른 클래스나, trait를 상속 가능
    • 타입으로서는 사용 못함, 즉 부모로서 상속해주는것은 불가능
  • Companion object
    • 같은 파일, 같은 이름의 classobject가 있으면 companion object임
    • 각자의 private 멤버에 접근 가능
    • classobject의 private 멤버에 제한없이 접근 가능(마치 정적 멤버처럼)
    • object인스턴스의 private 멤버에 접근 가능

Scala Application

  • 진입점으로 main 함수를 가진 object가 존재해야함
  • 간단하게 @main으로 대체 가능
  • 또는 object XX extends Application 안에 수행할 코드를 넣을 수도 있음
    • JVM 최적화를 방해할 가능성 있음, 쓰지말 것
  • script로 쓰려면 마지막에 expression이 나와야 함

Basic Types and Operations

타입 시스템

  • Java와 매칭되는 타입 시스템
  • Byte, Short, Int, Long, Float, Double
  • Char, String, Boolean
  • Java.lang.Integer ~ scala.Int
  • int 로 쓰면 Int와 같지만 권장되지 않음
  • Scala 컴파일러가 primitive로 쓰도록 바이트코드 컴파일 해줌

리터럴

  • 8진수 존재함, C++이나 Java랑 같음
  • Float은 1.0F, Long은 123L
  • 작은따옴표는 문자, 큰따옴표는 문자열
  • 문자 작성시 \101, \u0041로 Ascii, Uni-code 표현 가능
  • 여러줄 문자열 """, 여백 제거(stripMargin)는 |기준으로 자름
  • Symbol, 동일성이 보장되는 알파벳으로만 구성된 문자열. 성능 상 이점이 있음
    • 'abc 처럼 리터럴을 사용했으나, Deprecated 됨, Symbol("abc")로 쓰자.

연산자

  • 연산자는 메서드다
    • 타 언어에서 특별히 정의되었던 연산자들 모두 메서드다.
    • 메소드 이름의 자유도가 높다.
    • 그러므로 연산자 이름에도 자유도가 높다.
  • Identifier로 쓸 수 있는 문자열이 엄청 많다
    • 둘 이상의 연속된 특수문자도 가능
    • 특수문자와 알파벳, 숫자의 조합도 가능
    • 다음 섹션에서 자세히 설명
  • 메소드로 선언한 것들은 연산자처럼 쓸 수 있다.
    • 특히 인자를 한개만 받으면 infix 표기로 쓸 수 있다.
    • 인자가 두개 이상이면, infix로 쓰되, 뒤쪽을 괄호로 묶어야 한다.
  • 단향 연산자
    • 인자가 없으면, postfix unary 연산자로 쓸 수 있다.
    • 전치는 +-!~ 만 허용, 단 unary_+ 처럼 선언 필요
  • 우선순위
    • 첫 글자로 우선순위 결정됨
    • 그 우선순위 자체는 다른 언어와 거의 유사함
    • 다른특수문자들 */% +- : =! <> & ^ | 알파벳 대입연산 콤마
    • 예외1: =로 끝나는 대입 연산자들은 첫 문자에 상관없음
    • 예외2: :로 끝나는 연산자는 right to left 로 결합함

Functional Objects

  • Functional object
    • 이 책에서만 쓰는 용어인듯
    • Mutable한 상태를 가지지 않는 객체를 뜻함
  • 예제를 통해서 객체 생성 접근해보기
    • 사칙연산 가능한 유리수 객체 만들기

Constructing Rational number

  • Primary Constructor
    • Body가 없으면 block 없이 선언해도 됨
    • body는 primary 생성자 역할을 겸함 (Kotlin과 완전 동일)
    • 함수처럼 파라미터를 받을 수 있음
    • require 함수 assert, 위반시 IllegalArgumentException
    • this = self reference
    • val, var을 블록 내에 선언해서 필드로 사용
    • val, var을 붙이면 멤버로서 바로 사용 가능
  • Auxiliary Constructor
    • def this로 선언
    • 다른 생성자를 꼭 불러야함, 최종적으로는 primary가 불리게 되어 있음
    • Aux-const는 super class의 생성자를 호출할 수 없음 (Primary만 가능)
  • 필드 가시성
    • 기본적으로 public
    • private 으로 선언해서 사용 가능
  • String interpolation
    • s"$numer/$denom"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// class parameter without body
class Rational(n: Int, d: Int)

// class parameter with field, require, method, aux const
class Rational(n: Int, d: Int) {
  require(d != 0)
  private val g = gcd(n.abs, d.abs)
  val numer = n / g
  val denom = d / g

  def this(n: Int) = this(n, 1) // auxiliary constructor

  override def toString = s"$numer/$denom"

  def add(that: Rational): Rational = {
    new Rational(
      numer * that.denom + that.numer * denom,
      denom * that.denom
    )
  }

  private def gcd(a: Int, b: Int): Int =
    if (b == 0) a else gcd(b, a%b)
  
  def * (that: Rational) =
    new Rational(this.numer * that.numer, this.denom * that.denom)
}

Identifier

  • 4가지 종류의 Identifier
    1. alphanumeric identifier
    • letter 혹은 _ 로 시작해야함
    • 이후 letter, digit, _ 가능
    • $ 는 letter 취급이지만, 예약문자 이기때문에 못씀
    • 카멜 케이스 권장됨
    • _는 주의해서 쓸것 뒤쪽 특수문자를 붙여서 식별자로 인식하게함
    • 상수의 경우는 첫문자 대문자 카멜 케이스를 쓰는걸 권장 2. operator identifier
    • letter, digit, (), [], {}, "'`_.;, 제외
    • 나머지 모든 ASCII 코드와, Sm/So 유니코드 만으로 구성됨
    • 여러개의 특수문자를 연결해서 연산자처럼 사용 가능
    • 자바 컴파일 시 $를 사용해서 표현됨 :- = $colon$minus 3. mixed identifier
    • 1과 2 모두 사용된 경우
    • 일반적으로는 안쓰이고, unary_, 대입연산자, properties를 위해 사용됨 4. literal identifier
    • back tick ` 으로 묶어서 식별자로 사용
    • 대부분의 문자가 가능하고, 예약어도 가능
    • 자바의 함수명이 예약어와 겹칠 때 유용
1
2
val name_: Int = 1 // error "name_:" 가 식별자로 인식됨
val name_ : Int = 1 // ok 이지만 권장하지 않음

오버로딩과 암시적 변환

  • Rational을 만들고 Int와 곱셈이 되게 하고 싶음
    • r1 * r2: * (r1: Rational, r2: Rational) 메서드를 만들었음
    • r1 * 2: * (r1: Rational, n: Int) 메서드 오버로딩 해야함
    • 2 * r1: 암시적 변환 필요
  • 암시적 변환
    • implict과 적절한 변환 함수를 선언하면, 자동으로 변환해줌
    • 단 스칼라 3 및 최신 스칼라 2에서는 권장되지 않는 방법임
    • given을 쓰는게 바람직함
    • 명시적이지 않은 코드를 양산하므로, 항상 사용에 주의할것
    • 암시적 변환은 메소드 호출 시에, 맞는 메소드가 없을때 일어남
1
2
3
4
5
6
7
8
9
// Not recommended now, using given / using
implicit def intToRational(x: Int) = new Rational(x)

// scala3 recommended
given Conversion[Int, Rational] = new Rational(_)

// scala3 recommended: full version
given Conversion[Int, Rational] with
  def apply(n: Int): Rational = new Rational(n)

Built-in Control Structures

  • 간결함을 위해서 최소한의 제어 구문만 있음
  • 대부분 값, 표현으로 쓰일 수 있음

if, while

  • 둘다 다른 언어들과 거의 같음
  • if는 값으로 쓰일 수 있음
  • while, do~while 모두 있음, 단 표현으로 못씀
    • 대신 Unit 리턴함
  • 대입 연산은 Unit을 반환하므로, C스타일로 구현할 수 없음
  • 그냥 while은 쓰지 않는걸 지향함
1
while ((line = readLine()) != "") {} // 에러

for statement

  • 다른 언어와 비교할 때, header가 복잡함 + expression 사용 가능
    • generator 형식 쓸 수 있고
    • generator 여러개를 쓸 수도 있고
    • fitler를 쓰거나 여러개를 쓸 수 있고
    • 루프내에서 유효한 변수를 선언할 수도 있음
  • generator 문법
    • i <- List(1,2,3): 1,2,3
    • i <- Array(1,2,3): 1,2,3
    • i <- Map(1 to 1, 2 to 4): Pair(1,1), Pair(2,4)
    • (k, v) <- Map(1 to 1, 2 to 4): key, value 각각 사용
    • i <- 1 to 3: 1,2,3
    • i <- 1 until 4: 1,2,3
  • if로 filter를 추가할 수 있음
    • 아예 body안으로 보내지 않고
    • expression으로 쓸 경우 Unit으로 사용하지도 않음
    • 여러개의 if를 사용할 수도 있음
  • 여러개의 generator를 추가해 다중 루프로 사용 가능
    • 이 때는 ()로 묶고 ;로 두 generator를 나누거나
    • {}로 묶고 여러 줄로 나눠서 쓸 수 있음.
  • header내에 변수 선언 가능
    • val없이 선언하며, val로 간주됨
    • 하나의 iteration 내에서 유효하며, 매번 다시 계산됨
    • expression으로 사용할때나, 필터가 복잡할 때 유용함
  • yield 키워드를 통해서 expression으로 사용 가능
    • yield 뒤에 블록이 오는게 가능, 단 마지막에 리턴값을 적어줘야함
    • yield의 결과물은 무조건 가장 첫 generator의 타입을 따라감
    • 단 tuple이 결과물이고, 첫 generator 타입이 Map인 경우는 Map으로 만들어줌
    • Map일때 Pair가 아닌 결과물이 나오면 List로 만들어줌
    • TODO 특정 조건이 있어야 타입을 따라가게 할 수 있는것 같은데 정확하게 모르겠음
  • yield는 Iterable을 보존함
    • 여러번의 for loop을 거쳐도 iterator는 값을 산출하지 않음
    • fitler를 거치거나, 몇가지 기본 연산들을 거쳐도 동일함
    • 최종적으로 side-effect가 발현될 때 값을 산출함
    • TODO Iterable의 원리 이해하기
  • scala3에서는 for..do, for..yield로 블록 없이 쓸 수 있음
1
2
3
4
val f = Source.fromFile("build.sbt")    // f = <iterator>
val y = for c <- f if c != 'z' yield c  // y = <iterator>
val z = for c <- y yield s"$c$c"        // z = <iterator>
for s <- z do print(s)                  // printed!

try-catch-finally

  • throwThrowable 객체를 만들어 던짐
    • throw 문장은 Noting을 값으로 가짐
    • Nothing은 모든 타입의 자녀타입임, 즉 어떤 변수로도 대입할 수 있음
  • try는 Java와 완전 같음
    • 단 마지막에 오는 표현식으로 값을 부여할 수 있음
  • catch는 기본적으로 패턴 매칭을 쓰도록 구성됨
    • 무조건 case 구문의 나열로 구현해야함
    • Java처럼 throw할 예외를 명시할 필요 없음
    • 마찬가지로 마지막에 오는 표현식으로 값을 부여할 수 있음
  • finally도 java와 완전 같음
    • finally는 값을 부여할 수 없음 (무시됨)
    • finally가 주로 side effect를 만드는 속성을 가지기 때문
1
2
def f(): Int = try { return 1 } finally { return 2 } // 2
def f(): Int = try { 1 } finally { 2 } // 1

match expression

  • switch와 유사하나, 훨씬 강력함
    • expression으로 쓸 수 있고
    • filter를 붙일 수도 있고
    • pattern matching으로도 쓸 수 있음
    • 상수뿐만 아니라 온갖 오브젝트와 변수를 집어넣는게 가능
    • break 안써도 되고, 암시적으로 break가 작동
  • _ 를 else로 사용

break와 continue가 없음

  • FP적인 특성을 더 잘 살리기 위해 break, continue가 없음
  • tail-call등은 실제로 loop으로 컴파일됨
  • 그러므로 break, continue 대신 if문 분기나, 재귀를 써서 해결을 권장

variable scope

  • 블록단위의 스코프를 가짐
  • 블록 내에서 변수를 재정의 할 수 있음(like C++, not-like Java)
    • 인터프리터 내에서는 항상 재정의 가능
  • 클래스 변수의 접근 제한자 별 스코프는 아래와 같음
Modifier Class Companion Subclass Package World
no modifier O O O O O
protected O O O X X
private O O X X X
top-level private O O O O X

Function and Closures

  • 이 장에서는 함수의 종류에 대해서 다룬다.
  • 메소드는 당연히 사용 가능하다.
  • 로컬 함수 정의도 가능하다.
  • 클로저도 존재하며, Javascript와 똑같이 작동한다.

Fisrt-class functions

  • 람다 or 함수 리터럴을 뜻하는 말
  • function value vs literal
    • function literal: 소스코드
    • function value: 런타임 오브젝트
  • 기본 문법
    • (x: Int) => x+1 과 같이 쓴다.
    • 일반 함수와 달리 return 키워드를 사용할 수 없다.
    • 리턴 타입을 명시하는 것을 허용하지 않는다.
    • 블록으로 묶어 여러줄 가능
  • 람다(함수 리터럴)가 다른 함수에 인자로 주어질 때
    • 람다의 인자는 타입을 생략 가능
    • 람다의 인자가 하나일 경우는 괄호도 생략 가능

Placeholder Syntax

  • 람다를 표현하는 또다른 방법
  • 각 인자들이 딱 한번만 등장하는 간단한 함수에서 편하다.
  • _에 각 인자들이 순서대로 매핑된다.
  • (_: Int) 처럼 써서 타입을 나타낼 수도 있다.
1
2
3
4
5
6
7
8
val sum1 = (a:Int, b:Int) => a + b
val sum2 = (_:Int) + (_:Int)

def outer(f: (Int, Int) => Int) = f(3,4)
outer(sum1) // 7
outer(sum2) // 7
outer((a,b) => a+b) // 7
outer(_ + _) // 7

Partially applied function

  • functionName _로 사용
    • 일반 함수의 모든 인자를 _로 넣는 것을 의미
    • 자체로서 람다식이 됨
    • 즉 일반 함수를 function literal로 바꿔줌
  • sum(5, _)로 사용
    • 특정 함수값을 고정값으로 부여하는 새로운 함수를 생성함
    • Scala에는 디폴트 인자값이 그래서 필요 없구나!
    • 단 직접 호출시에는 괄호의 결합순서에 따라 오류가 발생
  • 일반 함수를 인자로 넘길수 있다.
    • 원칙적으로 일반 함수는 값이 아니므로 인자로 넘길 수 없음
    • 단 타입 시그네쳐가 일치하는 경우에는 _을 생략 가능
    • f(println)을 써도 f(println _)으로 컴파일러가 인지함
1
2
3
4
5
val x = sum // error
val x = sum _ // ok

sum(5, _)(3) // error
(sum(5, _))(3) // ok

Clousure

  • 자신을 감싸고 있는 함수 스코프의 변수들을 가져다 쓸 수 있음
  • 메소드, 로컬함수, 리터럴 모두 같은 규칙을 가지고 작동한다.
  • 변수 자체를 캡쳐하므로 var의 변화도 인지한다.
  • scope이 종료되어도 변수는 생성될 당시의 값을 유지한다.
1
2
3
4
def makeIncreaser(more: Int) = (x: Int) => x + more

makeIncreaser(1)(10) // 11
makeIncreaser(9999)(10) // 10009

Repeated Parameter

  • 가변인자와 똑같다. 타입 Int*로 받으면, Array[Int]로 인식된다.
  • Array를 echo(arr: _*) 같이 _*를 붙여 넘기면, 가변인자로 인식

Tail recursion

  • 함수의 리턴이 온전히 자기 자신의 재귀 호출인 경우
    • 이를 loop으로 최적화해준다.
    • 여러 함수가 번갈아가변서 불리면 안되고
    • 같은 함수라도, 다른 변수에 대입되어있는 것을 호출한 경우도 안됨
  • 재귀를 더 부담없게 쓸 수 있게 만들어줌
  • @tailrec 으로 컴파일러에게 힌트를 줄 수 있음
    • Tail recursion이 아닐경우 오류를 발생시켜 주므로 유용함

Control Abstraction

Reducing code duplication

  • HOC: Higher order function
    • 반복되는 동작을 인수화해서 제거
    • _로 반복되지 않는 부분을 치환한 뒤에 동작을 분리한다.

Currying

  • 함수를 인자를 기준으로, 함수열로 쪼갠다.
  • 함수를 중첩해서 선언할 필요 없이 괄호만 나열해주면 된다.
  • 한개 인자만 존재하면 사용할 수 있는 기능을 사용하게 해줌
    • 예를들여, 단일 인자일대 curly brace로 블록을 인가할 수 있다.
1
2
3
4
def plainOldSum(x: Int, y: Int) = x + y
def curriedSum(x: Int)(y: Int) = x + y

curriedSum(1) {2} // ok

사용자 정의 컨트롤

  • 한개의 인자만 있다면, 인자를 curly braces로 대체할 수 있음
  • 즉 사용자 정의의 컨트롤을 만들 수 있음
  • loan pattern 과 같이 자원을 회수해야하는 경우 유용함
    • 예를들면 connection 혹은 file, close를 불러야함
    • 단순히 curly-braces로 인가하게 되면 실행 순서에서 문제가 생김
1
2
3
4
5
def acquire(lock: Lock)()
acquire(lock) {
  // do something
  // 이 부분이 먼저 실행되어 버림
}

call-by-name

  • 위의 loan pattern을 if 처럼 사용하고 싶다면
  • 기존대로라면 값을 산출한 후, 함수에 인가함
    • 인가를 먼저 하고, 나중에 값을 산출해야 if처럼 동작함
  • 만약, 인가할 대상이 람다였다면
    • 어차피 함수이므로 값이 산출되지 않는다.
    • 단 의미없는 verbosity가 늘어남
  • call-by-name이라면
    • 순서가 제대로 동작하고
    • 코드도 call-by-value와 똑같음
1
2
3
4
5
6
7
8
9
10
11
// body 먼저 실행, 실행순서가 의도치 않음
def test(body: Unit) = { print(1); body }
test { print(2) } // output: 21

// lambda 인가, 실행 순서는 의도대로, verbose 함
def test(body: () => Unit) = { print(1); body() }
test { () => print(2) } // output: 12

// call-by-name, OK
def test(body: => Unit) = { print(1); body }
test { print(2) } // output: 12

Composition and Inherithance

  • Composition: 다른 오브젝트의 레퍼런스를 소유하며 결합
  • Inherithance: 부모/자녀 클래스 관계를 이룸

Abstract classes

  • abstract class로 클래스를 선언
  • 당연히 인스턴스 화 불가능
  • 한개 이상의 구현부가 없는 멤버를 가질 수 있음
    • Java, C++과 같은 컨셉
    • val, def, var 모두 구현부가 없을 수 있음
  • 왜 사용하나?
    • 일반적으로는 trait를 사용한다.
    • trait와 다르게, 주 생성자의 인자를 강제할 수 있음
    • trait와 다르게, Java에서 사용 가능
  • 단점: 다중 상속 불가능
    • 다이아몬드 상속을 피할 수 없다.

parameterless methods

  • 파라미터가 없을때, ()를 생략 가능
  • 호출시에도 생략하게 됨
  • side-effect가 없음을 강조함
  • side-effect가 있으면, ()를 써주는것이 권장됨

Extending classes

  • 상속은 extends
  • 부모의 추상 메서드들을 모두 오버라이드 해야함
  • override 키워드
    • 추상 메서드를 오버라이드 해 구현할때는 override 안써도 됨
    • Concrete 메서드를 오버라이드 할때는 반드시 override 불러야 함
  • 상속시에는 주 생성자를 반드시 extends 뒤에서 불러야 함