Scala 3를 익히기 위해선, 먼저 Scala 2를 알아야 할 것 같다. 관련 책 한권을 정리해본다.
- 01~10장: https://doocong.com/languages/scala-1/
- 11~20장: https://doocong.com/languages/scala-2/
- 21~30장: https://doocong.com/languages/scala-3/
A Scalable Language
scala를 써야하는 7가지 이유
- Scala is scalable
- 확장이 용이, 커스텀 타입을 빌트인 처럼 쓸 수 있다.
- 연산자 함수 등을 커스텀해, DSL을 만들기에도 용이하다.
- Scala is OOP
- Pure OOP = 모든 요소는 객체(값, 함수, 타입 포함)
- Powerful composition feature =
trait
- Scala is FP
- 함수을 first-class value로 취급한다.
- 값의 변경 대신, 입출력의 매핑에 집중한다.
- 사이드 이펙트 없음 = referentially transparent
- Scala is compatible
- interoperability with Java
- implicit conversion을 통한 Java 라이브러리 강화
- Scala is concise
- less boilerplate, easy to read and understand
- FP, 확장 가능한 라이브러리로 더 짧게 줄일 수 있음
- Scala is high-level
- 메소드를 전달함으로서 제어-추상화 가능
- Scala is staticically typed
- 정적 타입의 장점은 살리고, 타입추정으로 verbosity를 줄임
First Steps in Scala
- 변수 선언
- 변수 선언시
val
,var
사용, 타입 추정됨 val
= immutablevar
= mutabledef
= lazy (사실은 함수임)val x, y, z = 1
처럼 묶어서 선언 가능(var
도 가능)
- 변수 선언시
- 함수 선언
def
로 시작- 변수, 파라미터와 타입, 리턴타입 모두 Kotlin과 동일
- 단 body와 function 선언 사이에
=
이 들어가야함 - 한 줄 일 경우 블록 필요 없음
- 여러 줄 일 경우
{}
필요, scala3에서는 indent로 대체됨 return
은 생략 가능, 명식적으로 쓸 수도 있음- 단
return
을 쓸 경우는 리턴 타입을 명시해야함
- 반복, 조건
while
은 타 언어와 사용법 동일if
역시 동일, 추가로 expression으로 사용 가능for
는 generator 포맷으로 사용, immutable yield만 가능
1 |
|
block 에 관해서 (책 외 내용)
- 스칼라의 블록은 코틀린의 그것과 거의 같다
- 어느 위치에서나 블록을 열 수 있고
- 블록은 끝까지 실행 된 후 값을 산출한다.
- 몇몇 예외를 제외하고 블록을 열어 문법 요소 일부를 블록으로 치환하는게 가능하다.
Next Steps in Scala
Generics, Array, apply, update
- 제너릭은 대괄호로 표현
- 타입은 명시하지 않아도 자동으로 추정됨
apply
- 객체에 대해서 메소드 이름 없이 호출하면
apply
가 호출됨 - 인스턴스에 대해서 호출하면, 멤버
apply
가 호출됨 - 클래스에 대해서 호출하면, companion object의
apply
가 호출됨
- 객체에 대해서 메소드 이름 없이 호출하면
update
- 특정 객체에 대한 대입은 update로 치환됨
apply
와 마찬가지로 클래스에 대해서 호출하면, 컴패니언의 update가 호출됨
- infix 호출
- 인자가 하나인 메소드는 별다른 처리 없이 infix로 쓸 수 있음
1 |
|
List
- Array와 다른점
- immutable (Array는 mutable)
- 값 비교 가능 (Array는 reference 비교함)
- 연결 리스트이므로, random access에 선형시간이 걸림
- 각 노드가 head와 tail을 가지고 이어진 연결리스트
head
나tail
은 상수시간에 찾을 수 있다.
- 연산자 메서드 존재
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
를 가질 수 있음
new
와apply
비교new
와 함께 호출시 명시적 생성자 호출- 그냥 호출시 companion object의
apply
가 호출됨 - scala3
apply
가 없으면, 자동으로apply
로new
호출됨
Unit
타입- 모든 타입객체는 Unit으로 변환 가능
Any
타입 변수에 값을 대입하는것과는 다름- Unit으로 변환되면서 데이터가 사라짐
- 세미콜로는 생략해도 됨
Singleton Objects
object
로 선언, 클래스랑 같은 문법- 단 파라미터는 사용 불가능, 정적 속성을 가지므로 당연함
- 다른 클래스나, trait를 상속 가능
- 타입으로서는 사용 못함, 즉 부모로서 상속해주는것은 불가능
- Companion object
- 같은 파일, 같은 이름의
class
와object
가 있으면 companion object임 - 각자의
private
멤버에 접근 가능 class
는object
의 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
로 쓰되, 뒤쪽을 괄호로 묶어야 한다. - 또
:
로 끝나면 역순의infix
로 쓸 수 있다.3 :: List(2)
- 특히 인자를 한개만 받으면
- 단향 연산자
- 인자가 없으면, 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 referenceval
,var
을 블록 내에 선언해서 필드로 사용val
,var
을 붙이면 멤버로서 바로 사용 가능
- Auxiliary Constructor
def this
로 선언- 다른 생성자를 꼭 불러야함, 최종적으로는 primary가 불리게 되어 있음
- Aux-const는 super class의 생성자를 호출할 수 없음 (Primary만 가능)
- 필드 가시성
- 기본적으로
public
임 private
으로 선언해서 사용 가능
- 기본적으로
- String interpolation
s"$numer/$denom"
1 |
|
Identifier
- 4가지 종류의 Identifier
- 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 |
|
오버로딩과 암시적 변환
Rational
을 만들고Int
와 곱셈이 되게 하고 싶음r1 * r2
:* (r1: Rational, r2: Rational)
메서드를 만들었음r1 * 2
:* (r1: Rational, n: Int)
메서드 오버로딩 해야함2 * r1
: 암시적 변환 필요
- 암시적 변환
implict
과 적절한 변환 함수를 선언하면, 자동으로 변환해줌- 단 스칼라 3 및 최신 스칼라 2에서는 권장되지 않는 방법임
given
을 쓰는게 바람직함- 명시적이지 않은 코드를 양산하므로, 항상 사용에 주의할것
- 암시적 변환은 메소드 호출 시에, 맞는 메소드가 없을때 일어남
1 |
|
Built-in Control Structures
- 간결함을 위해서 최소한의 제어 구문만 있음
- 대부분 값, 표현으로 쓰일 수 있음
if, while
- 둘다 다른 언어들과 거의 같음
if
는 값으로 쓰일 수 있음while
,do~while
모두 있음, 단 표현으로 못씀- 대신
Unit
리턴함
- 대신
- 대입 연산은
Unit
을 반환하므로, C스타일로 구현할 수 없음 - 그냥 while은 쓰지 않는걸 지향함
1 |
|
for statement
- 다른 언어와 비교할 때, header가 복잡함 + expression 사용 가능
- generator 형식 쓸 수 있고
- generator 여러개를 쓸 수도 있고
- fitler를 쓰거나 여러개를 쓸 수 있고
- 루프내에서 유효한 변수를 선언할 수도 있음
- generator 문법
i <- List(1,2,3)
: 1,2,3i <- Array(1,2,3)
: 1,2,3i <- 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,3i <- 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 |
|
try-catch-finally
throw
로Throwable
객체를 만들어 던짐throw
문장은Noting
을 값으로 가짐Nothing
은 모든 타입의 자녀타입임, 즉 어떤 변수로도 대입할 수 있음
try
는 Java와 완전 같음- 단 마지막에 오는 표현식으로 값을 부여할 수 있음
catch
는 기본적으로 패턴 매칭을 쓰도록 구성됨- 무조건
case
구문의 나열로 구현해야함 - Java처럼 throw할 예외를 명시할 필요 없음
- 마찬가지로 마지막에 오는 표현식으로 값을 부여할 수 있음
- 무조건
finally
도 java와 완전 같음- 단
finally
는 값을 부여할 수 없음 (무시됨) finally
가 주로 side effect를 만드는 속성을 가지기 때문
- 단
1 |
|
match expression
- switch와 유사하나, 훨씬 강력함
- expression으로 쓸 수 있고
- filter를 붙일 수도 있고
- pattern matching으로도 쓸 수 있음
- 상수뿐만 아니라 온갖 오브젝트와 변수를 집어넣는게 가능
break
안써도 되고, 암시적으로break
가 작동
_
를 else로 사용
break와 continue가 없음
- FP적인 특성을 더 잘 살리기 위해 break, continue가 없음
- tail-call등은 실제로 loop으로 컴파일됨
- 그러므로 break, continue 대신 if문 분기나, 재귀를 써서 해결을 권장
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 |
|
Partially applied function
functionName _
로 사용- 일반 함수의 모든 인자를
_
로 넣는 것을 의미 - 자체로서 람다식이 됨
- 즉 일반 함수를 function literal로 바꿔줌
- 일반 함수의 모든 인자를
sum(5, _)
로 사용- 특정 함수값을 고정값으로 부여하는 새로운 함수를 생성함
- Scala에는 디폴트 인자값이 그래서 필요 없구나!
- 단 직접 호출시에는 괄호의 결합순서에 따라 오류가 발생
- 일반 함수를 인자로 넘길수 있다.
- 원칙적으로 일반 함수는 값이 아니므로 인자로 넘길 수 없음
- 단 타입 시그네쳐가 일치하는 경우에는
_
을 생략 가능 - 즉
f(println)
을 써도f(println _)
으로 컴파일러가 인지함
1 |
|
Clousure
- 자신을 감싸고 있는 함수 스코프의 변수들을 가져다 쓸 수 있음
- 메소드, 로컬함수, 리터럴 모두 같은 규칙을 가지고 작동한다.
- 변수 자체를 캡쳐하므로 var의 변화도 인지한다.
- scope이 종료되어도 변수는 생성될 당시의 값을 유지한다.
1 |
|
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 |
|
사용자 정의 컨트롤
- 한개의 인자만 있다면, 인자를 curly braces로 대체할 수 있음
- 즉 사용자 정의의 컨트롤을 만들 수 있음
- loan pattern 과 같이 자원을 회수해야하는 경우 유용함
- 예를들면 connection 혹은 file,
close
를 불러야함 - 단순히 curly-braces로 인가하게 되면 실행 순서에서 문제가 생김
- 예를들면 connection 혹은 file,
1 |
|
call-by-name
- 위의 loan pattern을
if
처럼 사용하고 싶다면 - 기존대로라면 값을 산출한 후, 함수에 인가함
- 인가를 먼저 하고, 나중에 값을 산출해야
if
처럼 동작함
- 인가를 먼저 하고, 나중에 값을 산출해야
- 만약, 인가할 대상이 람다였다면
- 어차피 함수이므로 값이 산출되지 않는다.
- 단 의미없는 verbosity가 늘어남
- call-by-name이라면
- 순서가 제대로 동작하고
- 코드도 call-by-value와 똑같음
1 |
|
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 뒤에서 불러야 함