Programming in Scala
1장
2장
3장
List
- list의 제일 앞에 원소 추가하는건 O(1), 뒤에 추가하는건 O(n)
- 그러므로 앞에 추가하는게 좋음. 만약에 뒤에 추가하고 싶다면 우선 뒤집고 앞에 추가한 다음에 다시 뒤집거나
ListBuffer
사용
Tuple
- tuple은
tuple._i
같은 형태로 원소 호출
- i는 1부터 시작 (하스켈, ML 등 정적 타입 언어의 영향)
- list와 tuple의 차이점은 여러 자료형을 한번에 담을 수 있느냐의 차이
Map
함수형 스타일
- var보단 val을 더 권장
- side effect를 최소화
- 만약에 Unit을 반환하는 함수가 있다면 주의할 것
4장
Class, Field, Method
Semicolon
- 한 줄을 여러 개로 나누려면 사용 ex)
val s = "hello"; println(s)
- 기본적으론 쓸 필요 없음
- 그런데 연산자가 있을 때 오류가 나올 수 있음
x
+ y
- 이럴 땐 괄호로 감싸도 되지만 일반적으론 연산자를 줄 맨 끝에 붙이는걸 추천함
x +
y
Singleton object
- 자바의 static을 대체
- 같은 이름의 class와 object가 동시에 존재할 때, 이들을 각각 companion class, companion object라고 부르며 언제나 같은 파일에 위치해야 함
- companion class, companion object은 서로의 private field에 접근 가능
- singleton object는 패러미터를 받을 수 없음
- companion class가 없는 singleton object는 standalone object라고 부름
5장
객체 동일성
- 자바랑 다르게
==
로 객체의 비교도 가능
- primitive type은 자바와 동일하게 동작
- reference type에서 자바랑 다르게 동작
- 우선 null 체크
- 그 다음
equals
호출
- 만약에 인스턴스 자체를 비교하고자 한다면
eq
, nq
사용
6장
Immutable Object
장점
- 코드 흐름의 추론이 쉬움
- 객체를 전달하는데 자유로움. 변경이 가능하다면 기존 값을 미리 복사해놓는 등의 방어조치가 필요
- 병렬 프로그래밍 환경에서 동시성 보장
- HashSet 등에서 상태 변경으로 키값도 같이 변하는 불상사를 막을 수 있음
단점
- 간단한 값 하나를 바꾸기 위해서 거대한 객체 그래프를 통째로 복사해야되는 케이스가 생길 수 있음
- 성능상 병목 가능
- 때문에 변경가능한 객체를 같이 제공해주는게 좋음
클래스 패러미터 조건 검사
필드 추가
- 클래스 패러미터에
val
, var
둘 다 붙어있지 않는 경우에는 해당 스코프에서만 참조가능한 private 변수가 됨
보조 생성자
def this(...) = {...}
- 모든 보조 생성자는 다른
this(...)
를 호출하며 시작
- 따라서 결국 기본 생성자를 호출하게 되어있음
식별자
- 기본적으로 Camel case
- 띄워쓰기를 _로 구분할 수 있긴 한데 되도록 말길
- $로 시작할 수도 있긴 한데 스칼라 내부에서만 사용하니까 쓰지말길
- 상수는 첫글자만 대문자로 (자바의
final String SOME_VAR
이렇게 쓰는게 아니라 val SomeVar
)
연산자 식별자
- +, -, :, ?, ~, # 등 출력 가능한 아스키 문자
- 내부적으로 $를 이용하여 해체하고 적합한 자바 식별자로 재생성함
혼합 식별자
- 영어, 숫자 뒤에 밑줄이 오고 연산자 식별자가 옴
리터럴 식별자
`...`
같은 형식
- 예약어를 포함해서 모든 문자열을 쓸 수 있음
7장
- 스칼라에서 할당의 결과는 할당의 값이 아니라
Unit
- 되도록이면
while
문은 지양할 것
for
컬렉션 순회
- 제네레이터 문법:
for (element <- collection)
- Range 타입도 사용 가능 ex)
i <- 1 to 5
, i <- 1 until 6
- 물론 스칼라에선 인덱스를 바탕으로 순회하는 일은 잘 없음
필터링
- collection에 if문을 넣어서 필터링 가능 ex)
for (element <- collection if ...)
중첩 순회
루프문 도중에 변수 바인딩하기
새로운 collection 반환하기
- for 루프문 코드 블록의 중괄호 앞에
yield
선언
- ex)
for (...) yield {...}
, for () yield ...
try-catch-finally
- 자바랑 다르게 checked excetpion도 별도의 throws를 붙이거나 반드시 catch할 필요가 없음
- 결과로 값을 반환. try가 성공하면 당연히 try의 반환값이고, 만약에 도중에 예외가 발생하였다면 case 수행 결과가 반환값
- finally에서 return을 명시적으로 호출하지 않는 이상 반환값은 버려짐. finally에서 try나 catch의 결과를 바꾸지 않도록 주의해야 함
패턴 매치로 예외 처리
try {...} catch {case ex: Exception => {...} ...}
match-case
- 자바의 switch-case문과 유사
- 자바의 switch-case는 정수나 Enum만 되는 반면 모든 상수 사용 가능
- break문이 암시적으로 존재함
- 다른 표현식들과 마찬가지로 값을 반환
break, continue
- 스칼라에서는 두 구문 모두 지원하지 않음. 대신에 재귀로 코드 짜는 걸 추천
- 만약에 굳이 break를 써야겠다면
scala.util.control.Breaks
클래스를 사용할 수도 있긴 함
9장
접근 제한
private
키워드를 메소드 정의 앞에 붙이는 방법
- local function으로 만드는 방법도 있음
위치 표시자
_
혹은 _ : Type
형태로 사용
- 패러미터를 순서대로 하나씩 채워라는 의미
부분 적용 함수
myFunc _
, myFunc(1, _: Int, 2)
같은 형태로 사용
- _의 갯수만큼 패러미터를 받는 새로운 함수를 자동 생성하여 반환
- 이후 패러미터를 넣어 함수를 호출하면 원본 함수를 재호출
- 패러미터가 함수를 받는다면 _를 생략하는 것도 가능
클로져
- 주어진 함수 리터럴로부터 실행 시점에 만들어낸 객체
- 함수 본문에 존재하는 모든 free variable binding을 capturing 하여 더 이상 free varibale가 남지 않게 close 한다하여 붙여진 이름
- 함수 스코프 밖에 있는 변수를 free variable, 스코프 내에 있는 변수를 bound variable라고 부름
- 함수 리터럴에 free variable가 없다면 closed term, 있다면 open term 이라고 부름
- 스칼라의 클로져는 변수의 값이 아니라 변수 그 자체를 포획
- free variable의 값을 수정하면 스코프에 상관없이 모두 수정됨
- 기본적으로 method parameter은 stack 공간에 존재하지만 클로져가 포획하게 되면 자동으로 heap 공간으로 재배치
반복 패러미터
- 마지막 패러미터에 *를 붙여서 사용 가능
- 실제로는
Array[Type]
이 됨
- 배열을 전달하고 싶으면
_*
의 형태로 사용
패러미터에 이름 붙이기
myFunc(param1 = 1, param2 = 2)
와 같은 형태로 사용
- 순서를 바꾸어도 문제 없음
_
와 혼용 가능. 이 경우 _
를 먼저 넣어야함
디폴트 패러미터
def myFunc(param = "default") ...
형태로 사용
꼬리 재귀
9장
커링
- loan pattern: 함수를 제공하는 쪽에서 resource의 open, close 등 라이프사이클은 관리하고, 사용할 수 있게끔 빌려주는 패턴
- 스칼라에서는 패러미터가 한 개인 함수를 호출할 때 중괄호로 감싸서 호출할 수 있음.
- 이를 이용하여 사용자가 마치 기본 문법인 것처럼 사용하는 것이 가능
def customFunc(op: Int => Int) : Unit = {
}
customFunc { x: Int => {
}}
이름에 의한 호출 사용
()
없이 바로 =>
사용
- ex)
=> Boolean
, => Int
- 바로 Boolean을 패러미터로 쓸 때와 차이점은 계산 시점의 차이이다
def byName(isTrue: => Boolean) {
if (isTrue) {
println("true")
} else {
println("false")
}
}
def byBoolean(isTrue: Boolean) {
if (isTrue) {
println("true")
} else {
println("false")
}
}
byName(1 > 0)
byBoolean(1 > 0)
10장
Field vs Method
class MyClass {
val asField: Int = 1 + 1
def asMethod: Int = 1 + 1
}
- 차이점은 클래스 초기화때 미리 값을 계산해 저장해둘 것인가 매 호출마다 계산할 것인가 뿐
- 미리 값을 계산해두려면 저장공간을 소모하게 됨
- 단일 접근 원칙: 필드든 메소드든 어떤 방식으로 정의하더라도 클라이언트의 코드를 수정하게끔 해서는 안 됨
- 괄호 없는 메소드를 사용할 것이라면 부수 효과도 없어야 함은 물론 변경 가능한 상태에 의존해서도 안 됨
- 클라이언트는 자신이 사용하는 필드가 말 그대로 필드인지 괄호 없는 메소드인지 굳이 구분할 필요가 없어야함
- 스칼라에서는 필드와 메소드가 같은 네임스페이스
Super class 생성자 호출
class MyClass extends SuperClass(params)
- extends에서 바로 호출하면 됨
11장
바닥에 있는 타입
Nothing
- Null이랑 비슷하지만 값이 존재하지 않는 타입을 의미
- Null과 달리 Int, Boolean등 값 타입과 호환됨
- 반환 타입이 Nothing인 것은 이 메소드가 정상적으로 값을 반환하지 않을 것이란 뜻. 즉, 비정상적인 종료를 표시
12장 - Trait
사용목적
- rich interface
- stackable modification
특징
- 일반적인 클래스에서 할 수 있는 대부분이 가능함
- 클래스 변수를 가질 수 없음 ex)
trait MyTrait(x: Int) // 컴파일 에러
- 믹스인 할 때 클래스에 따라
super
가 동적으로 바인딩됨
믹스인
- 이미 다른 클래스나 트레이트를 상속한 상황에서 추가로
with
구문과 함께 트레이트를 믹스인할 수 있음
Thin vs Rich Interface
- Thin 인터페이스는 구현하긴 쉬운데 선택지가 적어 용도에 딱 맞는걸 고르기 힘듬
- Rich 인터페이스는 선택지가 많은데 불필요한 코드가 많아져 구현하기 귀찮음
- 자바는 보통 Thin 인터페이스로 되어있음
- 트레이트를 보통 이 Rich 인터페이스로 만들면서 코드 관리는 쉽도록 사용
- 메소드를 트레이트 안에서 한번만 구현해도 됨
- 아니면 트레이트 안에 abstract method로 추가하여 구현은 떠넘겨도 됨
변경 쌓아올리기
- 트레이트의 super은 동적으로 바인딩됨
- 그에 따라 trait에서도 super을 쓰는 것이 가능
- trait에서 super을 쓰면 메소드 앞에
abstract override
를 붙여줘야 함
abstract override
메소드가 있는 트레이트는 반드시 해당 메소드의 구현이 존재하는 클래스에 믹스인되어야 함
13장
패키지
- 만약에 한 파일 안에 여러 패키지가 있어야된다면 중괄호로 감싸고 중첩시키는 것도 가능
- 이렇게 중첩시키게 되면 바깥 스코프로 바로 접근 가능
- 중괄호로 감싼게 아니라 자바처럼 한 파일에 한 패키지로 하면 자기 패키지만 보임
package com {
package navercorp {
class Naver {
}
package npush {
class Npush {
val naver = new Naver()
}
}
package session-io {
}
}
}
package com.navercorp.test {
class Test {
val naver = new Naver()
}
}
Chained package clause
- 중괄호 없이 나열하여 중첩 가능
- 코드가 오른쪽으로 밀리는게 싫을 때 사용
package com
package navercorp
package npush {
class Npush {
val naver = new Naver()
}
}
_root_
- 같은 이름을 가진 패키지가 섞여있을 때, 최상단 패키지에 접근하기 위해서 사용
package test {
class MyTest1 {
}
}
package hello
package world {
package test {
class MyTest2 {
}
}
package ex {
class Ex {
val test1 = new _root_.test.MyTest1
val test2 = new test.MyTest2
}
}
}
Import
- 모든 멤버에 접근하기 위해
._
사용
- 코드의 어느 부분에서나 import 가능
- 클래스에 국한되지 않고 일반 객체의 import도 가능
- 패키지 그 자체를 import 하는 것도 가능
import com.navercorp._
class Point(val x: Int, val y: Int) {
}
class Test {
import com.navercorp.npush._
def myMethod(point: Point) {
import point._
println(x + ", " + y)
}
}
Import Selector
- 멤버를 감추거나 이름을 바꾸기 위해 사용
import packagename.{member1, member2 => anotherName}
같은 형식으로 사용
{member => anotherName, _}
특정 멤버는 이름을 바꾸고 모두 불러오기
{member => _, _}
특정 멤버는 제외하고 모두 불러오기
- import 하고싶은 패키지 사이에 같은 이름의 멤버가 있을 때 모호함을 피할 수 있음
Implicit import
java.lang._
scala._
Predef._
접근 수식자
Private
- inner class는 자기 부모 class의 private에 접근할 수 있음
Protected
- 자바와 달리 subclass 에서만 접근 가능
- 자바에서는 같은 패키지 안에 있으면 접근 가능했음
보호 스코프
private[x]
, protected[x]
같은 형태로 좀 더 상세하게 정의 가능
- x까지는 제외하고 비공개, 보호라는 뜻
- x는 패키지, 클래스, 또는 오브젝트
private[this]
같은 형식으로 쓰면 인스턴스 내부에서만 접근 가능. private보다 좀 더 제한적
package com.navercorp
package test {
package test1 {
private[test] class Test1 {}
}
package test2 {
class Test2 {
val test1 = new Test1
}
}
}
패키지 객체
package object Name
형식으로 작성
- 자유롭게 import해서 사용 가능
- package 디렉토리에 package.scala 라는 파일 이름으로 하는 것이 좋음
15장
케이스 클래스
- class 선언 앞에
case
가 붙어있는 서브 클래스
- 클래스 이름과 같은 팩토리 메소드 추가
new MyClass()
대신에 MyClass()
로 생성 가능
- 모든 클래스 패러미터에 val 접두사를 붙여줌
toString
, hashCode
, equals
에 자연스러운 구현 추가
copy
메소드 추가
- 이름있는 패러미터를 이용해서 바꿀 필드를 정의
case class Ex(pa1: Int, pa2: Int)
val ex = Ex(1, 2)
val copyEx = ex.copy(pa2 = 3)
패턴 매치
- 자바의 switch 문과 유사한 기능
Selector match {case ...}
형태로 사용
- 각 case는 =>로 패턴과 계산식 분리
- 코드에 쓰인 순서대로 패턴을 하나씩 검사 후 매치되는 첫 번째 패턴을 선택하여 화살표 뒤의 계산식 수행
와일드카드 패턴
_
와일드카드 패턴은 모든 값과 매치되지만 계산식 안에서 사용할 수
- 주로 필요없는 부분을 무시하기위해 사용
상수 패턴
"+"
, 1
같은 상수 패턴은 == 연산자 적용하여 매치
- val이나 싱글톤 object도 매치됨 ex)
Nil
변수 패턴
e
같은 변수 패턴은 모든 값과 매칭 가능, 매칭된 이후 계산식이 수행될 때 같은 이름을 가진 변수가 있다면 매치된 값 참조
- 와일드카드 패턴과 마찬가지로 자바의
default:
처럼 사용 가능
- 이름이 소문자로 시작하면 변수, 그 외에는 모두 상수로 간주
- 만약에 소문자로 시작하는 상수를 쓰고싶으면 패키지부터 이름을 모두 써주거나 역따옴표 `로 감싸면 됨
생성자 패턴
MyCaseClass('test', e)
같은 생성자 패턴은 먼저 타입을 체크하고 패러미터를 하나하나 체크하여 패턴 매칭
- 패러미터도 패턴의 일부(deep match)인 덕분에
MyCaseClass('test', MyCaseClass('nested', e))
처럼 중첩이 가능함
시퀀스 패턴
List
, Array
등 시퀀스와 매칭
- 길이에 구애받고싶지 않다면
List(0, _*) => //...
같은 형식으로 작성
튜플 패턴
타입 지정 패턴
case e: String => //...
처럼 타입을 지정하면 특정 타입유형만 매칭, e가 계산식 내에서 해당 타입으로 들어옴
타입 소거
- 스칼라도 런타임에선 제네릭의 타입을 소거
- 따라서
case e: Map[Int, Int] => //...
처럼 특정 타입 제네릭만 매칭하려 해보면 unchecked warning과 함께 모든 타입에 매칭됨
- 배열만은 원소 타입과 값을 함께 저장하므로 매칭 가능
변수 바인딩
- 깊은 매치에서 매칭된 결과 일부를 변수로 바인딩하는 것
case myFunc("const", e @ myFunc("const1", _)) => //...
- 먼저
myFunc("const1", _)
을 매칭하여 e에 담고
- 다음으로
myFunc("const", e)
매칭
- 계산식에서 e를 호출하면
myFunc("const1", _)
이 됨
패턴 가드
- 변수 패턴에 조건 적용
case myFunc2(x, y) if x == y => //...
- 가드가 true가 될 때에만 매칭
패턴 겹침
봉인한 클래스
- 모든 패턴에 대응했는지 확인하기 쉽게 만들어줌
- 다른 컴파일 단위에서 함부로 클래스를 새로 정의하는걸 막아줌
sealed abstract class MyClass
처럼 제일 앞에 sealed
추가
Option
- 선택적인 값을 표현. 보통
Map
등에서 반환
- 값이 있으면
Some(x)
없으면 None
- 보통 패턴 매치를 써서 처리
변수 정의에서 패턴 사용
val myTuple = (10, "test")
val (number, string) = myTuple
case 시퀀스
{
case x =>
case y =>
}