├── README-KO.md
├── README-ZH.md
└── README.md
/README-KO.md:
--------------------------------------------------------------------------------
1 | # Databricks Scala Guide
2 |
3 | 1000명이 넘는 contributor를 가진 Spark는 빅데이터 분야에서 가장 큰 오픈소스 프로젝트이며, Scala를 사용하는 프로젝트 중 가장 활성화된 프로젝트 입니다. 이 가이드라인은 [Databricks](http://databricks.com/) 엔지니어 팀 뿐 아니라, Spark에 기여하는 엔지니어들의 경험을 바탕으로 만들어 졌습니다.
4 |
5 | 코드는 저자에 의해 __한 번 쓰여지지만__, 많은 다른 엔지니어들은 그 같은 코드를 __반복적으로 수정하고 읽습니다__. 대부분의 버그들은 보통 코드의 변경으로부터 나옵니다. 그래서 우리는 코드의 가독성과 유지 보수성을 향상시키기 위해 우리의 코드를 최적화 해야합니다. 이를 위한 최선의 방법은 간단한 코드를 작성하는 것입니다.
6 |
7 | Scala는 매우 강력하며 여러가지 페러다임에 적용 가능한 언어입니다. 우리는 아래의 가이드라인을 통해 여러가지 프로젝트를 빠른 속도로 진행하고 있습니다. 팀이나 회사의 요구사항 등에 따라서 일부 다르게 적용 해야 할 수도 있습니다.
8 |
9 | 
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
10 |
11 | ## 목차
12 |
13 | 1. [문서 역사](#history)
14 |
15 | 1. [구문 스타일](#syntactic)
16 | - [명명 규칙](#naming)
17 | - [변수 명명 규칙](#variable-naming)
18 | - [라인 길이](#linelength)
19 | - [30 규칙](#rule_of_30)
20 | - [공백 및 들여쓰기](#indent)
21 | - [빈 줄](#blanklines)
22 | - [괄호](#parentheses)
23 | - [중괄호](#curly)
24 | - [Long 정수](#long_literal)
25 | - [문서 스타일](#doc)
26 | - [클래스 내의 순서](#ordering_class)
27 | - [Imports](#imports)
28 | - [패턴 매칭](#pattern-matching)
29 | - [중위 표기](#infix)
30 | - [익명 함수](#anonymous)
31 |
32 | 1. [Scala 언어의 기능](#lang)
33 | - [케이스 클래스와 불변성](#case_class_immutability)
34 | - [apply 함수](#apply_method)
35 | - [override 수정자](#override_modifier)
36 | - [튜플 추출](#destruct_bind)
37 | - [Call by Name](#call_by_name)
38 | - [다중 매개 변수 표기](#multi-param-list)
39 | - [특수 문자 함수 (오퍼레이터 오버로딩)](#symbolic_methods)
40 | - [타입 추론](#type_inference)
41 | - [Return 예약어](#return)
42 | - [재귀 용법과 꼬리 재귀 용법](#recursion)
43 | - [Implicits](#implicits)
44 | - [예외 처리 (Try vs try)](#exception)
45 | - [Options](#option)
46 | - [모나드 채이닝](#chaining)
47 |
48 | 1. [동시성 제어](#concurrency)
49 | - [Scala concurrent.Map](#concurrency-scala-collection)
50 | - [동기화 (synchronized) 명시 vs Java 제공 동시성 라이브러리](#concurrency-sync-vs-map)
51 | - [동기화 (synchronized) 명시 vs Atomic 변수 vs @volatile](#concurrency-sync-vs-atomic)
52 | - [Private 변수](#concurrency-private-this)
53 | - [동시성 로직 분리](#concurrency-isolation)
54 |
55 | 1. [성능](#perf)
56 | - [Microbenchmarks](#perf-microbenchmarks)
57 | - [순회와 zipWithIndex](#perf-whileloops)
58 | - [Option과 null](#perf-option)
59 | - [Scala Collection 라이브러리](#perf-collection)
60 | - [private[this]](#perf-private)
61 |
62 | 1. [Java 호환성](#java)
63 | - [Scala에서 사용 할 수 없는 Java 기능](#java-missing-features)
64 | - [Traits와 Abstract 클래스](#java-traits)
65 | - [Type 별칭](#java-type-alias)
66 | - [기본 매개변수 값](#java-default-param-values)
67 | - [다중 매개변수 표기](#java-multi-param-list)
68 | - [가변인자](#java-varargs)
69 | - [Implicits](#java-implicits)
70 | - [관련 객체, 정적 함수 및 변수](#java-companion-object)
71 |
72 | 1. [테스트](#testing)
73 | - [예외 가로 채기](#testing-intercepting)
74 |
75 | 1. [기타](#misc)
76 | - [currentTimeMillis 보다는 nanoTime](#misc_currentTimeMillis_vs_nanoTime)
77 | - [URL 보다는 URI](#misc_uri_url)
78 | - [이미 존재 하는 함수를 다시 개발하는 것 보다는 기존의 잘 테스트 된 함수 사용](#misc_well_tested_method)
79 |
80 | ## 문서 역사
81 | - 2015-03-16: 초기 버전.
82 | - 2015-05-25: [override 수정자](#override_modifier) 섹션 추가.
83 | - 2015-08-23: "do NOT"에서 "avoid"으로 심각도 낮춤.
84 | - 2015-11-17: [apply 함수](#apply_method) 섹션 갱신: 한 객체의 apply 함수는 그 객체와 같은 이름을 가진 클래스를 반환해야 합니다.
85 | - 2015-11-17: 이 가이드라인이 [중국어로 번역되었습니다](README-ZH.md). 중국어 번역은 커뮤니티 맴버인 [Hawstein](https://github.com/Hawstein) 이 했습니다. 이 문서의 최신성을 보장하지 않습니다.
86 | - 2015-12-14: 이 가이드라인이 [한국어로 번역되었습니다](README-KO.md). 한국어 번역은 [Hyukjin Kwon](https://github.com/HyukjinKwon) 이 했으며, [Yun Park](https://github.com/yunpark93), [Kevin (Sangwoo) Kim](https://github.com/swkimme), [Hyunje Jo](https://github.com/RetrieverJo) 그리고 [Woocheol Choi](https://github.com/socialpercon) 가 검토를 했습니다. 이 문서의 최신성을 보장하지 않습니다.
87 | - 2016-06-15: [익명 함수](#anonymous) 섹션 추가.
88 | - 2016-06-21: [변수 명명 규칙](#variable-naming) 섹션 추가.
89 | - 2016-12-24: [케이스 클래스와 불변성](#case_class_immutability) 색션 추가.
90 | - 2017-02-23: [테스트](#testing) 섹션 추가.
91 | - 2017-04-18: [이미 존재 하는 함수를 다시 개발하는 것 보다는 기존의 잘 테스트 된 함수 사용](#misc_well_tested_method) 색션 추가.
92 |
93 | ## 구문 스타일
94 |
95 | ### 명명 규칙
96 |
97 | 우리는 주로 Java와 Scala의 표준 명명 규칙을 따릅니다.
98 |
99 | - Class, trait, 객체는 명명규칙 즉 낙타등 표기법(PascalCase) 을 따라야 합니다.
100 | ```scala
101 | class ClusterManager
102 |
103 | trait Expression
104 | ```
105 |
106 | - Package는 Java의 명명 규칙을 따라야 합니다. 모두 소문자로 ASCII 문자를 사용합니다.
107 | ```scala
108 | package com.databricks.resourcemanager
109 | ```
110 |
111 | - 메소드/함수는 낙타등 표기법 (camelCase)을 사용해야 합니다.
112 |
113 | - 모든 상수는 대문자로 표기 하고, 연관된 객체에 배치합니다.
114 | ```scala
115 | object Configuration {
116 | val DEFAULT_PORT = 10000
117 | }
118 | ```
119 |
120 | - Enum은 낙타등 표기법 (PascalCase)을 따라야 합니다.
121 |
122 | - Annotation 또한 낙타등 표기법 (PascalCase)을 따라야 합니다. 이 가이드라인이 Scala의 공식 가이드라인과 다름을 주의하시기 바랍니다.
123 | ```scala
124 | final class MyAnnotation extends StaticAnnotation
125 | ```
126 |
127 |
128 | ### 변수 명명 규칙
129 |
130 | - 변수는 낙타등 표기법 (PascalCase)을 사용해야 하고, 명백히 변수의 의미가 설명 될 수 있는 자명한 이름을 사용 해야 합니다.
131 |
132 | ```scala
133 | val serverPort = 1000
134 | val clientPort = 2000
135 | ```
136 |
137 | - 지엽적인 공간에서 변수 이름이 하나의 글자로 명명 되는 것은 괜찮습니다. 예를 들어, "i" 는 길지 않은 순환문 에서 (예를 들어, 10 라인의 코드) 그 순환문 안에서의 인덱스를 나타내기 위해 자주 사용 됩니다. 그러나, "l" (Larry의 맨 앞자)를 식별자로 사용하지 않습니다. 왜냐하면, "l", "1", "|" 그리고 "I" 은 구분하기가 어렵기 때문 입니다.
138 |
139 | ### 라인 길이
140 |
141 | - 라인 길이는 100자를 넘지 않습니다.
142 | - 단, import나 URL의 경우는 예외입니다. (그렇다 하더라도 100자의 제약을 지켜주도록 합니다).
143 |
144 |
145 | ### 30 규칙
146 |
147 | "한 개의 엘리먼트가 30개 이상의 하위 엘리먼트를 포함 하고 있다면, 심각한 문제가 있을 가능성이 높다." - [Refactoring in Large Software Projects](http://www.amazon.com/Refactoring-Large-Software-Projects-Restructurings/dp/0470858923).
148 |
149 | 일반적으로:
150 |
151 | - 함수는 30줄 이상의 라인을 초과하지 않아야 합니다.
152 | - 하나의 클래스당 30개 이상의 함수를 갖지 않도록 합니다.
153 |
154 |
155 | ### 공백 및 들여쓰기
156 |
157 | - 연산자 및 할당 연산자 앞 뒤에는 1칸 공백을 두도록 합니다.
158 | ```scala
159 | def add(int1: Int, int2: Int): Int = int1 + int2
160 | ```
161 |
162 | - 콤마 뒤에는 1칸 공백을 두도록 합니다.
163 | ```scala
164 | Seq("a", "b", "c") // 이와 같이 하도록 합니다.
165 |
166 | Seq("a","b","c") // 콤마 뒤에는 공백을 생략히지 않습니다.
167 | ```
168 |
169 | - 콜론 뒤에는 1칸 공백을 두도록 합니다.
170 | ```scala
171 | // 아래 예와 같이 하도록 합니다.
172 | def getConf(key: String, defaultValue: String): String = {
173 | // 코드
174 | }
175 |
176 | // 콜론 앞에는 공백을 두지 않습니다.
177 | def calculateHeaderPortionInBytes(count: Int) : Int = {
178 | // 코드
179 | }
180 |
181 | // 콜론 뒤에는 공백을 생략하지 않습니다.
182 | def multiply(int1:Int, int2:Int): Int = int1 * int2
183 | ```
184 |
185 | - 2칸 공백 들여쓰기를 합니다.
186 | ```scala
187 | if (true) {
188 | println("Wow!")
189 | }
190 | ```
191 |
192 | - 함수 선언에서 파라메터가 두 줄에 맞지 않아 들여쓰기를 하는 경우, 각 인자에 4칸 공백을 사용하고 각 라인에 배치 합니다. 반환 타입은 다음 줄에 배치되거나 같은 라인에 배치될 수 있습니다. 다음 라인에 쓰는 경우, 2칸 들여쓰기를 합니다.
193 |
194 | ```scala
195 | def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
196 | path: String,
197 | fClass: Class[F],
198 | kClass: Class[K],
199 | vClass: Class[V],
200 | conf: Configuration = hadoopConfiguration): RDD[(K, V)] = {
201 | // method body
202 | }
203 |
204 | def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
205 | path: String,
206 | fClass: Class[F],
207 | kClass: Class[K],
208 | vClass: Class[V],
209 | conf: Configuration = hadoopConfiguration)
210 | : RDD[(K, V)] = {
211 | // method body
212 | }
213 | ```
214 |
215 | - 클래스의 해더가 두 줄에 맞지 않을 때는, 각 인자에 4칸 공백을 사용하고 각 라인에 배치 합니다. 또한, extends를 2칸 공백 뒤에 배치하고, 그 뒤에 한 개의 빈 줄을 입력 합니다.
216 |
217 | ```scala
218 | class Foo(
219 | val param1: String, // 4 space indent for parameters
220 | val param2: String,
221 | val param3: Array[Byte])
222 | extends FooInterface // 2 space here
223 | with Logging {
224 |
225 | def firstMethod(): Unit = { ... } // blank line above
226 | }
227 | ```
228 |
229 | - 함수와 클래스 생성자 호출이 두 줄에 맞지 않는 경우는, 각 인자에 2칸 공백을 사용하고 각 라인에 배치 합니다.
230 |
231 | ```scala
232 | foo(
233 | someVeryLongFieldName, // 2 space indent here
234 | andAnotherVeryLongFieldName,
235 | "this is a string",
236 | 3.1415)
237 |
238 | new Bar(
239 | someVeryLongFieldName, // 2 space indent here
240 | andAnotherVeryLongFieldName,
241 | "this is a string",
242 | 3.1415)
243 | ```
244 |
245 | - 수직 정렬을 사용하지 않습니다. 이것은 중요치 않은 코드에 집중하게 하고, 차후에 코드 수정을 어렵게 만듭니다.
246 | ```scala
247 | // Don't align vertically
248 | val plus = "+"
249 | val minus = "-"
250 | val multiply = "*"
251 |
252 | // Do the following
253 | val plus = "+"
254 | val minus = "-"
255 | val multiply = "*"
256 | ```
257 |
258 |
259 | ### 빈 줄
260 |
261 | - 빈 줄은 아래의 경우에 사용합니다:
262 | - 연속되는 변수, 생성자, 함수 또는 내부 클래스들 사이 빈 줄이 삽입 될 수 있습니다.
263 | - 예외: 연속되는 변수 선언 사이 아무런 코드도 없다면 빈 줄은 옵션입니다. 이런 빈 줄들은 논리적인 그룹을 만들 때 사용 될 수 있습니다.
264 | - 함수 안에서 빈 줄을 삽입하여 논리적인 그룹을 만들 수 있습니다.
265 | - 첫 번째 맴버 앞이나 마지막 맴버 뒤에 빈 줄이 있을 수 있습니다.
266 | - 한개 또는 두개의 빈 줄을 사용하여 Class 선언들을 분리합니다.
267 | - 과도한 수의 빈 줄은 권장하지 않습니다.
268 |
269 |
270 | ### 괄호
271 |
272 | - I/O 접근이나 상태 변형에 대한 접근을 갖고 있거나 side-effect를 줄 수 있는 함수는 괄호와 함께 선언되어야 합니다.
273 | ```scala
274 | class Job {
275 | // Wrong: killJob changes state. Should have ().
276 | def killJob: Unit
277 |
278 | // Correct:
279 | def killJob(): Unit
280 | }
281 | ```
282 |
283 | - 함수 호출자는 반드시 함수의 정의를 따라야 합니다. 예를 들어, 함수가 괄호 없이 선언되었다면 괄호 없이 호출되어야 합니다. 이 것은 단지 문법적인 문제일 뿐만 아니라 `apply`를 호출 할 때에도 문제가 될 수 있습니다.
284 |
285 | ```scala
286 | class Foo {
287 | def apply(args: String*): Int
288 | }
289 |
290 | class Bar {
291 | def foo: Foo
292 | }
293 |
294 | new Bar().foo // This returns a Foo
295 | new Bar().foo() // This returns an Int!
296 | ```
297 |
298 |
299 | ### 중괄호
300 |
301 | 한 줄 조건부 식이나 순환문에도 중괄호를 넣어야 합니다. 단, if/else문의 경우에는 한 줄로 표기 하거나, side-effect가 없는 3항 연산자로 표기 할 수 있습니다.
302 |
303 | ```scala
304 | // Correct:
305 | if (true) {
306 | println("Wow!")
307 | }
308 |
309 | // Correct:
310 | if (true) statement1 else statement2
311 |
312 | // Correct:
313 | try {
314 | foo()
315 | } catch {
316 | ...
317 | }
318 |
319 | // Wrong:
320 | if (true)
321 | println("Wow!")
322 |
323 | // Wrong:
324 | try foo() catch {
325 | ...
326 | }
327 | ```
328 |
329 |
330 | ### Long 정수
331 |
332 | Long 정수의 접미사는 `L`로 사용합니다. 이는 가끔 `l`과 `1`을 구분하기 힘들 때가 있기 때문입니다.
333 |
334 | ```scala
335 | val longValue = 5432L // Do this
336 |
337 | val longValue = 5432l // Do NOT do this
338 | ```
339 |
340 |
341 | ### 문서 스타일
342 |
343 | Scala 주석 스타일 대신 Java 주석 스타일을 따릅니다.
344 | ```scala
345 | /** This is a correct one-liner, short description. */
346 |
347 | /**
348 | * This is correct multi-line JavaDoc comment. And
349 | * this is my second line, and if I keep typing, this would be
350 | * my third line.
351 | */
352 |
353 | /** In Spark, we don't use the ScalaDoc style so this
354 | * is not correct.
355 | */
356 | ```
357 |
358 |
359 | ### 클래스 내의 순서
360 |
361 | 만약 Class의 정의가 길고 많은 함수들을 포함하고 있다면, 논리적으로 분할 하고, 아래와 같은 주석 헤더를 이용하여 구분 합니다.
362 | ```scala
363 | class DataFrame {
364 |
365 | ///////////////////////////////////////////////////////////////////////////
366 | // DataFrame operations
367 | ///////////////////////////////////////////////////////////////////////////
368 |
369 | ...
370 |
371 | ///////////////////////////////////////////////////////////////////////////
372 | // RDD operations
373 | ///////////////////////////////////////////////////////////////////////////
374 |
375 | ...
376 | }
377 | ```
378 |
379 | 물론, 이 예와 같은 Class의 길이는 권장하지 않습니다. 일반적으로 내부 구현이 아닌 공개되어있는 API를 만들 때 위와 같은 형식 사용 됩니다.
380 |
381 | ### Imports
382 |
383 | - __와일드 카드를 이용한 import는 피하도록 합니다__. 단, 6개 이상 같은 페키지에서 import하는 경우 혹은 implicit 함수들을 import하는 경우는 허용 됩니다. 와일드카드 import는 외부(import 된 페키지)의 변화에 약할 수 있습니다.
384 | - import를 할 때는 상대 경로가 아닌 절대 경로를 사용합니다. 예를 들어 상대경로 `util.Random` 가 아닌`scala.util.Random` 을 사용합니다.
385 | - 또한, import는 아래와 같은 순서로 정렬해야 합니다:
386 | * `java.*` 와 `javax.*`
387 | * `scala.*`
388 | * Third-party 라이브러리 (`org.*`, `com.*`, etc)
389 | * 프로젝트 페키지 (`com.databricks.*` 혹은 Spark에서 작업하는 경우 `org.apache.spark`)
390 | - 각각의 그룹 안에서, import는 알파벳 순서로 정렬 합니다.
391 | - IntelliJ의 import 최적화 기능을 사용하여 자동으로 할 수 있습니다. 아래와 같은 config를 적용합니다:
392 |
393 |
394 | ```
395 | java
396 | javax
397 | _______ blank line _______
398 | scala
399 | _______ blank line _______
400 | all other imports
401 | _______ blank line _______
402 | com.databricks // or org.apache.spark if you are working on Spark
403 | ```
404 |
405 |
406 | ### 패턴 매칭
407 |
408 | - 함수 전체가 패턴 매칭을 하는 함수라면 `match` 를 함수의 정의로써 같은 줄에 놓습니다. 이와 같이 들여쓰기의 레벨을 한단계 줄이도록 합니다.
409 | ```scala
410 | def test(msg: Message): Unit = msg match {
411 | case ...
412 | }
413 | ```
414 |
415 | - 함수를 호출 할 때, 아래와 같은 중괄호 안에 (혹은 partial function 안에) 한 개의 `case` 만 있다면, 같은 줄에 넣어 함수 호출을 합니다.
416 | ```scala
417 | list.zipWithIndex.map { case (elem, i) =>
418 | // ...
419 | }
420 | ```
421 | 만약 여러 개의 `case` 문이 존재한다면 아래와 같이 들여쓰기를 합니다.
422 | ```scala
423 | list.map {
424 | case a: Foo => ...
425 | case b: Bar => ...
426 | }
427 | ```
428 |
429 | - 만약 어떤 객체의 타입을 패턴 매칭 하는 것이 목표라면, 전체 인자를 확장하지 않습니다. 왜냐하면, 이 것은 리펙토링을 더 힘들게 만들고 코드의 오류를 발생하기 쉽게 만듭니다.
430 | ```scala
431 | case class Pokemon(name: String, weight: Int, hp: Int, attack: Int, defense: Int)
432 | case class Human(name: String, hp: Int)
433 |
434 | // 아래 예와 같이 하지 않습니다. 왜냐하면,
435 | // 1. 새로운 필드가 Pokemon에 추가가 될 때, 우리는 이 패턴 매칭 또한 바꿔야 합니다.
436 | // 2. 특히, 같은 데이터 타입의 인자를 여러게 갖는 경우, 인자를 잘못 매칭 하기 쉬워집니다.
437 | targets.foreach {
438 | case target @ Pokemon(_, _, hp, _, defense) =>
439 | val loss = sys.min(0, myAttack - defense)
440 | target.copy(hp = hp - loss)
441 | case target @ Human(_, hp) =>
442 | target.copy(hp = hp - myAttack)
443 | }
444 |
445 | // Do this:
446 | targets.foreach {
447 | case target: Pokemon =>
448 | val loss = sys.min(0, myAttack - target.defense)
449 | target.copy(hp = target.hp - loss)
450 | case target: Human =>
451 | target.copy(hp = target.hp - myAttack)
452 | }
453 | ```
454 |
455 |
456 | ### 중위 표기
457 |
458 | 특수 문자 함수 (symbolic methods)를 제외하고는 __중위 표기를 피합니다__.
459 | ```scala
460 | // Correct
461 | list.map(func)
462 | string.contains("foo")
463 |
464 | // Wrong
465 | list map (func)
466 | string contains "foo"
467 |
468 | // But overloaded operators should be invoked in infix style
469 | arrayBuffer += elem
470 | ```
471 |
472 | ### 익명 함수
473 |
474 | 익명 함수를 위한 __여분의 소괄호 및 중괄호를 피합니다__.
475 | ```scala
476 | // Correct
477 | list.map { item =>
478 | ...
479 | }
480 |
481 | // Correct
482 | list.map(item => ...)
483 |
484 | // Wrong
485 | list.map(item => {
486 | ...
487 | })
488 |
489 | // Wrong
490 | list.map { item => {
491 | ...
492 | }}
493 |
494 | // Wrong
495 | list.map({ item => ... })
496 | ```
497 |
498 |
499 | ## Scala 언어의 기능
500 |
501 | ### 케이스 클래스와 불변성
502 |
503 | 케이스 클래스는 일반 클래스 입니다만, 컴파일러가 자동으로 아래와 같은 항목들을 지원합니다.
504 | - 생성자의 파라메터들을 위한 퍼블릭 getter들
505 | - 복제 생성자
506 | - 자동 toString/hash/equals 구현
507 |
508 | 케이스 클래스를 위한 생성자 파라메터들은 가변성을 갖지 않아야 합니다. 대신, 복제 생성자를 사용합니다. 이러한 가변 파라메터를 갖는 클래스들은 오류의 발생을 쉽게 만듭니다. 예를 들어, 해쉬맵은 변경 되기 전의 해쉬코드를 갖고 있는 잘못된 버킷에 객체를 놓을 수도 있습니다.
509 |
510 | ```scala
511 | // This is OK
512 | case class Person(name: String, age: Int)
513 |
514 | // This is NOT OK
515 | case class Person(name: String, var age: Int)
516 |
517 | // 값을 바꾸기 위해서는, 새로운 객체를 생성하는 복제 생성자를 사용합니다.
518 | val p1 = Person("Peter", 15)
519 | val p2 = p2.copy(age = 16)
520 | ```
521 |
522 |
523 | ### apply 함수
524 |
525 | Class 안에서의 apply 함수는 코드의 가독성을 저하 시킵니다. 특히, Scala에 익숙하지 않은 사람들에게는 더욱 생소 할수 있습니다. 또한 IDE가 호출을 따라가기 어렵게 만듭니다. 최악의 경우, [괄호](#parentheses) 항목의 예제에서 보이듯이 예상치 못 한 방향으로 코드의 정확성에 영향을 미칠 수 있습니다.
526 |
527 | 같은 이름을 갖는 객체에 펙토리 패턴으로써 apply 함수를 정의하는 것은 괜찮습니다. 이런 경우, apply 함수는 같은 이름의 class타입의 객체를 리턴해야 합니다.
528 | ```scala
529 | object TreeNode {
530 | // This is OK
531 | def apply(name: String): TreeNode = ...
532 |
533 | // This is bad because it does not return a TreeNode
534 | def apply(name: String): String = ...
535 | }
536 | ```
537 |
538 |
539 | ### override 수정자
540 | 항상 함수를 위한 override 수정자는 추상 함수를 오버라이드하는 경우이건 실제 함수를 오버라이드 하는 경우이건 항상 붙여줘야 합니다. Scala 컴파일러는 `override` 추상 함수들에 있어서는 수정자를 요구하지는 않습니다. 하지만 우리는 함수를 위한 override 수정자는 추상 함수를 오버라이드 하든 실제 함수를 오버라이드 하든 항상 붙여줘야 합니다.
541 |
542 | ```scala
543 | trait Parent {
544 | def hello(data: Map[String, String]): Unit = {
545 | print(data)
546 | }
547 | }
548 |
549 | class Child extends Parent {
550 | import scala.collection.Map
551 |
552 | // The following method does NOT override Parent.hello,
553 | // because the two Maps have different types.
554 | // If we added "override" modifier, the compiler would've caught it.
555 | def hello(data: Map[String, String]): Unit = {
556 | print("This is supposed to override the parent method, but it is actually not!")
557 | }
558 | }
559 | ```
560 |
561 |
562 |
563 | ### 튜플 추출
564 |
565 | 튜플 추출은 (바인딩 제거) 두 개의 변수를 하나의 표현식에서 선언 및 대입 할 수 있는 편한 방법입니다.
566 | ```scala
567 | val (a, b) = (1, 2)
568 | ```
569 |
570 | 그러나 생성자에서 이를 사용하지 말아야 합니다 (특히 `a` 와 `b` 가 `transient`의 어노테이션으로 표기되어 있는 경우). Scala 컴파일러는 여분의 Tuple2 필드를 하나 생성하게 되는데 이는 `transient`가 아래 예제에서 적용되지 않습니다.
571 | ```scala
572 | class MyClass {
573 | // This will NOT work because the compiler generates a non-transient Tuple2
574 | // that points to both a and b.
575 | @transient private val (a, b) = someFuncThatReturnsTuple2()
576 | }
577 | ```
578 |
579 |
580 | ### Call by Name
581 |
582 | __Call by name 은 피하도록 합니다__. `() => T`을 명시적으로 사용합니다.
583 |
584 | 배경: Scala는 함수의 인자가 by-name으로 정의되는 것을 허용합니다. 예를 들어 아래와 같은 코드는 정상적으로 작동 합니다.
585 | ```scala
586 | def print(value: => Int): Unit = {
587 | println(value)
588 | println(value + 1)
589 | }
590 |
591 | var a = 0
592 | def inc(): Int = {
593 | a += 1
594 | a
595 | }
596 |
597 | print(inc())
598 | ```
599 |
600 | 위의 예제에서는 `inc()`가 `print`에게 값 `1`이 아닌 함수 (closure) 로써 전달됩니다. 그리고 `print`에서 두 번 실행이 됩니다. 여기서 문제점은 호출하는 쪽에서는 call-by-name과 call-by-value를 구분 할 수 없다는 것 입니다. 따라서, 이 표현이 print가 호출 되기 전에 실행되었는지 아닌지(혹은 여러번 실행이 될 것이라는 것 까지도)를 확신 할 수 없게 됩니다. 이 것은 근본적으로 위험하고 side-effect가 있을 수 있게 됩니다.
601 |
602 |
603 | ### 다중 매개 변수 표기
604 |
605 | __다중의 변수를 리스트로 묶어 표기하는 것을 피하도록 합니다__. 이는 연산자의 오버로딩을 복잡하게 하고, Scala에 익숙치 않은 개발자들을 헷갈리게 할 수 있습니다.
606 |
607 | ```scala
608 | // Avoid this!
609 | case class Person(name: String, age: Int)(secret: String)
610 | ```
611 |
612 | 하나의 주의할 예외로는 implicit에서 낮은 레벨의 라이브러리를 정의 할 때 (리스트로 묶은) 사용 되는 두번째 인자 입니다. 하지만 되도록이면 [implicit은 피해야 합니다](#implicits).
613 |
614 |
615 | ### 특수 문자 함수 (오퍼레이터 오버라이딩)
616 |
617 | __특수 문자(오퍼레이터) 를 함수 이름으로 사용하지 않아야 합니다__. 단, 사칙연산에 있어서, 기호에 알맞게 작용 하는 경우는 허용합니다. (예를 들어 `+`, `-`, `*`, `/`). 그 외에는 어떤 환경에서도 이렇게 사용 되어선 안됩니다. 이런 함수들이 사용 되는 경우에는 가독성이 매우 떨어지고 함수들을 이해하기 힘들게 됩니다. 아래의 두 가지 예제를 참고하시기 바랍니다:
618 | ```scala
619 | // symbolic method names are hard to understand
620 | channel ! msg
621 | stream1 >>= stream2
622 |
623 | // self-evident what is going on
624 | channel.send(msg)
625 | stream1.join(stream2)
626 | ```
627 |
628 |
629 | ### 타입 추론
630 |
631 | Scala의 타입 추론 (특히 left-side 타입 추론) 과 함수 (closure) 추론은 코드를 더 간결하게 만들 수 있습니다. 아래와 같은 몇 가지 경우는 명시적인 타입이 주어져야 합니다:
632 |
633 | - __Public 함수는 명시적으로 타입이 주어져야 합니다__. 그렇지 않다면 컴파일러가 잘못된 타입을 추론 할 수 있습니다.
634 | - __Implicit 함수들은 명시적으로 타입이 주어져야 합니다__. 그렇지 않다면 Scala 컴파일러는 증분 컴파일에서 실패 할 수 있습니다.
635 | - __변수 혹은 타입이 생략된 함수(closure)는 명시적으로 타입이 주어져야합니다__. 좋은 리트머스 테스트는 명시적인 타입들이 사용되어야 합니다. 리뷰어들이 3초 내에 타입을 확인 할 수 없는 경우는 권장되지 않습니다.
636 |
637 | ### Return 예약어
638 |
639 | __Return을 함수(closure)에 사용하지 않도록 합니다__. `return` 은 컴파일러가 ``scala.runtime.NonLocalReturnControl`` 을 위해 ``try/catch`` 를 하도록 만듭니다. 이것은 예상치 못한 컴파일러의 행동으로 이어질 수 있습니다. 아래 예제를 참고해주시길 바랍니다:
640 | ```scala
641 | def receive(rpc: WebSocketRPC): Option[Response] = {
642 | tableFut.onComplete { table =>
643 | if (table.isFailure) {
644 | return None // Do not do that!
645 | } else { ... }
646 | }
647 | }
648 | ```
649 | 이 `.onComplete` 함수는 익명의 함수(closure) `{ table => ... }`를 받고 그 것을 다른 스레드로 보냅니다. 이 함수(closure)는 결국 `NonLocalReturnControl` 을 내뿜게 되고, 이 것은 __다른 스레드__ 에서 잡히게 됩니다. 이 것은 여기서 실행된 함수에게 아무런 영향을 미치지 않게 됩니다.
650 |
651 | 그러나 몇 가지 경우에는 `return` 키워드의 사용이 권고됩니다.
652 |
653 | - `return` 구문을 사용하여, 한 단계의 들여쓰기 레벨을 추가 하지 않고, 흐름 제어를 단순화 시킵니다.
654 | ```scala
655 | def doSomething(obj: Any): Any = {
656 | if (obj eq null) {
657 | return null
658 | }
659 | // do something ...
660 | }
661 | ```
662 |
663 | - `return` 구문을 사용하여, 플래그 변수를 만들지 않고, 루프를 일찍 종료 합니다.
664 | ```scala
665 | while (true) {
666 | if (cond) {
667 | return
668 | }
669 | }
670 | ```
671 |
672 | ### 재귀 용법 과 꼬리 재귀 용법
673 |
674 | __재귀는 피하도록 합니다__. 단, 이 문제가 자연적으로 재귀로 해결되어야 하는 경우는 사용합니다(예를 들어, 그래프 순회 혹은 트리 순회).
675 |
676 | 꼬리 재귀 용법이 적용되어야 하는 함수에 있어서는, `@tailrec` 어노태이션을 사용합니다. 이는 컴파일러가 이 것이 꼬리 재귀 용법이 적용 되어야 한다는 것을 확인 할 수 있도록 합니다 (사실은, 함수(closure)의 사용과 functional transformation등 으로 많은 꼬리 재귀 용법이 사용 되지 않을 수 있습니다)).
677 |
678 | 대부분의 코드는 간단한 루프를 통해 추론하는 것이 더 쉽습니다. 꼬리 재귀를 통해 만들어진 함수는 길고 이해하기 어렵습니다. 예를 들어서 아래 예제는 꼬리 재귀용법보다는 간단한 루프를 사용해서 쉽게 만들 수 있습니다.
679 | ```scala
680 | // Tail recursive version.
681 | def max(data: Array[Int]): Int = {
682 | @tailrec
683 | def max0(data: Array[Int], pos: Int, max: Int): Int = {
684 | if (pos == data.length) {
685 | max
686 | } else {
687 | max0(data, pos + 1, if (data(pos) > max) data(pos) else max)
688 | }
689 | }
690 | max0(data, 0, Int.MinValue)
691 | }
692 |
693 | // Explicit loop version
694 | def max(data: Array[Int]): Int = {
695 | var max = Int.MinValue
696 | for (v <- data) {
697 | if (v > max) {
698 | max = v
699 | }
700 | }
701 | max
702 | }
703 | ```
704 |
705 |
706 | ### Implicits
707 |
708 | __implicit의 사용은 피하도록 합니다__. 단, 아래 경우에 대해서는 예외일 수 있습니다.
709 | - 도메인-특정-언어(DSL)를 빌드 하는 경우
710 | - 암시적 타입의 인자를 사용하는 경우(예를 들어. `ClassTag`, `TypeTag`)
711 | - 특정 클래스 안에서 타입 변환의 코드를 줄이기 위해 사용되는 경우 (예를 들어,Scala 함수(closure) 에서 Java 함수(closure)로의 변환)
712 |
713 | 우리는 코드를 작성한 사람이 아닌 다른 개발자가 이 코드를 implicit의 정의를 읽지 않고 이해 할 수 있도록 합니다. implicit은 상당히 복잡하고 코드를 이해하기 어렵게 만듭니다. Twitter의 Scala 가이드라인에서는 이와 같이 얘기합니다:"만약 당신이 implicit을 사용 하고 있다면, 이를 사용 하지 않고 같은 목적을 달성 할수 없는지 확인하세요."
714 |
715 | 만약 꼭 이를 사용해야 한다면 (예를 들어 DSL을 개선하기 위해), implicit 함수를 오버로드 하지 않습니다. 예를 들어 다른 유저가 손쉽게 골라서 import할 수 있도록 implicit 함수가 중복되지 않는 이름을 갖게 합니다.
716 | ```scala
717 | // Don't do the following, as users cannot selectively import only one of the methods.
718 | object ImplicitHolder {
719 | def toRdd(seq: Seq[Int]): RDD[Int] = ...
720 | def toRdd(seq: Seq[Long]): RDD[Long] = ...
721 | }
722 |
723 | // Do the following:
724 | object ImplicitHolder {
725 | def intSeqToRdd(seq: Seq[Int]): RDD[Int] = ...
726 | def longSeqToRdd(seq: Seq[Long]): RDD[Long] = ...
727 | }
728 | ```
729 |
730 |
731 | ## 예외 처리 (Try vs try)
732 |
733 | - Throwable 또는 Exception 유형을 다루지 않도록 합니다. `scala.util.control.NonFatal` 를 사용합니다:
734 | ```scala
735 | try {
736 | ...
737 | } catch {
738 | case NonFatal(e) =>
739 | // handle exception; note that NonFatal does not match InterruptedException
740 | case e: InterruptedException =>
741 | // handle InterruptedException
742 | }
743 | ```
744 | 이것은 우리가 `NonLocalReturnControl`를 에러 처리 하지 않도록 해 줍니다([Return 예약어](#return) 항목에 설명되어 있는 대로).
745 |
746 | - API 안에서 `Try` 를 사용 하지 않습니다. 예를 들어 어떤 함수에서도 Try를 반환값으로 사용하지 않습니다. 정상적으로 실행되지 않는 경우 명시적으로 예외를 던지고, Java의 try/catch 문을 사용하여 핸들링 하는 것이 권장됩니다.
747 |
748 | 배경: Scala는 `Try`, `Success` 그리고 `Failure`를 통해서 모나딕한 에러 핸들리을 지원합니다. 이는 로직의 체이닝을 가능하게 합니다. 그러나, 이 모나딕한 에러 핸들링은 종종 다중 레벨의 복잡성을 가하고, 코드의 가독성을 저하 시킨다는 것을 경험을 통해 알게 됐습니다. 더군다나, 종종 어느 부분에서 에러가 나오고, 예상치 못 한 예외가 나오는지 알기가 힘듭니다. 그 이유는 `Try` 안에서 이 에러와 예외가 인코딩 되지 않기 때문 입니다. 따라서, 우리는 에러 핸들링을 위해 `Try`의 사용을 권고하지 않습니다. 특히:
749 |
750 | 이 예제의 경우:
751 | ```scala
752 | class UserService {
753 | /** Look up a user's profile in the user database. */
754 | def get(userId: Int): Try[User]
755 | }
756 | ```
757 | 이렇게 쓰이는 것이 낫습니다:
758 | ```scala
759 | class UserService {
760 | /**
761 | * Look up a user's profile in the user database.
762 | * @return None if the user is not found.
763 | * @throws DatabaseConnectionException when we have trouble connecting to the database/
764 | */
765 | @throws(DatabaseConnectionException)
766 | def get(userId: Int): Option[User]
767 | }
768 | ```
769 | 두번째는 확실히 어떤 에러를 핸들링 하는지 호출하는 쪽에서 알기가 쉽습니다.
770 |
771 |
772 | ### Options
773 |
774 | - 값이 비어 있을 수 있을 때 `Option`을 사용합니다. `null`과 대조되어, `Option`은 API에서 명시된 대로 `None`값을 갖을 수 있습니다.
775 | - `Option`을 생성 할 때 `Some`보다는 `Option`을 사용하도록 합니다. 이는 `null` 값으로 부터 안전하도록 합니다.
776 | ```scala
777 | def myMethod1(input: String): Option[String] = Option(transform(input))
778 |
779 | // This is not as robust because transform can return null, and then
780 | // myMethod2 will return Some(null).
781 | def myMethod2(input: String): Option[String] = Some(transform(input))
782 | ```
783 | - None을 사용하여 예외를 표현하지 않습니다. 대신, 명시적으로 예외를 던집니다.
784 | - `Option`에서의 값을 확신 할 수 있지 않는 이상, `Option`에서 `get`을 명시적으로 호출하지 않습니다.
785 |
786 | ### 모나드 채이닝
787 |
788 | Scala의 강력한 특징중 하나는 모나드 채이닝 입니다. 거의 모든 것(예를 들어 collections, Option, Futrue 혹은 Try) 이 모나드 채이닝을 지원하고 같이 맞물려서 동작 할 수 있습니다. 이 것은 놀라울 정도로 강력한 개념입니다. 하지만 이 채이닝은 함부로 남용되어서는 안됩니다. 특히:
789 |
790 | - 3개 이상의 (내부를 포함)모나드 채이닝은 피하도록 합니다.
791 | - 만약 코드의 논리를 이해하는데 5초 이상이 걸린다면, 모나드 체이닝을 사용 하지 않고, 같은 성과를 이룰수 있는 방법을 생각해 볼 필요가 있습니다. 일반적으로 `flatMap` 혹은 `fold`가 이에 해당 됩니다.
792 | - `flatMap` 후에는 거의 항상 모나드 채이닝을 이어가지 않습니다 (왜냐하면 타입이 바뀌기 때문입니다).
793 |
794 | 모나드 체인은 종종 명시적으로 타입이 주어진 중간 값을 저장하는 식으로 채이닝을 끊어 더 이해하기 쉽도록 만듭니다. 예를 들어:
795 | ```scala
796 | class Person(val data: Map[String, String])
797 | val database = Map[String, Person]
798 | // Sometimes the client can store "null" value in the store "address"
799 |
800 | // A monadic chaining approach
801 | def getAddress(name: String): Option[String] = {
802 | database.get(name).flatMap { elem =>
803 | elem.data.get("address")
804 | .flatMap(Option.apply) // handle null value
805 | }
806 | }
807 |
808 | // A more readable approach, despite much longer
809 | def getAddress(name: String): Option[String] = {
810 | if (!database.contains(name)) {
811 | return None
812 | }
813 |
814 | database(name).data.get("address") match {
815 | case Some(null) => None // handle null value
816 | case Some(addr) => Option(addr)
817 | case None => None
818 | }
819 | }
820 |
821 | ```
822 |
823 |
824 | ## 동시성 제어
825 |
826 | ### Scala concurrent.Map
827 |
828 | __`java.util.concurrent.ConcurrentHashMap` 이 `scala.collection.concurrent.Map` 보다 권장됩니다__. 특히, `scala.collection.concurrent.Map`의 `getOrElseUpdate` 함수는 atomic하지 않습니다 (이는 Scala 2.11.6에서 고쳐졌습니다. [SI-7943](https://issues.scala-lang.org/browse/SI-7943)). 우리가 관리하고 있는 모든 프로젝트에서는 Scala 2.10과 Scala 2.11의 크로스 빌딩을 하기 때문에 `scala.collection.concurrent.Map`의 사용은 피해야 합니다.
829 |
830 | ### 동기화(synchronized) 명시 vs Java 제공 동시성 라이브러리
831 |
832 | 동시성을 제어하기 위해서 3가지의 추천하는 방법이 있습니다. __섞어서 사용하지 않습니다__, 왜냐하면 이는 프로그램을 더욱 복잡하게 하고 데드락을 일으킬 수 있기 때문입니다.
833 |
834 | 1. `java.util.concurrent.ConcurrentHashMap`: 모든 상태가 map에 저장 되고, 빈번한 접근이 일어 날 때 사용합니다.
835 | ```scala
836 | private[this] val map = new java.util.concurrent.ConcurrentHashMap[String, String]
837 | ```
838 |
839 | 2. `java.util.Collections.synchronizedMap`: 모든 상태가 map에 저장되고, 빈번한 접근이 일어나지 않지만 코드를 안전하게 만들고 싶을 때 사용합니다. 만약 아무런 동시성 접근이 일어나지 않는다면, JVM JIT 컴파일러는 동기화의 오버헤드를 지울 수 있습니다.
840 | ```scala
841 | private[this] val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])
842 | ```
843 |
844 | 3. 명시적으로 동기화를 하는 방법: 이 방법은 여러 변수들을 동시성 제어를 할 수 있도록 합니다. 2번과 비슷하게 JVM JIT 컴파일러가 동기화의 오버헤드를 지울 수 있습니다.
845 | ```scala
846 | class Manager {
847 | private[this] var count = 0
848 | private[this] val map = new java.util.HashMap[String, String]
849 | def update(key: String, value: String): Unit = synchronized {
850 | map.put(key, value)
851 | count += 1
852 | }
853 | def getCount: Int = synchronized { count }
854 | }
855 | ```
856 |
857 | 1번의 경우와 2번의 경우, 값을 읽거나 이터레이터로 해당 콜랙션에 접근시, 이 보호된 영역에서 빠져나오게 되는 것을 주의 합니다. 이는 종종 `Map.keySet`이나 `Map.values`을 사용 하는 경우 벌어지게 됩니다. 만약 값을 읽거나 값들이 루프를 돌아야 하는 경우, 복사본을 만들어 사용하도록 합니다.
858 | ```scala
859 | val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])
860 |
861 | // This is broken!
862 | def values: Iterable[String] = map.values
863 |
864 | // Instead, copy the elements
865 | def values: Iterable[String] = map.synchronized { Seq(map.values: _*) }
866 | ```
867 |
868 | ### 동기화(synchronized) 명시 vs Atomic 변수 vs @volatile
869 |
870 | `java.util.concurrent.atomic` 페키지는 원시타입을 원자적으로 읽고 쓸 수 있는 API를 제공 합니다(예를 들어 `AtomicBoolean`, `AtomicInteger` 와 `AtomicReference`).
871 |
872 | 항상 `@volatile` 보다는 이를 사용한 변수들을 사용 하는 것이 권고됩니다. 이들은 많은 기능들을 제공하며, 코드의 가독성을 증가시켜 줍니다. 이 변수들은 내부적으로 `@volatile`을 사용하여 구현되어있습니다.
873 |
874 | 이 명시적인 동기화 보다는 Atomic 변수를 사용하는 것이 권장되는 경우가 몇 가지 있습니다: (1) 어떤 객체의 모든 중요한 갱신이 하나의 *단일* 변수에 존재 할 때 그리고 동시성 접근이 예상 될 때. 이 변수들은 원자적으로 동작하기 때문에 효과적인 동시성 제어를 제공합니다. 혹은 (2) 동기화가 명확하게 `getAndSet` 함수로 표현 될 수 있을 때. 예를 들어:
875 | ```scala
876 | // good: clearly and efficiently express only-once execution of concurrent code
877 | val initialized = new AtomicBoolean(false)
878 | ...
879 | if (!initialized.getAndSet(true)) {
880 | ...
881 | }
882 |
883 | // poor: less clear what is guarded by synchronization, may unnecessarily synchronize
884 | val initialized = false
885 | ...
886 | var wasInitialized = false
887 | synchronized {
888 | wasInitialized = initialized
889 | initialized = true
890 | }
891 | if (!wasInitialized) {
892 | ...
893 | }
894 | ```
895 |
896 | ### Private 변수
897 |
898 | `private` 변수들이 외부 같은 클래스의 다른 객체들로부터 접근이 가능하다는 것을 주의하시기 바랍니다. 따라서, 이를 `this.synchronized` (혹은 `synchronized`) 으로 보호하는 것은 기술적으로 충분하지 않습니다. 그 대신, `private[this]`를 사용하시기 바랍니다.
899 | ```scala
900 | // The following is still unsafe.
901 | class Foo {
902 | private var count: Int = 0
903 | def inc(): Unit = synchronized { count += 1 }
904 | }
905 |
906 | // The following is safe.
907 | class Foo {
908 | private[this] var count: Int = 0
909 | def inc(): Unit = synchronized { count += 1 }
910 | }
911 | ```
912 |
913 |
914 | ### 동시성 로직 분리
915 |
916 | 일반적으로, 동시성과 동기화 로직은 최대한 분리되고 독립적이어야 합니다. 이 것은 다음을 의미합니다:
917 |
918 | - API레벨에서 유저에게 노출된 함수나 콜백함수에 이 동기화 변수들을 노출 하는 것을 피합니다.
919 | - 복잡한 모듈에서는 작은 내부 모듈을 만들어 동시성을 위한 변수들을 갖고 있도록 합니다.
920 |
921 |
922 | ## 성능
923 |
924 | 대부분의 코드는 보통 성능에 대하여 크게 고려되지 않습니다. 성능 향상을 위한 코드를 위해서 몇 가지 팁이 있습니다.
925 |
926 | ### Microbenchmarks
927 |
928 | 좋은 microbenchmark를 작성하는 것은 아주 어려운 일 입니다, 왜냐하면 Scala 컴파일러와 JVM JIT 컴파일러는 많은 마법과 같은 일을 코드에 하기 때문입니다. 왜냐하면 대게의 경우 microbenchmark는 측정하고자 하는 것을 측정하지 않습니다.
929 |
930 | microbenchmark를 작성하려면 [jmh](http://openjdk.java.net/projects/code-tools/jmh/)를 사용하시기 바랍니다. "죽은 코드" 제거와 상수값 대체 그리고 루프 풀기를 이해하기 위해 직접 [모든 샘플](http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/)을 읽도록 합니다.
931 |
932 | ### 순회와 zipWithIndex
933 |
934 | `for`나 혹은 functional transformations (예를 들어, `map` 혹은 `foreach`) 보다는 `while` 루프를 를 사용하기를 권장합니다. For 루프나 functional transformations은 상당히 느립니다(이유는 가상 함수 호출과 boxing 때문입니다).
935 | ```scala
936 |
937 | val arr = // array of ints
938 | // zero out even positions
939 | val newArr = list.zipWithIndex.map { case (elem, i) =>
940 | if (i % 2 == 0) 0 else elem
941 | }
942 |
943 | // This is a high performance version of the above
944 | val newArr = new Array[Int](arr.length)
945 | var i = 0
946 | val len = newArr.length
947 | while (i < len) {
948 | newArr(i) = if (i % 2 == 0) 0 else arr(i)
949 | i += 1
950 | }
951 | ```
952 |
953 | ### Option 과 null
954 |
955 | 성능을 고려한 코드를 위해, 가상 함수 호출과 boxing을 피하는 `Option`보다는 `null`의 사용이 권장됩니다. Null을 갖을 수 있는 변수에는 Nullable 이라고 label을 확실히 하도록 합니다.
956 |
957 | ```scala
958 | class Foo {
959 | @javax.annotation.Nullable
960 | private[this] var nullableField: Bar = _
961 | }
962 | ```
963 |
964 | ### Scala Collection 라이브러리
965 |
966 | 성능을 고려한 코드를 위해, Scala의 라이브러리 사용 보다는 Java의 collection 라이브러리 사용이 권장됩니다. 이는 Scala의 라이브러리가 종종 Java 라이브러리 보다 느리기 때문입니다.
967 |
968 | ### private[this]
969 |
970 | 성능을 고려한 코드를 위해, `private` 보다는 `private[this]`이 권장됩니다. `private[this]`는 접근자 함수를 생성하지 않고 하나의 변수만 생성합니다. 우리의 경험으로는 JVM JIT 컴파일러는 항상 `private` 변수를 한 번에 (하나의 정의로) 처리하지 못하였습니다. 따라서 해당 변수에 접근 할 가상 함수 호출을 없애기 위해서 `private[this]` 을 사용하는 것이 더 안전합니다.
971 | ```scala
972 | class MyClass {
973 | private val field1 = ...
974 | private[this] val field2 = ...
975 |
976 | def perfSensitiveMethod(): Unit = {
977 | var i = 0
978 | while (i < 1000000) {
979 | field1 // This might invoke a virtual method call
980 | field2 // This is just a field access
981 | i += 1
982 | }
983 | }
984 | }
985 | ```
986 |
987 |
988 | ## Java 호환성
989 |
990 | 이 항목은 Java 호환 가능한 API를 만들기 위한 가이드라인을 다루고 있습니다. 이 것은 현재 당신이 만들고 있는 컴포넌트가 Java와의 호환성을 필요로 하지 않는다면 적용되지 않습니다. 이 가이드라인은 주로 우리가 Spark의 Java API를 만드는 과정에서 우리가 경험한 것을 바탕으로 작성되었습니다.
991 |
992 | ### Scala에서 사용 할 수 없는 Java 기능
993 |
994 | 아래의 Java특징들은 Scala에 없습니다. 만약 아래의 기능이 필요하다면 Java에서 정의하여 사용하시기 바랍니다. 그러나 Scala 문서를 보시면 Java로 정의된 파일에 대한 보장은 하지 않는다고 명시되어 있습니다.
995 |
996 | - Static 변수
997 | - Static 내부 클래스
998 | - Java enum
999 | - Annotation
1000 |
1001 |
1002 | ### Traits 와 Abstract 클래스
1003 |
1004 | 외부에서 구현 될 수 있는 인터페이스의 경우 아래의 항목을 명심하시길 바랍니다:
1005 |
1006 | - Trait에 있는 기본으로 정의되어 있는 함수들은 Java에서 사용 할 수 없습니다. 대신 추상 클래스를 사용하시기 바랍니다.
1007 | - 일반적으로 trait의 사용을 피하시길 바랍니다. 단, 인터페이스가 미래의 어떤 경우에도 어떠한 정의된 구현을 사용하지 않는 다는 것을 확신 할 수 있다면 사용 할 수 있습니다.
1008 | ```scala
1009 | // The default implementation doesn't work in Java
1010 | trait Listener {
1011 | def onTermination(): Unit = { ... }
1012 | }
1013 |
1014 | // Works in Java
1015 | abstract class Listener {
1016 | def onTermination(): Unit = { ... }
1017 | }
1018 | ```
1019 |
1020 |
1021 | ### Type 별칭
1022 |
1023 | 별칭을 사용하지 않습니다. 이들은 바이트코드와 Java에서 보여지지 않습니다.
1024 |
1025 |
1026 | ### 기본 매개변수 값
1027 |
1028 | 인자에 기본값을 주어 사용하지 않습니다. 대신 함수를 오버로드 합니다.
1029 | ```scala
1030 | // Breaks Java interoperability
1031 | def sample(ratio: Double, withReplacement: Boolean = false): RDD[T] = { ... }
1032 |
1033 | // The following two work
1034 | def sample(ratio: Double, withReplacement: Boolean): RDD[T] = { ... }
1035 | def sample(ratio: Double): RDD[T] = sample(ratio, withReplacement = false)
1036 | ```
1037 |
1038 | ### 다중 매개변수 표기
1039 |
1040 | 여러 인자를 리스트로 묶어 사용하지 않습니다.
1041 |
1042 | ### 가변인자
1043 |
1044 | - varargs 함수가 Java에서 사용 될 수 있도록 `@scala.annotation.varargs` 어노테이션을 적용합니다. Scala 컴파일러는 하나는 Scala를 위해(바이트코드 인자는 Seq 입니다) 다른 하나는 Java를 위해 (바이트코드 인자는 배열 입니다) 총 두개의 함수를 만듭니다.
1045 | ```scala
1046 | @scala.annotation.varargs
1047 | def select(exprs: Expression*): DataFrame = { ... }
1048 | ```
1049 |
1050 | - 추상 varargs 함수는 Java에서 작동하지 않습니다. 이는 Scala의 버그 때문입니다([SI-1459](https://issues.scala-lang.org/browse/SI-1459), [SI-9013](https://issues.scala-lang.org/browse/SI-9013)).
1051 |
1052 | - varargs 함수들을 오버로딩할 때 조심하도록 합니다. varargs 함수를 다른 varargs 타입과 오버로딩 하는 것은 소스의 호환성을 보장하지 않습니다.
1053 | ```scala
1054 | class Database {
1055 | @scala.annotation.varargs
1056 | def remove(elems: String*): Unit = ...
1057 |
1058 | // Adding this will break source compatibility for no-arg remove() call.
1059 | @scala.annotation.varargs
1060 | def remove(elems: People*): Unit = ...
1061 | }
1062 |
1063 | // This won't compile anymore because it is ambiguous
1064 | new Database().remove()
1065 | ```
1066 | 대신, 명시적 타입을 갖는 인자를 처음 오게 합니다:
1067 | ```scala
1068 | class Database {
1069 | @scala.annotation.varargs
1070 | def remove(elems: String*): Unit = ...
1071 |
1072 | // The following is OK.
1073 | @scala.annotation.varargs
1074 | def remove(elem: People, elems: People*): Unit = ...
1075 | }
1076 | ```
1077 |
1078 |
1079 | ### Implicits
1080 |
1081 | 클래스나 함수를 위해 implicit을 사용하지 않습니다. 이는 `ClassTag`, `TypeTag` 를 포함합니다.
1082 | ```scala
1083 | class JavaFriendlyAPI {
1084 | // This is NOT Java friendly, since the method contains an implicit parameter (ClassTag).
1085 | def convertTo[T: ClassTag](): T
1086 | }
1087 | ```
1088 |
1089 | ### 관련 객체, 정적 함수 및 변수
1090 |
1091 | 동반하는 객체들과 정적 함수/변수 들을 사용 할 때, 몇 가지 조심해야 할 부분이 있습니다.
1092 |
1093 | - 동반(companion) 객체들은 Java에서 사용하기에는 조금 어색합니다(동반(companion) 객체 `Foo` 는 `Foo$` 클래스의 `Foo$` 타입의 `MODULE$` 정적 변수 입니다).
1094 | ```scala
1095 | object Foo
1096 |
1097 | // equivalent to the following Java code
1098 | public class Foo$ {
1099 | Foo$ MODULE$ = // instantiation of the object
1100 | }
1101 | ```
1102 | 만약 동반(companion) 객체를 사용해야 한다면, Java 정적 변수를 다른 클래스에 만듭니다.
1103 |
1104 | - 불행히도, JVM 정적 변수를 Scala에서 정의하는 방법은 없습니다. Java파일을 만들어 이를 정의하는데 사용하도록 합니다.
1105 | - 동반(companion) 객체의 함수들은 자동으로 동반(companion) 클래스의 정적 함수로 변하게 됩니다. 단, 같은 함수가 존재하지 않아야 합니다. 정적 함수의 생성이 보장 되도록 하는 가장 좋은 방법은 Java테스트 파일을 작성하여 이 정적 함수를 호출하는 것 입니다.
1106 | ```scala
1107 | class Foo {
1108 | def method2(): Unit = { ... }
1109 | }
1110 |
1111 | object Foo {
1112 | def method1(): Unit = { ... } // a static method Foo.method1 is created in bytecode
1113 | def method2(): Unit = { ... } // a static method Foo.method2 is NOT created in bytecode
1114 | }
1115 |
1116 | // FooJavaTest.java (in test/scala/com/databricks/...)
1117 | public class FooJavaTest {
1118 | public static void compileTest() {
1119 | Foo.method1(); // This one should compile fine
1120 | Foo.method2(); // This one should fail because method2 is not generated.
1121 | }
1122 | }
1123 | ```
1124 |
1125 | - 하나의 case 객체 (혹은 심지어 보통 동반(companion) 객체) MyClass는 사실 MyClass 타입이 아닙니다.
1126 | ```scala
1127 | case object MyClass
1128 |
1129 | // Test.java
1130 | if (MyClass$.MODULE instanceof MyClass) {
1131 | // The above condition is always false
1132 | }
1133 | ```
1134 | 이를 적절한 타입 구조를 갖을 수 있도록 구현하기 위해서 동반(companion) 클래스를 정의하고, 이를 case 객체에서 상속 받도록 합니다:
1135 | ```scala
1136 | class MyClass
1137 | case object MyClass extends MyClass
1138 | ```
1139 |
1140 | ## 테스트
1141 |
1142 | ### 예외 가로 채기
1143 |
1144 | 특정한 예외를 발생 시키는 행동을 테스트 할 때는 (예를 들어, 잘못된 인자를 주어 함수를 호출 하는 것), 가능한 한 예외의 타입을 구체적으로 명시 하도록 합니다. (ScalaTest를 사용하는 경우) 단순히 `intercept[Exception]` 이나 `intercept[Throwable]` 을 해서는 안됩니다. 왜냐하면, 이 것은 _모든_ 타입의 예외가 발생 했다는 것을 체크하기 때문입니다. 이 경우, 만들어진 테스트들은 오류가 발생했다는 것만 확인 하고, 실제 확인해야 하는 행동을 확인하지 않은채 조용히 통과 할 것 입니다.
1145 |
1146 | ```scala
1147 | // 잘못된 경우
1148 | intercept[Exception] {
1149 | thingThatThrowsException()
1150 | }
1151 |
1152 | // 올바른 경우
1153 | intercept[MySpecificTypeOfException] {
1154 | thingThatThrowsException()
1155 | }
1156 | ```
1157 |
1158 | 만약 예외의 타입이 구체적으로 명시 될 수 없다면, 코드 스멜의 징후일 수 있습니다. 낮은 레벨의 테스트를 하거나 구체적인 타입의 예외를 발생시키도록 해당 코드를 수정 해야 합니다.
1159 |
1160 | ## 기타
1161 |
1162 | ### currentTimeMillis 보다는 nanoTime
1163 |
1164 | *지속 시간*을 계산할 때 혹은 *타임아웃*을 확인 할 때에는, 심지어 millisecond 이하의 숫자들이 필요 없는 경우에도 `System.currentTimeMillis()`의 사용을 피하시고 `System.nanoTime()`을 사용 하시길 바랍니다.
1165 |
1166 | `System.currentTimeMillis()`는 현재 시간을 반환하고 현재 시스템의 클록을 뒤따라 바꿉니다. 따라서 이러한 네거티브 클록 조정은 긴 시간의 타임아웃을 초래할 수 있습니다(클록 시간 이전 값으로 잡을 때 까지). 이 것은 네트워크가 장 시간 중단 된 후에, ntpd가 다음 "step"으로 진행할 때 발생 될 수 있습니다. 가장 전형적인 예로는 시스템 부팅 동안 DHCP 시간이 평소보다 오래 소요될 때 입니다. 이는, 이해하거나 재현하기 힘든 에러를 초래 할수 있습니다. `System.nanoTime()`은 wall-clock에 상관 없이, 항상 일정하게 증가 합니다.
1167 |
1168 | 주의:
1169 | - 절대 `nanoTime()`의 절대값을 절대로 직렬화 하거나 다른 시스템으로 보내지 않습니다. 이 절대값은 의미가 없으며, 시스템 관련 값이고, 시스템이 재부팅 되면 리셋됩니다.
1170 | - 절대 `nanoTime()`의 절대값은 양수로 보장되지 않습니다(하지만 `t2 - t1` 은 올바른 값을 계산하도록 보장 됩니다).
1171 | - `nanoTime()`은 292년을 주기로 다시 계산합니다. 따라서 만약 Spark 작업(job)이 아주 긴 시간이 걸릴 것으로 예상된다면, 다른 무언가를 찾아야 하겠죠 :)
1172 |
1173 | ### URL 보다는 URI
1174 |
1175 | 어떤 서비스의 URL을 정렬 할 때, `URI` 표현을 사용하는 것이 권장됩니다.
1176 |
1177 | `URL`의 [동일성 검사](http://docs.oracle.com/javase/7/docs/api/java/net/URL.html#equals(java.lang.Object)) 는 사실 IP 주소를 알아내기 위해 (블로킹) 네트워크 호출을 합니다. `URI` 클래스는 필드의 동일성을 확인하고 `URL`의 상위 집합 입니다.
1178 |
1179 | ### 이미 존재 하는 함수를 다시 개발하는 것 보다는 기존의 잘 테스트 된 함수 사용
1180 |
1181 | 이미 존재하며 잘 테스트 되어있는 함수가 있고 이 함수가 어떤 성능 문제도 갖고 있지 않을 때에는, 이를 사용 하도록 합니다. 이러한 함수를 다시 구현하면 버그가 발생할 수 있으며, 이를 테스트하는데 시간이 필요합니다 (어쩌면 이 함수를 테스트 해야 한다는 것을 잊어버릴 수도 있습니다!).
1182 |
1183 |
1184 | ```scala
1185 | val beginNs = System.nanoTime()
1186 | // 시간 측정을 위한 일을 합니다.
1187 | Thread.sleep(1000)
1188 | val elapsedNs = System.nanoTime() - beginNs
1189 |
1190 | // 아래 예와 같이 하지 않습니다. 아래는 매직 넘버를 사용하고 있어서 쉽게 실수 할 수 있습니다.
1191 | val elapsedMs = elapsedNs / 1000 / 1000
1192 |
1193 | // 아래 예와 같이 Java의 TimeUnit API 를 사용합니다.
1194 | import java.util.concurrent.TimeUnit
1195 | val elapsedMs2 = TimeUnit.NANOSECONDS.toMillis(elapsedNs)
1196 |
1197 | // 아래 예와 같이 Scala의 Duration API를 사용합니다.
1198 | import scala.concurrent.duration._
1199 | val elapsedMs3 = elapsedNs.nanos.toMillis
1200 | ```
1201 |
1202 | 예외 경우:
1203 | - 이미 잘 테스트 되어있는 함수를 사용하기위해 새로운 종속성(dependency)을 추가해야 하는 경우, 만약 이러한 함수가 간단한 편이라면, 다시 구현하는 것이 새로운 종속성을 추가하는 것 보다 낫습니다. 하지만, 테스트를 해야 된다는 것을 잊지 말아야 합니다.
1204 | - 기존의 함수가 사용 용도에 최적화 되어 있지 않고 느린 경우. 이러한 경우에는 벤치마킹을 먼저 하고, 너무 이른 최적화는 피하도록 합니다.
1205 |
--------------------------------------------------------------------------------
/README-ZH.md:
--------------------------------------------------------------------------------
1 | # Databricks Scala 编程风格指南
2 |
3 | ## 声明 (Disclaimer)
4 |
5 | The Chinese version of the [Databricks Scala Guide](https://github.com/databricks/scala-style-guide) is contributed and maintained by community member [Hawstein](https://github.com/Hawstein). We do not guarantee that it will always be kept up-to-date.
6 |
7 | 本文档翻译自 [Databricks Scala Guide](https://github.com/databricks/scala-style-guide),目前由 [Hawstein](https://github.com/Hawstein) 进行维护。由于是利用业余时间进行翻译并维护,因此该中文文档并不保证总是与[原文档](https://github.com/databricks/scala-style-guide)一样处于最新版本,不过我会尽可能及时地去更新它。
8 |
9 | ## 前言
10 |
11 | Spark 有超过 1000 位贡献者,就我们所知,应该是目前大数据领域里最大的开源项目且是最活跃的 Scala 项目。这份指南是在我们指导,或是与 Spark 贡献者及 [Databricks](http://databricks.com/) 工程团队一起工作时总结出来的。
12 |
13 | 代码由作者 __一次编写__ ,然后由大量工程师 __多次阅读并修改__ 。事实上,大部分的 bug 来源于后人对代码的修改,因此我们需要长期去优化我们的代码,提升代码的可读性和可维护性。达到这个目标最好的方式就是编写简单易懂的代码。
14 |
15 | Scala 是一种强大到令人难以置信的多范式编程语言。我们总结出了以下指南,它可以很好地应用在一个高速发展的项目。当然,这个指南并非绝对,根据团队需求的不同,可以有不同的标准。
16 |
17 | 
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
18 |
19 | ## 目录
20 |
21 | 1. [文档历史](#history)
22 |
23 | 1. [语法风格](#syntactic)
24 | - [命名约定](#naming)
25 | - [变量命名约定](#variable-naming)
26 | - [一行长度](#linelength)
27 | - [30 法则](#rule_of_30)
28 | - [空格与缩进](#indent)
29 | - [空行](#blanklines)
30 | - [括号](#parentheses)
31 | - [大括号](#curly)
32 | - [长整型字面量](#long_literal)
33 | - [文档风格](#doc)
34 | - [类内秩序](#ordering_class)
35 | - [Imports](#imports)
36 | - [模式匹配](#pattern-matching)
37 | - [中缀方法](#infix)
38 | - [匿名方法](#anonymous)
39 |
40 | 1. [Scala 语言特性](#lang)
41 | - [样例类与不可变性](#case_class_immutability)
42 | - [apply 方法](#apply_method)
43 | - [override 修饰符](#override_modifier)
44 | - [解构绑定](#destruct_bind)
45 | - [按名称传参](#call_by_name)
46 | - [多参数列表](#multi-param-list)
47 | - [符号方法 (运算符重载)](#symbolic_methods)
48 | - [类型推导](#type_inference)
49 | - [Return 语句](#return)
50 | - [递归及尾递归](#recursion)
51 | - [Implicits](#implicits)
52 | - [异常处理 (Try 还是 try)](#exception)
53 | - [Options](#option)
54 | - [单子链接](#chaining)
55 |
56 | 1. [并发](#concurrency)
57 | - [Scala concurrent.Map](#concurrency-scala-collection)
58 | - [显式同步 vs 并发集合](#concurrency-sync-vs-map)
59 | - [显式同步 vs 原子变量 vs @volatile](#concurrency-sync-vs-atomic)
60 | - [私有字段](#concurrency-private-this)
61 | - [隔离](#concurrency-isolation)
62 |
63 | 1. [性能](#perf)
64 | - [Microbenchmarks](#perf-microbenchmarks)
65 | - [Traversal 与 zipWithIndex](#perf-whileloops)
66 | - [Option 与 null](#perf-option)
67 | - [Scala 集合库](#perf-collection)
68 | - [private[this]](#perf-private)
69 |
70 | 1. [与 Java 的互操作性](#java)
71 | - [Scala 中缺失的 Java 特性](#java-missing-features)
72 | - [Traits 与抽象类](#java-traits)
73 | - [类型别名](#java-type-alias)
74 | - [默认参数值](#java-default-param-values)
75 | - [多参数列表](#java-multi-param-list)
76 | - [可变参数](#java-varargs)
77 | - [Implicits](#java-implicits)
78 | - [伴生对象, 静态方法与字段](#java-companion-object)
79 |
80 | 1. [测试](#testing)
81 | - [异常拦截](#testing-intercepting)
82 |
83 | 1. [其它](#misc)
84 | - [优先使用 nanoTime 而非 currentTimeMillis](#misc_currentTimeMillis_vs_nanoTime)
85 | - [优先使用 URI 而非 URL](#misc_uri_url)
86 | - [优先使用现存的经过良好测试的方法而非重新发明轮子](#misc_well_tested_method)
87 |
88 | ## 文档历史
89 | - 2015-03-16: 最初版本。
90 | - 2015-05-25: 增加 [override 修饰符](#override_modifier) 一节。
91 | - 2015-08-23: 把一些规则的严重程度从「不要」降级到「避免」。
92 | - 2015-11-17: 更新 [apply 方法](#apply_method) 一节:伴生对象中的 apply 方法应该返回其伴生类。
93 | - 2015-11-17: 该指南被翻译成[中文](README-ZH.md),由 [Hawstein](https://github.com/Hawstein) 进行维护,中文文档并不保证总是与原文档一样处于最新版本。
94 | - 2015-12-14: 该指南被翻译成[韩文](README-KO.md), 韩文版本由 [Hyukjin Kwon](https://github.com/HyukjinKwon) 进行翻译并且由 [Yun Park](https://github.com/yunpark93), [Kevin (Sangwoo) Kim](https://github.com/swkimme), [Hyunje Jo](https://github.com/RetrieverJo) 和 [Woochel Choi](https://github.com/socialpercon) 进行校对。韩文版本并不保证总是与原文档一样处于最新版本。
95 | - 2016-06-15: 增加 [匿名方法](#anonymous) 一节。
96 | - 2016-06-21: 增加 [变量命名约定](#variable-naming) 一节。
97 | - 2016-12-24: 增加 [样例类与不可变性](#case_class_immutability) 一节。
98 | - 2017-02-23: 增加 [测试](#testing) 一节。
99 | - 2017-04-18: 增加 [优先使用现存的经过良好测试的方法而非重新发明轮子](#misc_well_tested_method) 一节。
100 |
101 | ## 语法风格
102 |
103 | ### 命名约定
104 |
105 | 我们主要遵循 Java 和 Scala 的标准命名约定。
106 |
107 | - 类,trait, 对象应该遵循 Java 中类的命名约定,即 PascalCase 风格。
108 |
109 | ```scala
110 | class ClusterManager
111 |
112 | trait Expression
113 | ```
114 |
115 | - 包名应该遵循 Java 中包名的命名约定,即使用全小写的 ASCII 字母。
116 |
117 | ```scala
118 | package com.databricks.resourcemanager
119 | ```
120 |
121 | - 方法/函数应当使用驼峰式风格命名。
122 |
123 | - 常量命名使用全大写字母,并将它们放在伴生对象中。
124 |
125 | ```scala
126 | object Configuration {
127 | val DEFAULT_PORT = 10000
128 | }
129 | ```
130 |
131 | - 枚举命名与类命名一致,使用 PascalCase 风格。
132 |
133 | - 注解也应遵循 Java 中的约定,即使用 PascalCase 风格。注意,这一点与 Scala 的官方指南不同。
134 |
135 | ```scala
136 | final class MyAnnotation extends StaticAnnotation
137 | ```
138 |
139 | ### 变量命名约定
140 |
141 | - 变量命名应当遵循驼峰式命名方法,并且变量名应当是不言而喻的,即变量名可以直观地反应它的涵义。
142 | ```scala
143 | val serverPort = 1000
144 | val clientPort = 2000
145 | ```
146 |
147 | - 可以在小段的局部代码中使用单字符的变量名,比如在小段的循环体中(例如 10 行以内的代码),“i” 常常被用作循环索引。然而,即使在小段的代码中,也不要使用 “l” (Larry 中的 l)作为标识符,因为它看起来和 “1”,“|”,“I” 很像,难以区分,容易搞错。
148 |
149 | ### 一行长度
150 |
151 | - 一行长度的上限是 100 个字符。
152 | - 唯一的例外是 import 语句和 URL (即便如此,也尽量将它们保持在 100 个字符以下)。
153 |
154 |
155 | ### 30 法则
156 |
157 | 「如果一个元素包含的子元素超过 30 个,那么极有可能出现了严重的问题」 - [Refactoring in Large Software Projects](http://www.amazon.com/Refactoring-Large-Software-Projects-Restructurings/dp/0470858923)。
158 |
159 | 一般来说:
160 |
161 | - 一个方法包含的代码行数不宜超过 30 行。
162 | - 一个类包含的方法数量不宜超过 30 个。
163 |
164 |
165 | ### 空格与缩进
166 |
167 | - 运算符前后保留一个空格,包括赋值运算符。
168 | ```scala
169 | def add(int1: Int, int2: Int): Int = int1 + int2
170 | ```
171 |
172 | - 逗号后保留一个空格。
173 | ```scala
174 | Seq("a", "b", "c") // 使用这种方式
175 |
176 | Seq("a","b","c") // 不要忽略逗号后的空格
177 | ```
178 |
179 | - 冒号后保留一个空格。
180 | ```scala
181 | // 使用这种方式
182 | def getConf(key: String, defaultValue: String): String = {
183 | // some code
184 | }
185 |
186 | // 冒号前不需要使用空格
187 | def calculateHeaderPortionInBytes(count: Int) : Int = {
188 | // some code
189 | }
190 |
191 | // 不要忽略冒号后的空格
192 | def multiply(int1:Int, int2:Int): Int = int1 * int2
193 | ```
194 |
195 | - 一般情况下,使用两个空格的缩进。
196 |
197 | ```scala
198 | if (true) {
199 | println("Wow!")
200 | }
201 | ```
202 |
203 | - 对于方法声明,如果一行无法容纳下所有的参数,那么使用 4 个空格来缩进它们。返回类型可以与最后一个参数在同一行,也可以放在下一行,使用两个空格缩进。
204 |
205 | ```scala
206 | def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
207 | path: String,
208 | fClass: Class[F],
209 | kClass: Class[K],
210 | vClass: Class[V],
211 | conf: Configuration = hadoopConfiguration): RDD[(K, V)] = {
212 | // method body
213 | }
214 |
215 | def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
216 | path: String,
217 | fClass: Class[F],
218 | kClass: Class[K],
219 | vClass: Class[V],
220 | conf: Configuration = hadoopConfiguration)
221 | : RDD[(K, V)] = {
222 | // method body
223 | }
224 | ```
225 |
226 | - 如果一行无法容纳下类头(即 extends 后面那部分),则把它们放到新的一行,用两个空格缩进,然后在类内空一行再开始函数或字段的定义(或是包的导入)。
227 |
228 | ```scala
229 | class Foo(
230 | val param1: String, // 4 space indent for parameters
231 | val param2: String,
232 | val param3: Array[Byte])
233 | extends FooInterface // 2 space here
234 | with Logging {
235 |
236 | def firstMethod(): Unit = { ... } // blank line above
237 | }
238 | ```
239 |
240 | - 不要使用垂直对齐。它使你的注意力放在代码的错误部分并增大了后人修改代码的难度。
241 |
242 | ```scala
243 | // Don't align vertically
244 | val plus = "+"
245 | val minus = "-"
246 | val multiply = "*"
247 |
248 | // Do the following
249 | val plus = "+"
250 | val minus = "-"
251 | val multiply = "*"
252 | ```
253 |
254 |
255 | ### 空行
256 |
257 | - 一个空行可以出现在:
258 | - 连续的类成员或初始化器(initializers)之间:字段,构造函数,方法,嵌套类,静态初始化器及实例初始化器。
259 | - 例外:连续的两个字段之间的空行是可选的(前提是它们之间没有其它代码),这一类空行主要为这些字段做逻辑上的分组。
260 | - 在方法体内,根据需要,使用空行来为语句创建逻辑上的分组。
261 | - 在类的第一个成员之前或最后一个成员之后,空行都是可选的(既不鼓励也不阻止)。
262 | - 使用一个或两个空行来分隔不同类的定义。
263 | - 不鼓励使用过多的空行。
264 |
265 |
266 | ### 括号
267 |
268 | - 方法声明应该加括号(即使没有参数列表),除非它们是没有副作用(状态改变,IO 操作都认为是有副作用的)的访问器(accessor)。
269 |
270 | ```scala
271 | class Job {
272 | // Wrong: killJob changes state. Should have ().
273 | def killJob: Unit
274 |
275 | // Correct:
276 | def killJob(): Unit
277 | }
278 | ```
279 |
280 | - 函数调用应该与函数声明在形式上保持一致,也就是说,如果一个方法声明时带了括号,那调用时也要把括号带上。注意这不仅仅是语法层面的人为约定,当返回对象中定义了 `apply` 方法时,这一点还会影响正确性。
281 |
282 | ```scala
283 | class Foo {
284 | def apply(args: String*): Int
285 | }
286 |
287 | class Bar {
288 | def foo: Foo
289 | }
290 |
291 | new Bar().foo // This returns a Foo
292 | new Bar().foo() // This returns an Int!
293 | ```
294 |
295 |
296 | ### 大括号
297 |
298 | 即使条件语句或循环语句只有一行时,也请使用大括号。唯一的例外是,当你把 if/else 作为一个单行的三元操作符来使用并且没有副作用时,这时你可以不加大括号。
299 |
300 | ```scala
301 | // Correct:
302 | if (true) {
303 | println("Wow!")
304 | }
305 |
306 | // Correct:
307 | if (true) statement1 else statement2
308 |
309 | // Correct:
310 | try {
311 | foo()
312 | } catch {
313 | ...
314 | }
315 |
316 | // Wrong:
317 | if (true)
318 | println("Wow!")
319 |
320 | // Wrong:
321 | try foo() catch {
322 | ...
323 | }
324 | ```
325 |
326 |
327 | ### 长整型字面量
328 |
329 | 长整型字面量使用大写的 `L` 作为后缀,不要使用小写,因为它和数字 `1` 长得很像,常常难以区分。
330 |
331 | ```scala
332 | val longValue = 5432L // Do this
333 |
334 | val longValue = 5432l // Do NOT do this
335 | ```
336 |
337 |
338 | ### 文档风格
339 |
340 | 使用 Java Doc 风格,而非 Scala Doc 风格。
341 |
342 | ```scala
343 | /** This is a correct one-liner, short description. */
344 |
345 | /**
346 | * This is correct multi-line JavaDoc comment. And
347 | * this is my second line, and if I keep typing, this would be
348 | * my third line.
349 | */
350 |
351 | /** In Spark, we don't use the ScalaDoc style so this
352 | * is not correct.
353 | */
354 | ```
355 |
356 |
357 | ### 类内秩序
358 |
359 | 如果一个类很长,包含许多的方法,那么在逻辑上把它们分成不同的部分并加上注释头,以此组织它们。
360 |
361 | ```scala
362 | class DataFrame {
363 |
364 | ///////////////////////////////////////////////////////////////////////////
365 | // DataFrame operations
366 | ///////////////////////////////////////////////////////////////////////////
367 |
368 | ...
369 |
370 | ///////////////////////////////////////////////////////////////////////////
371 | // RDD operations
372 | ///////////////////////////////////////////////////////////////////////////
373 |
374 | ...
375 | }
376 | ```
377 |
378 | 当然,强烈不建议把一个类写得这么长,一般只有在构建某些公共 API 时才允许这么做。
379 |
380 |
381 | ### Imports
382 |
383 | - __导入时避免使用通配符__, 除非你需要导入超过 6 个实体或者隐式方法。通配符导入会使代码在面对外部变化时不够健壮。
384 | - 始终使用绝对路径来导入包 (如:`scala.util.Random`) ,而不是相对路径 (如:`util.Random`)。
385 | - 此外,导入语句按照以下顺序排序:
386 | * `java.*` 和 `javax.*`
387 | * `scala.*`
388 | * 第三方库 (`org.*`, `com.*`, 等)
389 | * 项目中的类 (对于 Spark 项目,即 `com.databricks.*` 或 `org.apache.spark`)
390 | - 在每一组导入语句内,按照字母序进行排序。
391 | - 你可以使用 IntelliJ 的「import organizer」来自动处理,请使用以下配置:
392 |
393 | ```
394 | java
395 | javax
396 | _______ blank line _______
397 | scala
398 | _______ blank line _______
399 | all other imports
400 | _______ blank line _______
401 | com.databricks // or org.apache.spark if you are working on spark
402 | ```
403 |
404 |
405 | ### 模式匹配
406 |
407 | - 如果整个方法就是一个模式匹配表达式,可能的话,可以把 match 关键词与方法声明放在同一行,以此减少一级缩进。
408 |
409 | ```scala
410 | def test(msg: Message): Unit = msg match {
411 | case ...
412 | }
413 | ```
414 |
415 | - 当以闭包形式调用一个函数时,如果只有一个 case 语句,那么把 case 语句与函数调用放在同一行。
416 |
417 | ```scala
418 | list.zipWithIndex.map { case (elem, i) =>
419 | // ...
420 | }
421 | ```
422 | 如果有多个 case 语句,把它们缩进并且包起来。
423 |
424 | ```scala
425 | list.map {
426 | case a: Foo => ...
427 | case b: Bar => ...
428 | }
429 | ```
430 |
431 | - 如果唯一的目的就是想匹配某个对象的类型,那么不要展开所有的参数来做模式匹配,这样会使得重构变得更加困难,代码更容易出错。
432 |
433 | ```scala
434 | case class Pokemon(name: String, weight: Int, hp: Int, attack: Int, defense: Int)
435 | case class Human(name: String, hp: Int)
436 |
437 | // 不要像下面那样做,因为
438 | // 1. 当 pokemon 加入一个新的字段,我们需要改变下面的模式匹配代码
439 | // 2. 非常容易发生误匹配,尤其是当所有字段的类型都一样的时候
440 | targets.foreach {
441 | case target @ Pokemon(_, _, hp, _, defense) =>
442 | val loss = sys.min(0, myAttack - defense)
443 | target.copy(hp = hp - loss)
444 | case target @ Human(_, hp) =>
445 | target.copy(hp = hp - myAttack)
446 | }
447 |
448 | // 像下面这样做就好多了:
449 | targets.foreach {
450 | case target: Pokemon =>
451 | val loss = sys.min(0, myAttack - target.defense)
452 | target.copy(hp = target.hp - loss)
453 | case target: Human =>
454 | target.copy(hp = target.hp - myAttack)
455 | }
456 | ```
457 |
458 |
459 | ### 中缀方法
460 |
461 | __避免中缀表示法__,除非是符号方法(即运算符重载)。
462 |
463 | ```scala
464 | // Correct
465 | list.map(func)
466 | string.contains("foo")
467 |
468 | // Wrong
469 | list map (func)
470 | string contains "foo"
471 |
472 | // 重载的运算符应该以中缀形式调用
473 | arrayBuffer += elem
474 | ```
475 |
476 | ### 匿名方法
477 |
478 | 对于匿名方法,__避免使用过多的小括号和花括号__。
479 |
480 | ```scala
481 | // Correct
482 | list.map { item =>
483 | ...
484 | }
485 |
486 | // Correct
487 | list.map(item => ...)
488 |
489 | // Wrong
490 | list.map(item => {
491 | ...
492 | })
493 |
494 | // Wrong
495 | list.map { item => {
496 | ...
497 | }}
498 |
499 | // Wrong
500 | list.map({ item => ... })
501 | ```
502 |
503 |
504 | ## Scala 语言特性
505 |
506 | ### 样例类与不可变性
507 |
508 | 样例类(case class)本质也是普通的类,编译器会自动地为它加上以下支持:
509 | - 构造器参数的公有 getter 方法
510 | - 拷贝构造函数
511 | - 构造器参数的模式匹配
512 | - 默认的 toString/hash/equals 实现
513 |
514 | 对于样例类来说,构造器参数不应设为可变的,可以使用拷贝构造函数达到同样的效果。使用可变的样例类容易出错,例如,哈希表中,对象根据旧的哈希值被放在错误的位置上。
515 |
516 | ```scala
517 | // This is OK
518 | case class Person(name: String, age: Int)
519 |
520 | // This is NOT OK
521 | case class Person(name: String, var age: Int)
522 |
523 | // 通过拷贝构造函数创建一个新的实例来改变其中的值
524 | val p1 = Person("Peter", 15)
525 | val p2 = p2.copy(age = 16)
526 | ```
527 |
528 |
529 | ### apply 方法
530 |
531 | 避免在类里定义 apply 方法。这些方法往往会使代码的可读性变差,尤其是对于不熟悉 Scala 的人。它也难以被 IDE(或 grep)所跟踪。在最坏的情况下,它还可能影响代码的正确性,正如你在[括号](#parentheses)一节中看到的。
532 |
533 | 然而,将 apply 方法作为工厂方法定义在伴生对象中是可以接受的。在这种情况下,apply 方法应该返回其伴生类的类型。
534 |
535 | ```scala
536 | object TreeNode {
537 | // 下面这种定义是 OK 的
538 | def apply(name: String): TreeNode = ...
539 |
540 | // 不要像下面那样定义,因为它没有返回其伴生类的类型:TreeNode
541 | def apply(name: String): String = ...
542 | }
543 | ```
544 |
545 |
546 | ### override 修饰符
547 |
548 | 无论是覆盖具体的方法还是实现抽象的方法,始终都为方法加上 override 修饰符。实现抽象方法时,不加 override 修饰符,Scala 编译器也不会报错。即便如此,我们也应该始终把 override 修饰符加上,以此显式地表示覆盖行为。以此避免由于方法签名不同(而你也难以发现)而导致没有覆盖到本应覆盖的方法。
549 |
550 | ```scala
551 | trait Parent {
552 | def hello(data: Map[String, String]): Unit = {
553 | print(data)
554 | }
555 | }
556 |
557 | class Child extends Parent {
558 | import scala.collection.Map
559 |
560 | // 下面的方法没有覆盖 Parent.hello,
561 | // 因为两个 Map 的类型是不同的。
562 | // 如果我们加上 override 修饰符,编译器就会帮你找出问题并报错。
563 | def hello(data: Map[String, String]): Unit = {
564 | print("This is supposed to override the parent method, but it is actually not!")
565 | }
566 | }
567 | ```
568 |
569 |
570 |
571 | ### 解构绑定
572 |
573 | 解构绑定(有时也叫元组提取)是一种在一个表达式中为两个变量赋值的便捷方式。
574 |
575 | ```scala
576 | val (a, b) = (1, 2)
577 | ```
578 |
579 | 然而,请不要在构造函数中使用它们,尤其是当 `a` 和 `b` 需要被标记为 `transient` 的时候。Scala 编译器会产生一个额外的 Tuple2 字段,而它并不是暂态的(transient)。
580 |
581 | ```scala
582 | class MyClass {
583 | // 以下代码无法 work,因为编译器会产生一个非暂态的 Tuple2 指向 a 和 b
584 | @transient private val (a, b) = someFuncThatReturnsTuple2()
585 | }
586 | ```
587 |
588 |
589 | ### 按名称传参
590 |
591 | __避免使用按名传参__. 显式地使用 `() => T` 。
592 |
593 | 背景:Scala 允许按名称来定义方法参数,例如:以下例子是可以成功执行的:
594 |
595 | ```scala
596 | def print(value: => Int): Unit = {
597 | println(value)
598 | println(value + 1)
599 | }
600 |
601 | var a = 0
602 | def inc(): Int = {
603 | a += 1
604 | a
605 | }
606 |
607 | print(inc())
608 | ```
609 |
610 | 在上面的代码中,`inc()` 以闭包的形式传递给 `print` 函数,并且在 `print` 函数中被执行了两次,而不是以数值 `1` 传入。按名传参的一个主要问题是在方法调用处,我们无法区分是按名传参还是按值传参。因此无法确切地知道这个表达式是否会被执行(更糟糕的是它可能会被执行多次)。对于带有副作用的表达式来说,这一点是非常危险的。
611 |
612 |
613 | ### 多参数列表
614 |
615 | __避免使用多参数列表__。它们使运算符重载变得复杂,并且会使不熟悉 Scala 的程序员感到困惑。例如:
616 |
617 | ```scala
618 | // Avoid this!
619 | case class Person(name: String, age: Int)(secret: String)
620 | ```
621 |
622 | 一个值得注意的例外是,当在定义底层库时,可以使用第二个参数列表来存放隐式(implicit)参数。尽管如此,[我们应该避免使用 implicits](#implicits)!
623 |
624 |
625 | ### 符号方法(运算符重载)
626 |
627 | __不要使用符号作为方法名__,除非你是在定义算术运算的方法(如:`+`, `-`, `*`, `/`),否则在任何其它情况下,都不要使用。符号化的方法名让人难以理解方法的意图是什么,来看下面两个例子:
628 |
629 | ```scala
630 | // 符号化的方法名难以理解
631 | channel ! msg
632 | stream1 >>= stream2
633 |
634 | // 下面的方法意图则不言而喻
635 | channel.send(msg)
636 | stream1.join(stream2)
637 | ```
638 |
639 |
640 | ### 类型推导
641 |
642 | Scala 的类型推导,尤其是左侧类型推导以及闭包推导,可以使代码变得更加简洁。尽管如此,也有一些情况我们是需要显式地声明类型的:
643 |
644 | - __公有方法应该显式地声明类型__,编译器推导出来的类型往往会使你大吃一惊。
645 | - __隐式方法应该显式地声明类型__,否则在增量编译时,它会使 Scala 编译器崩溃。
646 | - __如果变量或闭包的类型并非显而易见,请显式声明类型__。一个不错的判断准则是,如果评审代码的人无法在 3 秒内确定相应实体的类型,那么你就应该显式地声明类型。
647 |
648 |
649 | ### Return 语句
650 |
651 | __闭包中避免使用 return__。`return` 会被编译器转成 ``scala.runtime.NonLocalReturnControl`` 异常的 ``try/catch`` 语句,这可能会导致意外行为。请看下面的例子:
652 |
653 | ```scala
654 | def receive(rpc: WebSocketRPC): Option[Response] = {
655 | tableFut.onComplete { table =>
656 | if (table.isFailure) {
657 | return None // Do not do that!
658 | } else { ... }
659 | }
660 | }
661 | ```
662 |
663 | `.onComplete` 方法接收一个匿名闭包并把它传递到一个不同的线程中。这个闭包最终会抛出一个 `NonLocalReturnControl` 异常,并在 __一个不同的线程中__被捕获,而这里执行的方法却没有任何影响。
664 |
665 | 然而,也有少数情况我们是推荐使用 `return` 的。
666 |
667 | - 使用 `return` 来简化控制流,避免增加一级缩进。
668 |
669 | ```scala
670 | def doSomething(obj: Any): Any = {
671 | if (obj eq null) {
672 | return null
673 | }
674 | // do something ...
675 | }
676 | ```
677 |
678 | - 使用 `return` 来提前终止循环,这样就不用额外构造状态标志。
679 |
680 | ```scala
681 | while (true) {
682 | if (cond) {
683 | return
684 | }
685 | }
686 | ```
687 |
688 | ### 递归及尾递归
689 |
690 | __避免使用递归__,除非问题可以非常自然地用递归来描述(比如,图和树的遍历)。
691 |
692 | 对于那些你意欲使之成为尾递归的方法,请加上 `@tailrec` 注解以确保编译器去检查它是否真的是尾递归(你会非常惊讶地看到,由于使用了闭包和函数变换,许多看似尾递归的代码事实并非尾递归)。
693 |
694 | 大多数的代码使用简单的循环和状态机会更容易推理,使用尾递归反而可能会使它更加繁琐且难以理解。例如,下面的例子中,命令式的代码比尾递归版本的代码要更加易读:
695 |
696 | ```scala
697 | // Tail recursive version.
698 | def max(data: Array[Int]): Int = {
699 | @tailrec
700 | def max0(data: Array[Int], pos: Int, max: Int): Int = {
701 | if (pos == data.length) {
702 | max
703 | } else {
704 | max0(data, pos + 1, if (data(pos) > max) data(pos) else max)
705 | }
706 | }
707 | max0(data, 0, Int.MinValue)
708 | }
709 |
710 | // Explicit loop version
711 | def max(data: Array[Int]): Int = {
712 | var max = Int.MinValue
713 | for (v <- data) {
714 | if (v > max) {
715 | max = v
716 | }
717 | }
718 | max
719 | }
720 | ```
721 |
722 |
723 | ### Implicits
724 |
725 | __避免使用 implicit__,除非:
726 |
727 | - 你在构建领域特定的语言(DSL)
728 | - 你在隐式类型参数中使用它(如:`ClassTag`,`TypeTag`)
729 | - 你在你自己的类中使用它(意指不要污染外部空间),以此减少类型转换的冗余度(如:Scala 闭包到 Java 闭包的转换)。
730 |
731 | 当使用 implicit 时,我们应该确保另一个工程师可以直接理解使用语义,而无需去阅读隐式定义本身。Implicit 有着非常复杂的解析规则,这会使代码变得极其难以理解。Twitter 的 Effective Scala 指南中写道:「如果你发现你在使用 implicit,始终停下来问一下你自己,是否可以在不使用 implicit 的条件下达到相同的效果」。
732 |
733 | 如果你必需使用它们(比如:丰富 DSL),那么不要重载隐式方法,即确保每个隐式方法有着不同的名字,这样使用者就可以选择性地导入它们。
734 |
735 | ```scala
736 | // 别这么做,这样使用者无法选择性地只导入其中一个方法。
737 | object ImplicitHolder {
738 | def toRdd(seq: Seq[Int]): RDD[Int] = ...
739 | def toRdd(seq: Seq[Long]): RDD[Long] = ...
740 | }
741 |
742 | // 应该将它们定义为不同的名字:
743 | object ImplicitHolder {
744 | def intSeqToRdd(seq: Seq[Int]): RDD[Int] = ...
745 | def longSeqToRdd(seq: Seq[Long]): RDD[Long] = ...
746 | }
747 | ```
748 |
749 |
750 | ## 异常处理 (Try 还是 try)
751 |
752 | - 不要捕获 Throwable 或 Exception 类型的异常。请使用 `scala.util.control.NonFatal`:
753 |
754 | ```scala
755 | try {
756 | ...
757 | } catch {
758 | case NonFatal(e) =>
759 | // 异常处理;注意 NonFatal 无法匹配 InterruptedException 类型的异常
760 | case e: InterruptedException =>
761 | // 处理 InterruptedException
762 | }
763 | ```
764 | 这能保证我们不会去捕获 `NonLocalReturnControl` 异常(正如在[Return 语句](#return)中所解释的)。
765 |
766 | - 不要在 API 中使用 `Try`,即,不要在任何方法中返回 Try。对于异常执行,请显式地抛出异常,并使用 Java 风格的 try/catch 做异常处理。
767 |
768 | 背景资料:Scala 提供了单子(monadic)错误处理(通过 `Try`,`Success` 和 `Failure`),这样便于做链式处理。然而,根据我们的经验,发现使用它通常会带来更多的嵌套层级,使得代码难以阅读。此外,对于预期错误还是异常,在语义上常常是不明晰的。因此,我们不鼓励使用 `Try` 来做错误处理,尤其是以下情况:
769 |
770 | 一个人为的例子:
771 |
772 | ```scala
773 | class UserService {
774 | /** Look up a user's profile in the user database. */
775 | def get(userId: Int): Try[User]
776 | }
777 | ```
778 | 以下的写法会更好:
779 |
780 | ```scala
781 | class UserService {
782 | /**
783 | * Look up a user's profile in the user database.
784 | * @return None if the user is not found.
785 | * @throws DatabaseConnectionException when we have trouble connecting to the database/
786 | */
787 | @throws(DatabaseConnectionException)
788 | def get(userId: Int): Option[User]
789 | }
790 | ```
791 | 第二种写法非常明显地能让调用者知道需要处理哪些错误情况。
792 |
793 |
794 | ### Options
795 |
796 | - 如果一个值可能为空,那么请使用 `Option`。相对于 `null`,`Option` 显式地表明了一个 API 的返回值可能为空。
797 | - 构造 `Option` 值时,请使用 `Option` 而非 `Some`,以防那个值为 `null`。
798 |
799 | ```scala
800 | def myMethod1(input: String): Option[String] = Option(transform(input))
801 |
802 | // This is not as robust because transform can return null, and then
803 | // myMethod2 will return Some(null).
804 | def myMethod2(input: String): Option[String] = Some(transform(input))
805 | ```
806 | - 不要使用 None 来表示异常,有异常时请显式抛出。
807 | - 不要在一个 `Option` 值上直接调用 `get` 方法,除非你百分百确定那个 `Option` 值不是 `None`。
808 |
809 |
810 | ### 单子链接
811 |
812 | 单子链接是 Scala 的一个强大特性。Scala 中几乎一切都是单子(如:集合,Option,Future,Try 等),对它们的操作可以链接在一起。这是一个非常强大的概念,但你应该谨慎使用,尤其是:
813 |
814 | - 避免链接(或嵌套)超过 3 个操作。
815 | - 如果需要花超过 5 秒钟来理解其中的逻辑,那么你应该尽量去想想有没什么办法在不使用单子链接的条件下来达到相同的效果。一般来说,你需要注意的是:不要滥用 `flatMap` 和 `fold`。
816 | - 链接应该在 flatMap 之后断开(因为类型发生了变化)。
817 |
818 | 通过给中间结果显式地赋予一个变量名,将链接断开变成一种更加过程化的风格,能让单子链接更加易于理解。来看下面的例子:
819 |
820 | ```scala
821 | class Person(val data: Map[String, String])
822 | val database = Map[String, Person]
823 | // Sometimes the client can store "null" value in the store "address"
824 |
825 | // A monadic chaining approach
826 | def getAddress(name: String): Option[String] = {
827 | database.get(name).flatMap { elem =>
828 | elem.data.get("address")
829 | .flatMap(Option.apply) // handle null value
830 | }
831 | }
832 |
833 | // 尽管代码会长一些,但以下方法可读性更高
834 | def getAddress(name: String): Option[String] = {
835 | if (!database.contains(name)) {
836 | return None
837 | }
838 |
839 | database(name).data.get("address") match {
840 | case Some(null) => None // handle null value
841 | case Some(addr) => Option(addr)
842 | case None => None
843 | }
844 | }
845 |
846 | ```
847 |
848 |
849 | ## 并发
850 |
851 | ### Scala concurrent.Map
852 |
853 | __优先考虑使用 `java.util.concurrent.ConcurrentHashMap` 而非 `scala.collection.concurrent.Map`__。尤其是 `scala.collection.concurrent.Map` 中的 `getOrElseUpdate` 方法要慎用,它并非原子操作(这个问题在 Scala 2.11.16 中 fix 了:[SI-7943](https://issues.scala-lang.org/browse/SI-7943))。由于我们做的所有项目都需要在 Scala 2.10 和 Scala 2.11 上使用,因此要避免使用 `scala.collection.concurrent.Map`。
854 |
855 |
856 | ### 显式同步 vs 并发集合
857 |
858 | 有 3 种推荐的方法来安全地并发访问共享状态。__不要混用它们__,因为这会使程序变得难以推理,并且可能导致死锁。
859 |
860 | - `java.util.concurrent.ConcurrentHashMap`:当所有的状态都存储在一个 map 中,并且有高程度的竞争时使用。
861 |
862 | ```scala
863 | private[this] val map = new java.util.concurrent.ConcurrentHashMap[String, String]
864 | ```
865 |
866 | - `java.util.Collections.synchronizedMap`:使用情景:当所有状态都存储在一个 map 中,并且预期不存在竞争情况,但你仍想确保代码在并发下是安全的。如果没有竞争出现,JVM 的 JIT 编译器能够通过偏置锁(biased locking)移除同步开销。
867 |
868 | ```scala
869 | private[this] val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])
870 | ```
871 |
872 | - 通过同步所有临界区进行显式同步,可用于监视多个变量。与 2 相似,JVM 的 JIT 编译器能够通过偏置锁(biased locking)移除同步开销。
873 |
874 | ```scala
875 | class Manager {
876 | private[this] var count = 0
877 | private[this] val map = new java.util.HashMap[String, String]
878 | def update(key: String, value: String): Unit = synchronized {
879 | map.put(key, value)
880 | count += 1
881 | }
882 | def getCount: Int = synchronized { count }
883 | }
884 | ```
885 |
886 | 注意,对于 case 1 和 case 2,不要让集合的视图或迭代器从保护区域逃逸。这可能会以一种不明显的方式发生,比如:返回了 `Map.keySet` 或 `Map.values`。如果需要传递集合的视图或值,生成一份数据拷贝再传递。
887 |
888 | ```scala
889 | val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])
890 |
891 | // This is broken!
892 | def values: Iterable[String] = map.values
893 |
894 | // Instead, copy the elements
895 | def values: Iterable[String] = map.synchronized { Seq(map.values: _*) }
896 | ```
897 |
898 | ### 显式同步 vs 原子变量 vs @volatile
899 |
900 | `java.util.concurrent.atomic` 包提供了对基本类型的无锁访问,比如:`AtomicBoolean`, `AtomicInteger` 和 `AtomicReference`。
901 |
902 | 始终优先考虑使用原子变量而非 `@volatile`,它们是相关功能的严格超集并且从代码上看更加明显。原子变量的底层实现使用了 `@volatile`。
903 |
904 | 优先考虑使用原子变量而非显式同步的情况:(1)一个对象的所有临界区更新都被限制在单个变量里并且预期会有竞争情况出现。原子变量是无锁的并且允许更为有效的竞争。(2)同步被明确地表示为 `getAndSet` 操作。例如:
905 |
906 | ```scala
907 | // good: 明确又有效地表达了下面的并发代码只执行一次
908 | val initialized = new AtomicBoolean(false)
909 | ...
910 | if (!initialized.getAndSet(true)) {
911 | ...
912 | }
913 |
914 | // poor: 下面的同步就没那么明晰,而且会出现不必要的同步
915 | val initialized = false
916 | ...
917 | var wasInitialized = false
918 | synchronized {
919 | wasInitialized = initialized
920 | initialized = true
921 | }
922 | if (!wasInitialized) {
923 | ...
924 | }
925 | ```
926 |
927 | ### 私有字段
928 |
929 | 注意,`private` 字段仍然可以被相同类的其它实例所访问,所以仅仅通过 `this.synchronized`(或 `synchronized`)来保护它从技术上来说是不够的,不过你可以通过 `private[this]` 修饰私有字段来达到目的。
930 |
931 | ```scala
932 | // 以下代码仍然是不安全的。
933 | class Foo {
934 | private var count: Int = 0
935 | def inc(): Unit = synchronized { count += 1 }
936 | }
937 |
938 | // 以下代码是安全的。
939 | class Foo {
940 | private[this] var count: Int = 0
941 | def inc(): Unit = synchronized { count += 1 }
942 | }
943 | ```
944 |
945 |
946 | ### 隔离
947 |
948 | 一般来说,并发和同步逻辑应该尽可能地被隔离和包含起来。这实际上意味着:
949 |
950 | - 避免在 API 层面、面向用户的方法以及回调中暴露同步原语。
951 | - 对于复杂模块,创建一个小的内部模块来包含并发原语。
952 |
953 |
954 | ## 性能
955 |
956 | 对于你写的绝大多数代码,性能都不应该成为一个问题。然而,对于一些性能敏感的代码,以下有一些小建议:
957 |
958 | ### Microbenchmarks
959 |
960 | 由于 Scala 编译器和 JVM JIT 编译器会对你的代码做许多神奇的事情,因此要写出一个好的微基准程序(microbenchmark)是极其困难的。更多的情况往往是你的微基准程序并没有测量你想要测量的东西。
961 |
962 | 如果你要写一个微基准程序,请使用 [jmh](http://openjdk.java.net/projects/code-tools/jmh/)。请确保你阅读了[所有的样例](http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/),这样你才理解微基准程序中「死代码」移除、常量折叠以及循环展开的效果。
963 |
964 |
965 | ### Traversal 与 zipWithIndex
966 |
967 | 使用 `while` 循环而非 `for` 循环或函数变换(如:`map`、`foreach`),for 循环和函数变换非常慢(由于虚函数调用和装箱的缘故)。
968 |
969 | ```scala
970 |
971 | val arr = // array of ints
972 | // 偶数位置的数置零
973 | val newArr = list.zipWithIndex.map { case (elem, i) =>
974 | if (i % 2 == 0) 0 else elem
975 | }
976 |
977 | // 这是上面代码的高性能版本
978 | val newArr = new Array[Int](arr.length)
979 | var i = 0
980 | val len = newArr.length
981 | while (i < len) {
982 | newArr(i) = if (i % 2 == 0) 0 else arr(i)
983 | i += 1
984 | }
985 | ```
986 |
987 | ### Option 与 null
988 |
989 | 对于性能有要求的代码,优先考虑使用 `null` 而不是 `Option`,以此避免虚函数调用以及装箱操作。用 Nullable 注解明确标示出可能为 `null` 的值。
990 |
991 | ```scala
992 | class Foo {
993 | @javax.annotation.Nullable
994 | private[this] var nullableField: Bar = _
995 | }
996 | ```
997 |
998 | ### Scala 集合库
999 |
1000 | 对于性能有要求的代码,优先考虑使用 Java 集合库而非 Scala 集合库,因为一般来说,Scala 集合库要比 Java 的集合库慢。
1001 |
1002 | ### private[this]
1003 |
1004 | 对于性能有要求的代码,优先考虑使用 `private[this]` 而非 `private`。`private[this]` 生成一个字段而非生成一个访问方法。根据我们的经验,JVM JIT 编译器并不总是会内联 `private` 字段的访问方法,因此通过使用
1005 | `private[this]` 来确保没有虚函数调用会更保险。
1006 |
1007 | ```scala
1008 | class MyClass {
1009 | private val field1 = ...
1010 | private[this] val field2 = ...
1011 |
1012 | def perfSensitiveMethod(): Unit = {
1013 | var i = 0
1014 | while (i < 1000000) {
1015 | field1 // This might invoke a virtual method call
1016 | field2 // This is just a field access
1017 | i += 1
1018 | }
1019 | }
1020 | }
1021 | ```
1022 |
1023 |
1024 | ## 与 Java 的互操作性
1025 |
1026 | 本节内容介绍的是构建 Java 兼容 API 的准则。如果你构建的组件并不需要与 Java 有交互,那么请无视这一节。这一节的内容主要是从我们开发 Spark 的 Java API 的经历中得出的。
1027 |
1028 |
1029 | ### Scala 中缺失的 Java 特性
1030 |
1031 | 以下的 Java 特性在 Scala 中是没有的,如果你需要使用以下特性,请在 Java 中定义它们。然而,需要提醒一点的是,你无法为 Java 源文件生成 ScalaDoc。
1032 |
1033 | - 静态字段
1034 | - 静态内部类
1035 | - Java 枚举
1036 | - 注解
1037 |
1038 |
1039 | ### Traits 与抽象类
1040 |
1041 | 对于允许从外部实现的接口,请记住以下几点:
1042 |
1043 | - 包含了默认方法实现的 trait 是无法在 Java 中使用的,请使用抽象类来代替。
1044 | - 一般情况下,请避免使用 trait,除非你百分百确定这个接口即使在未来也不会有默认的方法实现。
1045 |
1046 | ```scala
1047 | // 以下默认实现无法在 Java 中使用
1048 | trait Listener {
1049 | def onTermination(): Unit = { ... }
1050 | }
1051 |
1052 | // 可以在 Java 中使用
1053 | abstract class Listener {
1054 | def onTermination(): Unit = { ... }
1055 | }
1056 | ```
1057 |
1058 |
1059 | ### 类型别名
1060 |
1061 | 不要使用类型别名,它们在字节码和 Java 中是不可见的。
1062 |
1063 |
1064 | ### 默认参数值
1065 |
1066 | 不要使用默认参数值,通过重载方法来代替。
1067 |
1068 | ```scala
1069 | // 打破了与 Java 的互操作性
1070 | def sample(ratio: Double, withReplacement: Boolean = false): RDD[T] = { ... }
1071 |
1072 | // 以下方法是 work 的
1073 | def sample(ratio: Double, withReplacement: Boolean): RDD[T] = { ... }
1074 | def sample(ratio: Double): RDD[T] = sample(ratio, withReplacement = false)
1075 | ```
1076 |
1077 | ### 多参数列表
1078 |
1079 | 不要使用多参数列表。
1080 |
1081 | ### 可变参数
1082 |
1083 | - 为可变参数方法添加 `@scala.annotation.varargs` 注解,以确保它能在 Java 中使用。Scala 编译器会生成两个方法,一个给 Scala 使用(字节码参数是一个 Seq),另一个给 Java 使用(字节码参数是一个数组)。
1084 |
1085 | ```scala
1086 | @scala.annotation.varargs
1087 | def select(exprs: Expression*): DataFrame = { ... }
1088 | ```
1089 |
1090 | - 需要注意的一点是,由于 Scala 编译器的一个 bug([SI-1459](https://issues.scala-lang.org/browse/SI-1459),[SI-9013](https://issues.scala-lang.org/browse/SI-9013)),抽象的变参方法是无法在 Java 中使用的。
1091 |
1092 | - 重载变参方法时要小心,用另一个类型去重载变参方法会破坏源码的兼容性。
1093 |
1094 | ```scala
1095 | class Database {
1096 | @scala.annotation.varargs
1097 | def remove(elems: String*): Unit = ...
1098 |
1099 | // 当调用无参的 remove 方法时会出问题。
1100 | @scala.annotation.varargs
1101 | def remove(elems: People*): Unit = ...
1102 | }
1103 |
1104 | // remove 方法有歧义,因此编译不过。
1105 | new Database().remove()
1106 | ```
1107 | 一种解决方法是,在可变参数前显式地定义第一个参数:
1108 |
1109 | ```scala
1110 | class Database {
1111 | @scala.annotation.varargs
1112 | def remove(elems: String*): Unit = ...
1113 |
1114 | // 以下重载是 OK 的。
1115 | @scala.annotation.varargs
1116 | def remove(elem: People, elems: People*): Unit = ...
1117 | }
1118 | ```
1119 |
1120 |
1121 | ### Implicits
1122 |
1123 | 不要为类或方法使用 implicit,包括了不要使用 `ClassTag` 和 `TypeTag`。
1124 |
1125 | ```scala
1126 | class JavaFriendlyAPI {
1127 | // 以下定义对 Java 是不友好的,因为方法中包含了一个隐式参数(ClassTag)。
1128 | def convertTo[T: ClassTag](): T
1129 | }
1130 | ```
1131 |
1132 | ### 伴生对象,静态方法与字段
1133 |
1134 | 当涉及到伴生对象和静态方法/字段时,有几件事情是需要注意的:
1135 |
1136 | - 伴生对象在 Java 中的使用是非常别扭的(伴生对象 `Foo` 会被定义为 `Foo$` 类内的一个类型为 `Foo$` 的静态字段 `MODULE$`)。
1137 |
1138 | ```scala
1139 | object Foo
1140 |
1141 | // 等价于以下的 Java 代码
1142 | public class Foo$ {
1143 | Foo$ MODULE$ = // 对象的实例化
1144 | }
1145 | ```
1146 | 如果非要使用伴生对象,可以在一个单独的类中创建一个 Java 静态字段。
1147 |
1148 | - 不幸的是,没有办法在 Scala 中定义一个 JVM 静态字段。请创建一个 Java 文件来定义它。
1149 | - 伴生对象里的方法会被自动转成伴生类里的静态方法,除非方法名有冲突。确保静态方法正确生成的最好方式是用 Java 写一个测试文件,然后调用生成的静态方法。
1150 |
1151 | ```scala
1152 | class Foo {
1153 | def method2(): Unit = { ... }
1154 | }
1155 |
1156 | object Foo {
1157 | def method1(): Unit = { ... } // 静态方法 Foo.method1 会被创建(字节码)
1158 | def method2(): Unit = { ... } // 静态方法 Foo.method2 不会被创建
1159 | }
1160 |
1161 | // FooJavaTest.java (in test/scala/com/databricks/...)
1162 | public class FooJavaTest {
1163 | public static void compileTest() {
1164 | Foo.method1(); // 正常编译
1165 | Foo.method2(); // 编译失败,因为 method2 并没有生成
1166 | }
1167 | }
1168 | ```
1169 |
1170 | - 样例对象(case object) MyClass 的类型并不是 MyClass。
1171 |
1172 | ```scala
1173 | case object MyClass
1174 |
1175 | // Test.java
1176 | if (MyClass$.MODULE instanceof MyClass) {
1177 | // 上述条件始终为 false
1178 | }
1179 | ```
1180 | 要实现正确的类型层级结构,请定义一个伴生类,然后用一个样例对象去继承它:
1181 |
1182 | ```scala
1183 | class MyClass
1184 | case object MyClass extends MyClass
1185 | ```
1186 |
1187 | ## 测试
1188 |
1189 | ### 异常拦截
1190 |
1191 | 当测试某个操作(比如用无效的参数调用一个函数)是否会抛出异常时,对于抛出的异常类型指定得越具体越好。你不应该简单地使用 `intercept[Exception]` 或 `intercept[Throwable]`(ScalaTest 语法),这能拦截任意异常,只能断言有异常抛出,而不能确定是什么异常。这样做在测试中能捕获到代码中的异常并且通过测试,然而却没真正检验你想验证的行为。
1192 |
1193 |
1194 | ```scala
1195 | // 不要使用下面这种方式
1196 | intercept[Exception] {
1197 | thingThatThrowsException()
1198 | }
1199 |
1200 | // 这才是推荐的做法
1201 | intercept[MySpecificTypeOfException] {
1202 | thingThatThrowsException()
1203 | }
1204 | ```
1205 |
1206 | 如果你无法指定代码会抛出的异常的具体类型,说明你这段代码可能写得不好,需要重构。这种情况下,你要么测试更底层的代码,要么改写代码令其抛出类型更加具体的异常。
1207 |
1208 |
1209 | ## 其它
1210 |
1211 | ### 优先使用 nanoTime 而非 currentTimeMillis
1212 |
1213 | 当要计算*持续时间*或者检查*超时*的时候,避免使用 `System.currentTimeMillis()`。请使用 `System.nanoTime()`,即使你对亚毫秒级的精度并不感兴趣。
1214 |
1215 | `System.currentTimeMillis()` 返回的是当前的时钟时间,并且会跟进系统时钟的改变。因此,负的时钟调整可能会导致超时而挂起很长一段时间(直到时钟时间赶上先前的值)。这种情况可能发生在网络已经中断一段时间,ntpd 走过了一步之后。最典型的例子是,在系统启动的过程中,DHCP 花费的时间要比平常的长。这可能会导致非常难以理解且难以重现的问题。而 `System.nanoTime()` 则可以保证是单调递增的,与时钟变化无关。
1216 |
1217 | 注意事项:
1218 |
1219 | - 永远不要序列化一个绝对的 `nanoTime()` 值或是把它传递给另一个系统。绝对的 `nanoTime()` 值是无意义的、与系统相关的,并且在系统重启时会重置。
1220 | - 绝对的 `nanoTime()` 值并不保证总是正数(但 `t2 - t1` 能确保总是产生正确的值)。
1221 | - `nanoTime()` 每 292 年就会重新计算起。所以,如果你的 Spark 任务需要花非常非常非常长的时间,你可能需要别的东西来处理了:)
1222 |
1223 |
1224 | ### 优先使用 URI 而非 URL
1225 |
1226 | 当存储服务的 URL 时,你应当使用 `URI` 来表示。
1227 |
1228 | `URL` 的[相等性检查](http://docs.oracle.com/javase/7/docs/api/java/net/URL.html#equals(java.lang.Object))实际上执行了一次网络调用(这是阻塞的)来解析 IP 地址。`URI` 类在表示能力上是 `URL` 的超集,并且它执行的是字段的相等性检查。
1229 |
1230 | ### 优先使用现存的经过良好测试的方法而非重新发明轮子
1231 |
1232 | 当存在一个已经经过良好测试的方法,并且不会存在性能问题,那么优先使用这个方法。重新实现它可能会引入Bug,同时也需要花费时间来进行测试(也可能我们甚至忘记去测试这个方法!)。
1233 |
1234 | ```scala
1235 | val beginNs = System.nanoTime()
1236 | // Do something
1237 | Thread.sleep(1000)
1238 | val elapsedNs = System.nanoTime() - beginNs
1239 |
1240 | // 不要使用下面这种方式。这种方法容易出错
1241 | val elapsedMs = elapsedNs / 1000 / 1000
1242 |
1243 | // 推荐方法:使用Java TimeUnit API
1244 | import java.util.concurrent.TimeUnit
1245 | val elapsedMs2 = TimeUnit.NANOSECONDS.toMillis(elapsedNs)
1246 |
1247 | // 推荐方法:使用Scala Duration API
1248 | import scala.concurrent.duration._
1249 | val elapsedMs3 = elapsedNs.nanos.toMillis
1250 | ```
1251 |
1252 | 例外:
1253 | - 使用现存的方法需要引入新的依赖。如果一个方法特别简单,比起引入一个新依赖,重新实现它通常更好。但是记得进行测试。
1254 | - 现存的方法没有针对我们的用法进行优化,性能达不到要求。但是首先做一下benchmark, 避免过早优化。
1255 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Databricks Scala Guide
2 |
3 | With over 1000 contributors, Apache Spark is to the best of our knowledge the largest open-source project in Big Data and the most active project written in Scala. This guide draws from our experience coaching and working with engineers contributing to Spark as well as our [Databricks](http://databricks.com/) engineering team.
4 |
5 | Code is __written once__ by its author, but __read and modified multiple times__ by lots of other engineers. As most bugs actually come from future modification of the code, we need to optimize our codebase for long-term, global readability and maintainability. The best way to achieve this is to write simple code.
6 |
7 | Scala is an incredibly powerful language that is capable of many paradigms. We have found that the following guidelines work well for us on projects with high velocity. Depending on the needs of your team, your mileage might vary.
8 |
9 | 
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
10 |
11 |
12 | ## Table of Contents
13 |
14 | 1. [Document History](#history)
15 |
16 | 2. [Syntactic Style](#syntactic)
17 | * [Naming Convention](#naming)
18 | * [Variable Naming Convention](#variable-naming)
19 | * [Line Length](#linelength)
20 | * [Rule of 30](#rule_of_30)
21 | * [Spacing and Indentation](#indent)
22 | * [Blank Lines (Vertical Whitespace)](#blanklines)
23 | * [Parentheses](#parentheses)
24 | * [Curly Braces](#curly)
25 | * [Long Literals](#long_literal)
26 | * [Documentation Style](#doc)
27 | * [Ordering within a Class](#ordering_class)
28 | * [Imports](#imports)
29 | * [Pattern Matching](#pattern-matching)
30 | * [Infix Methods](#infix)
31 | * [Anonymous Methods](#anonymous)
32 |
33 | 1. [Scala Language Features](#lang)
34 | * [Case Classes and Immutability](#case_class_immutability)
35 | * [apply Method](#apply_method)
36 | * [override Modifier](#override_modifier)
37 | * [Destructuring Binds](#destruct_bind)
38 | * [Call by Name](#call_by_name)
39 | * [Multiple Parameter Lists](#multi-param-list)
40 | * [Symbolic Methods (Operator Overloading)](#symbolic_methods)
41 | * [Type Inference](#type_inference)
42 | * [Return Statements](#return)
43 | * [Recursion and Tail Recursion](#recursion)
44 | * [Implicits](#implicits)
45 | * [Exception Handling (Try vs try)](#exception)
46 | * [Options](#option)
47 | * [Monadic Chaining](#chaining)
48 |
49 | 1. [Concurrency](#concurrency)
50 | * [Scala concurrent.Map](#concurrency-scala-collection)
51 | * [Explicit Synchronization vs Concurrent Collections](#concurrency-sync-vs-map)
52 | * [Explicit Synchronization vs Atomic Variables vs @volatile](#concurrency-sync-vs-atomic)
53 | * [Private Fields](#concurrency-private-this)
54 | * [Isolation](#concurrency-isolation)
55 |
56 | 1. [Performance](#perf)
57 | * [Microbenchmarks](#perf-microbenchmarks)
58 | * [Traversal and zipWithIndex](#perf-whileloops)
59 | * [Option and null](#perf-option)
60 | * [Scala Collection Library](#perf-collection)
61 | * [private[this]](#perf-private)
62 |
63 | 1. [Java Interoperability](#java)
64 | * [Java Features Missing from Scala](#java-missing-features)
65 | * [Traits and Abstract Classes](#java-traits)
66 | * [Type Aliases](#java-type-alias)
67 | * [Default Parameter Values](#java-default-param-values)
68 | * [Multiple Parameter Lists](#java-multi-param-list)
69 | * [Varargs](#java-varargs)
70 | * [Implicits](#java-implicits)
71 | * [Companion Objects, Static Methods and Fields](#java-companion-object)
72 |
73 | 1. [Testing](#testing)
74 | * [Intercepting Exceptions](#testing-intercepting)
75 |
76 | 1. [Miscellaneous](#misc)
77 | * [Prefer nanoTime over currentTimeMillis](#misc_currentTimeMillis_vs_nanoTime)
78 | * [Prefer URI over URL](#misc_uri_url)
79 | * [Prefer existing well-tested methods over reinventing the wheel](#misc_well_tested_method)
80 |
81 |
82 |
83 | ## Document History
84 | - 2015-03-16: Initial version.
85 | - 2015-05-25: Added [override Modifier](#override_modifier) section.
86 | - 2015-08-23: Downgraded the severity of some rules from "do NOT" to "avoid".
87 | - 2015-11-17: Updated [apply Method](#apply_method) section: apply method in companion object should return the companion class.
88 | - 2015-11-17: This guide has been [translated into Chinese](README-ZH.md). The Chinese translation is contributed by community member [Hawstein](https://github.com/Hawstein). We do not guarantee that it will always be kept up-to-date.
89 | - 2015-12-14: This guide has been [translated into Korean](README-KO.md). The Korean translation is contributed by [Hyukjin Kwon](https://github.com/HyukjinKwon) and reviewed by [Yun Park](https://github.com/yunpark93), [Kevin (Sangwoo) Kim](https://github.com/swkimme), [Hyunje Jo](https://github.com/RetrieverJo) and [Woochel Choi](https://github.com/socialpercon). We do not guarantee that it will always be kept up-to-date.
90 | - 2016-06-15: Added [Anonymous Methods](#anonymous) section.
91 | - 2016-06-21: Added [Variable Naming Convention](#variable-naming) section.
92 | - 2016-12-24: Added [Case Classes and Immutability](#case_class_immutability) section.
93 | - 2017-02-23: Added [Testing](#testing) section.
94 | - 2017-04-18: Added [Prefer existing well-tested methods over reinventing the wheel](#misc_well_tested_method) section.
95 |
96 | ## Syntactic Style
97 |
98 | ### Naming Convention
99 |
100 | We mostly follow Java's and Scala's standard naming conventions.
101 |
102 | - Classes, traits, objects should follow Java class convention, i.e. PascalCase style.
103 | ```scala
104 | class ClusterManager
105 |
106 | trait Expression
107 | ```
108 |
109 | - Packages should follow Java package naming conventions, i.e. all-lowercase ASCII letters.
110 | ```scala
111 | package com.databricks.resourcemanager
112 | ```
113 |
114 | - Methods/functions should be named in camelCase style.
115 |
116 | - Constants should be all uppercase letters and be put in a companion object.
117 | ```scala
118 | object Configuration {
119 | val DEFAULT_PORT = 10000
120 | }
121 | ```
122 |
123 | - Enums should be PascalCase.
124 |
125 | - Annotations should also follow Java convention, i.e. PascalCase. Note that this differs from Scala's official guide.
126 | ```scala
127 | final class MyAnnotation extends StaticAnnotation
128 | ```
129 |
130 |
131 | ### Variable Naming Convention
132 |
133 | - Variables should be named in camelCase style, and should have self-evident names.
134 | ```scala
135 | val serverPort = 1000
136 | val clientPort = 2000
137 | ```
138 |
139 | - It is OK to use one-character variable names in small, localized scope. For example, "i" is commonly used as the loop index for a small loop body (e.g. 10 lines of code). However, do NOT use "l" (as in Larry) as the identifier, because it is difficult to differentiate "l" from "1", "|", and "I".
140 |
141 | ### Line Length
142 |
143 | - Limit lines to 100 characters.
144 | - The only exceptions are import statements and URLs (although even for those, try to keep them under 100 chars).
145 |
146 |
147 | ### Rule of 30
148 |
149 | "If an element consists of more than 30 subelements, it is highly probable that there is a serious problem" - [Refactoring in Large Software Projects](http://www.amazon.com/Refactoring-Large-Software-Projects-Restructurings/dp/0470858923).
150 |
151 | In general:
152 |
153 | - A method should contain less than 30 lines of code.
154 | - A class should contain less than 30 methods.
155 |
156 |
157 | ### Spacing and Indentation
158 |
159 | - Put one space before and after operators, including the assignment operator.
160 | ```scala
161 | def add(int1: Int, int2: Int): Int = int1 + int2
162 | ```
163 |
164 | - Put one space after commas.
165 | ```scala
166 | Seq("a", "b", "c") // do this
167 |
168 | Seq("a","b","c") // don't omit spaces after commas
169 | ```
170 |
171 | - Put one space after colons.
172 | ```scala
173 | // do this
174 | def getConf(key: String, defaultValue: String): String = {
175 | // some code
176 | }
177 |
178 | // don't put spaces before colons
179 | def calculateHeaderPortionInBytes(count: Int) : Int = {
180 | // some code
181 | }
182 |
183 | // don't omit spaces after colons
184 | def multiply(int1:Int, int2:Int): Int = int1 * int2
185 | ```
186 |
187 | - Use 2-space indentation in general.
188 | ```scala
189 | if (true) {
190 | println("Wow!")
191 | }
192 | ```
193 |
194 | - For method declarations, use 4 space indentation for their parameters and put each in each line when the parameters don't fit in two lines. Return types can be either on the same line as the last parameter, or start a new line with 2 space indent.
195 |
196 | ```scala
197 | def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
198 | path: String,
199 | fClass: Class[F],
200 | kClass: Class[K],
201 | vClass: Class[V],
202 | conf: Configuration = hadoopConfiguration): RDD[(K, V)] = {
203 | // method body
204 | }
205 |
206 | def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
207 | path: String,
208 | fClass: Class[F],
209 | kClass: Class[K],
210 | vClass: Class[V],
211 | conf: Configuration = hadoopConfiguration)
212 | : RDD[(K, V)] = {
213 | // method body
214 | }
215 | ```
216 |
217 | - For classes whose header doesn't fit in two lines, use 4 space indentation for its parameters, put each in each line, put the extends on the next line with 2 space indent, and add a blank line after class header.
218 |
219 | ```scala
220 | class Foo(
221 | val param1: String, // 4 space indent for parameters
222 | val param2: String,
223 | val param3: Array[Byte])
224 | extends FooInterface // 2 space indent here
225 | with Logging {
226 |
227 | def firstMethod(): Unit = { ... } // blank line above
228 | }
229 | ```
230 |
231 | - For method and class constructor invocations, use 2 space indentation for its parameters and put each in each line when the parameters don't fit in two lines.
232 |
233 | ```scala
234 | foo(
235 | someVeryLongFieldName, // 2 space indent here
236 | andAnotherVeryLongFieldName,
237 | "this is a string",
238 | 3.1415)
239 |
240 | new Bar(
241 | someVeryLongFieldName, // 2 space indent here
242 | andAnotherVeryLongFieldName,
243 | "this is a string",
244 | 3.1415)
245 | ```
246 |
247 | - Do NOT use vertical alignment. They draw attention to the wrong parts of the code and make the aligned code harder to change in the future.
248 | ```scala
249 | // Don't align vertically
250 | val plus = "+"
251 | val minus = "-"
252 | val multiply = "*"
253 |
254 | // Do the following
255 | val plus = "+"
256 | val minus = "-"
257 | val multiply = "*"
258 | ```
259 |
260 |
261 | ### Blank Lines (Vertical Whitespace)
262 |
263 | - A single blank line appears:
264 | - Between consecutive members (or initializers) of a class: fields, constructors, methods, nested classes, static initializers, instance initializers.
265 | - Exception: A blank line between two consecutive fields (having no other code between them) is optional. Such blank lines are used as needed to create logical groupings of fields.
266 | - Within method bodies, as needed to create logical groupings of statements.
267 | - Optionally before the first member or after the last member of the class (neither encouraged nor discouraged).
268 | - Use one or two blank line(s) to separate class definitions.
269 | - Excessive number of blank lines is discouraged.
270 |
271 |
272 | ### Parentheses
273 |
274 | - Methods should be declared with parentheses, unless they are accessors that have no side-effect (state mutation, I/O operations are considered side-effects).
275 | ```scala
276 | class Job {
277 | // Wrong: killJob changes state. Should have ().
278 | def killJob: Unit
279 |
280 | // Correct:
281 | def killJob(): Unit
282 | }
283 | ```
284 | - Callsite should follow method declaration, i.e. if a method is declared with parentheses, call with parentheses.
285 | Note that this is not just syntactic. It can affect correctness when `apply` is defined in the return object.
286 | ```scala
287 | class Foo {
288 | def apply(args: String*): Int
289 | }
290 |
291 | class Bar {
292 | def foo: Foo
293 | }
294 |
295 | new Bar().foo // This returns a Foo
296 | new Bar().foo() // This returns an Int!
297 | ```
298 |
299 |
300 | ### Curly Braces
301 |
302 | Put curly braces even around one-line conditional or loop statements. The only exception is if you are using if/else as an one-line ternary operator that is also side-effect free.
303 | ```scala
304 | // Correct:
305 | if (true) {
306 | println("Wow!")
307 | }
308 |
309 | // Correct:
310 | if (true) statement1 else statement2
311 |
312 | // Correct:
313 | try {
314 | foo()
315 | } catch {
316 | ...
317 | }
318 |
319 | // Wrong:
320 | if (true)
321 | println("Wow!")
322 |
323 | // Wrong:
324 | try foo() catch {
325 | ...
326 | }
327 | ```
328 |
329 |
330 | ### Long Literals
331 |
332 | Suffix long literal values with uppercase `L`. It is often hard to differentiate lowercase `l` from `1`.
333 | ```scala
334 | val longValue = 5432L // Do this
335 |
336 | val longValue = 5432l // Do NOT do this
337 | ```
338 |
339 |
340 | ### Documentation Style
341 |
342 | Use Java docs style instead of Scala docs style.
343 | ```scala
344 | /** This is a correct one-liner, short description. */
345 |
346 | /**
347 | * This is correct multi-line JavaDoc comment. And
348 | * this is my second line, and if I keep typing, this would be
349 | * my third line.
350 | */
351 |
352 | /** In Spark, we don't use the ScalaDoc style so this
353 | * is not correct.
354 | */
355 | ```
356 |
357 |
358 | ### Ordering within a Class
359 |
360 | If a class is long and has many methods, group them logically into different sections, and use comment headers to organize them.
361 | ```scala
362 | class DataFrame {
363 |
364 | ///////////////////////////////////////////////////////////////////////////
365 | // DataFrame operations
366 | ///////////////////////////////////////////////////////////////////////////
367 |
368 | ...
369 |
370 | ///////////////////////////////////////////////////////////////////////////
371 | // RDD operations
372 | ///////////////////////////////////////////////////////////////////////////
373 |
374 | ...
375 | }
376 | ```
377 |
378 | Of course, the situation in which a class grows this long is strongly discouraged, and is generally reserved only for building certain public APIs.
379 |
380 |
381 | ### Imports
382 |
383 | - __Avoid using wildcard imports__, unless you are importing more than 6 entities, or implicit methods. Wildcard imports make the code less robust to external changes.
384 | - Always import packages using absolute paths (e.g. `scala.util.Random`) instead of relative ones (e.g. `util.Random`).
385 | - In addition, sort imports in the following order:
386 | * `java.*` and `javax.*`
387 | * `scala.*`
388 | * Third-party libraries (`org.*`, `com.*`, etc)
389 | * Project classes (`com.databricks.*` or `org.apache.spark` if you are working on Spark)
390 | - Within each group, imports should be sorted in alphabetic ordering.
391 | - You can use IntelliJ's import organizer to handle this automatically, using the following config:
392 |
393 | ```
394 | java
395 | javax
396 | _______ blank line _______
397 | scala
398 | _______ blank line _______
399 | all other imports
400 | _______ blank line _______
401 | com.databricks // or org.apache.spark if you are working on Spark
402 | ```
403 |
404 |
405 | ### Pattern Matching
406 |
407 | - For method whose entire body is a pattern match expression, put the match on the same line as the method declaration if possible to reduce one level of indentation.
408 | ```scala
409 | def test(msg: Message): Unit = msg match {
410 | case ...
411 | }
412 | ```
413 |
414 | - When calling a function with a closure (or partial function), if there is only one case, put the case on the same line as the function invocation.
415 | ```scala
416 | list.zipWithIndex.map { case (elem, i) =>
417 | // ...
418 | }
419 | ```
420 | If there are multiple cases, indent and wrap them.
421 | ```scala
422 | list.map {
423 | case a: Foo => ...
424 | case b: Bar => ...
425 | }
426 | ```
427 |
428 | - If the only goal is to match on the type of the object, do NOT expand fully all the arguments, as it makes refactoring more difficult and the code more error prone.
429 | ```scala
430 | case class Pokemon(name: String, weight: Int, hp: Int, attack: Int, defense: Int)
431 | case class Human(name: String, hp: Int)
432 |
433 | // Do NOT do the following, because
434 | // 1. When a new field is added to Pokemon, we need to change this pattern matching as well
435 | // 2. It is easy to mismatch the arguments, especially for the ones that have the same data types
436 | targets.foreach {
437 | case target @ Pokemon(_, _, hp, _, defense) =>
438 | val loss = sys.min(0, myAttack - defense)
439 | target.copy(hp = hp - loss)
440 | case target @ Human(_, hp) =>
441 | target.copy(hp = hp - myAttack)
442 | }
443 |
444 | // Do this:
445 | targets.foreach {
446 | case target: Pokemon =>
447 | val loss = sys.min(0, myAttack - target.defense)
448 | target.copy(hp = target.hp - loss)
449 | case target: Human =>
450 | target.copy(hp = target.hp - myAttack)
451 | }
452 | ```
453 |
454 |
455 | ### Infix Methods
456 |
457 | __Avoid infix notation__ for methods that aren't symbolic methods (i.e. operator overloading).
458 | ```scala
459 | // Correct
460 | list.map(func)
461 | string.contains("foo")
462 |
463 | // Wrong
464 | list map (func)
465 | string contains "foo"
466 |
467 | // But overloaded operators should be invoked in infix style
468 | arrayBuffer += elem
469 | ```
470 |
471 |
472 | ### Anonymous Methods
473 |
474 | __Avoid excessive parentheses and curly braces__ for anonymous methods.
475 | ```scala
476 | // Correct
477 | list.map { item =>
478 | ...
479 | }
480 |
481 | // Correct
482 | list.map(item => ...)
483 |
484 | // Wrong
485 | list.map(item => {
486 | ...
487 | })
488 |
489 | // Wrong
490 | list.map { item => {
491 | ...
492 | }}
493 |
494 | // Wrong
495 | list.map({ item => ... })
496 | ```
497 |
498 |
499 | ## Scala Language Features
500 |
501 | ### Case Classes and Immutability
502 |
503 | Case classes are regular classes but extended by the compiler to automatically support:
504 | - Public getters for constructor parameters
505 | - Copy constructor
506 | - Pattern matching on constructor parameters
507 | - Automatic toString/hash/equals implementation
508 |
509 | Constructor parameters should NOT be mutable for case classes. Instead, use copy constructor.
510 | Having mutable case classes can be error prone, e.g. hash maps might place the object in the wrong bucket using the old hash code.
511 | ```scala
512 | // This is OK
513 | case class Person(name: String, age: Int)
514 |
515 | // This is NOT OK
516 | case class Person(name: String, var age: Int)
517 |
518 | // To change values, use the copy constructor to create a new instance
519 | val p1 = Person("Peter", 15)
520 | val p2 = p2.copy(age = 16)
521 | ```
522 |
523 |
524 | ### apply Method
525 |
526 | Avoid defining apply methods on classes. These methods tend to make the code less readable, especially for people less familiar with Scala. It is also harder for IDEs (or grep) to trace. In the worst case, it can also affect correctness of the code in surprising ways, as demonstrated in [Parentheses](#parentheses).
527 |
528 | It is acceptable to define apply methods on companion objects as factory methods. In these cases, the apply method should return the companion class type.
529 | ```scala
530 | object TreeNode {
531 | // This is OK
532 | def apply(name: String): TreeNode = ...
533 |
534 | // This is bad because it does not return a TreeNode
535 | def apply(name: String): String = ...
536 | }
537 | ```
538 |
539 |
540 | ### override Modifier
541 | Always add override modifier for methods, both for overriding concrete methods and implementing abstract methods. The Scala compiler does not require `override` for implementing abstract methods. However, we should always add `override` to make the override obvious, and to avoid accidental non-overrides due to non-matching signatures.
542 | ```scala
543 | trait Parent {
544 | def hello(data: Map[String, String]): Unit = {
545 | print(data)
546 | }
547 | }
548 |
549 | class Child extends Parent {
550 | import scala.collection.Map
551 |
552 | // The following method does NOT override Parent.hello,
553 | // because the two Maps have different types.
554 | // If we added "override" modifier, the compiler would've caught it.
555 | def hello(data: Map[String, String]): Unit = {
556 | print("This is supposed to override the parent method, but it is actually not!")
557 | }
558 | }
559 | ```
560 |
561 |
562 |
563 | ### Destructuring Binds
564 |
565 | Destructuring bind (sometimes called tuple extraction) is a convenient way to assign two variables in one expression.
566 | ```scala
567 | val (a, b) = (1, 2)
568 | ```
569 |
570 | However, do NOT use them in constructors, especially when `a` and `b` need to be marked transient. The Scala compiler generates an extra Tuple2 field that will not be transient for the above example.
571 | ```scala
572 | class MyClass {
573 | // This will NOT work because the compiler generates a non-transient Tuple2
574 | // that points to both a and b.
575 | @transient private val (a, b) = someFuncThatReturnsTuple2()
576 | }
577 | ```
578 |
579 |
580 | ### Call by Name
581 |
582 | __Avoid using call by name__. Use `() => T` explicitly.
583 |
584 | Background: Scala allows method parameters to be defined by-name, e.g. the following would work:
585 | ```scala
586 | def print(value: => Int): Unit = {
587 | println(value)
588 | println(value + 1)
589 | }
590 |
591 | var a = 0
592 | def inc(): Int = {
593 | a += 1
594 | a
595 | }
596 |
597 | print(inc())
598 | ```
599 | in the above code, `inc()` is passed into `print` as a closure and is executed (twice) in the print method, rather than being passed in as a value `1`. The main problem with call-by-name is that the caller cannot differentiate between call-by-name and call-by-value, and thus cannot know for sure whether the expression will be executed or not (or maybe worse, multiple times). This is especially dangerous for expressions that have side-effect.
600 |
601 |
602 | ### Multiple Parameter Lists
603 |
604 | __Avoid using multiple parameter lists__. They complicate operator overloading, and can confuse programmers less familiar with Scala. For example:
605 |
606 | ```scala
607 | // Avoid this!
608 | case class Person(name: String, age: Int)(secret: String)
609 | ```
610 |
611 | One notable exception is the use of a 2nd parameter list for implicits when defining low-level libraries. That said, [implicits should be avoided](#implicits)!
612 |
613 |
614 | ### Symbolic Methods (Operator Overloading)
615 |
616 | __Do NOT use symbolic method names__, unless you are defining them for natural arithmetic operations (e.g. `+`, `-`, `*`, `/`). Under no other circumstances should they be used. Symbolic method names make it very hard to understand the intent of the methods. Consider the following two examples:
617 | ```scala
618 | // symbolic method names are hard to understand
619 | channel ! msg
620 | stream1 >>= stream2
621 |
622 | // self-evident what is going on
623 | channel.send(msg)
624 | stream1.join(stream2)
625 | ```
626 |
627 |
628 | ### Type Inference
629 |
630 | Scala type inference, especially left-side type inference and closure inference, can make code more concise. That said, there are a few cases where explicit typing should be used:
631 |
632 | - __Public methods should be explicitly typed__, otherwise the compiler's inferred type can often surprise you.
633 | - __Implicit methods should be explicitly typed__, otherwise it can crash the Scala compiler with incremental compilation.
634 | - __Variables or closures with non-obvious types should be explicitly typed__. A good litmus test is that explicit types should be used if a code reviewer cannot determine the type in 3 seconds.
635 |
636 |
637 | ### Return Statements
638 |
639 | __Avoid using return in closures__. `return` is turned into ``try/catch`` of ``scala.runtime.NonLocalReturnControl`` by the compiler. This can lead to unexpected behaviors. Consider the following example:
640 | ```scala
641 | def receive(rpc: WebSocketRPC): Option[Response] = {
642 | tableFut.onComplete { table =>
643 | if (table.isFailure) {
644 | return None // Do not do that!
645 | } else { ... }
646 | }
647 | }
648 | ```
649 | the `.onComplete` method takes the anonymous closure `{ table => ... }` and passes it to a different thread. This closure eventually throws the `NonLocalReturnControl` exception that is captured __in a different thread__ . It has no effect on the poor method being executed here.
650 |
651 | However, there are a few cases where `return` is preferred.
652 |
653 | - Use `return` as a guard to simplify control flow without adding a level of indentation
654 | ```scala
655 | def doSomething(obj: Any): Any = {
656 | if (obj eq null) {
657 | return null
658 | }
659 | // do something ...
660 | }
661 | ```
662 |
663 | - Use `return` to terminate a loop early, rather than constructing status flags
664 | ```scala
665 | while (true) {
666 | if (cond) {
667 | return
668 | }
669 | }
670 | ```
671 |
672 | ### Recursion and Tail Recursion
673 |
674 | __Avoid using recursion__, unless the problem can be naturally framed recursively (e.g. graph traversal, tree traversal).
675 |
676 | For methods that are meant to be tail recursive, apply `@tailrec` annotation to make sure the compiler can check it is tail recursive. (You will be surprised how often seemingly tail recursive code is actually not tail recursive due to the use of closures and functional transformations.)
677 |
678 | Most code is easier to reason about with a simple loop and explicit state machines. Expressing it with tail recursions (and accumulators) can make it more verbose and harder to understand. For example, the following imperative code is more readable than the tail recursive version:
679 |
680 | ```scala
681 | // Tail recursive version.
682 | def max(data: Array[Int]): Int = {
683 | @tailrec
684 | def max0(data: Array[Int], pos: Int, max: Int): Int = {
685 | if (pos == data.length) {
686 | max
687 | } else {
688 | max0(data, pos + 1, if (data(pos) > max) data(pos) else max)
689 | }
690 | }
691 | max0(data, 0, Int.MinValue)
692 | }
693 |
694 | // Explicit loop version
695 | def max(data: Array[Int]): Int = {
696 | var max = Int.MinValue
697 | for (v <- data) {
698 | if (v > max) {
699 | max = v
700 | }
701 | }
702 | max
703 | }
704 | ```
705 |
706 |
707 | ### Implicits
708 |
709 | __Avoid using implicits__, unless:
710 | - you are building a domain-specific language
711 | - you are using it for implicit type parameters (e.g. `ClassTag`, `TypeTag`)
712 | - you are using it private to your own class to reduce verbosity of converting from one type to another (e.g. Scala closure to Java closure)
713 |
714 | When implicits are used, we must ensure that another engineer who did not author the code can understand the semantics of the usage without reading the implicit definition itself. Implicits have very complicated resolution rules and make the code base extremely difficult to understand. From Twitter's Effective Scala guide: "If you do find yourself using implicits, always ask yourself if there is a way to achieve the same thing without their help."
715 |
716 | If you must use them (e.g. enriching some DSL), do not overload implicit methods, i.e. make sure each implicit method has distinct names, so users can selectively import them.
717 | ```scala
718 | // Don't do the following, as users cannot selectively import only one of the methods.
719 | object ImplicitHolder {
720 | def toRdd(seq: Seq[Int]): RDD[Int] = ...
721 | def toRdd(seq: Seq[Long]): RDD[Long] = ...
722 | }
723 |
724 | // Do the following:
725 | object ImplicitHolder {
726 | def intSeqToRdd(seq: Seq[Int]): RDD[Int] = ...
727 | def longSeqToRdd(seq: Seq[Long]): RDD[Long] = ...
728 | }
729 | ```
730 |
731 |
732 | ## Exception Handling (Try vs try)
733 |
734 | - Do NOT catch Throwable or Exception. Use `scala.util.control.NonFatal`:
735 | ```scala
736 | try {
737 | ...
738 | } catch {
739 | case NonFatal(e) =>
740 | // handle exception; note that NonFatal does not match InterruptedException
741 | case e: InterruptedException =>
742 | // handle InterruptedException
743 | }
744 | ```
745 | This ensures that we do not catch `NonLocalReturnControl` (as explained in [Return Statements](#return-statements)).
746 |
747 | - Do NOT use `Try` in APIs, that is, do NOT return Try in any methods. Instead, prefer explicitly throwing exceptions for abnormal execution and Java style try/catch for exception handling.
748 |
749 | Background information: Scala provides monadic error handling (through `Try`, `Success`, and `Failure`) that facilitates chaining of actions. However, we found from our experience that the use of it often leads to more levels of nesting that are harder to read. In addition, it is often unclear what the semantics are for expected errors vs exceptions because those are not encoded in `Try`. As a result, we discourage the use of `Try` for error handling. In particular:
750 |
751 | As a contrived example:
752 | ```scala
753 | class UserService {
754 | /** Look up a user's profile in the user database. */
755 | def get(userId: Int): Try[User]
756 | }
757 | ```
758 | is better written as
759 | ```scala
760 | class UserService {
761 | /**
762 | * Look up a user's profile in the user database.
763 | * @return None if the user is not found.
764 | * @throws DatabaseConnectionException when we have trouble connecting to the database/
765 | */
766 | @throws(DatabaseConnectionException)
767 | def get(userId: Int): Option[User]
768 | }
769 | ```
770 | The 2nd one makes it very obvious error cases the caller needs to handle.
771 |
772 |
773 | ### Options
774 |
775 | - Use `Option` when the value can be empty. Compared with `null`, an `Option` explicitly states in the API contract that the value can be `None`.
776 | - When constructing an `Option`, use `Option` rather than `Some` to guard against `null` values.
777 | ```scala
778 | def myMethod1(input: String): Option[String] = Option(transform(input))
779 |
780 | // This is not as robust because transform can return null, and then
781 | // myMethod2 will return Some(null).
782 | def myMethod2(input: String): Option[String] = Some(transform(input))
783 | ```
784 | - Do not use None to represent exceptions. Instead, throw exceptions explicitly.
785 | - Do not call `get` directly on an `Option`, unless you know absolutely for sure the `Option` has some value.
786 |
787 |
788 | ### Monadic Chaining
789 |
790 | One of Scala's powerful features is monadic chaining. Almost everything (e.g. collections, Option, Future, Try) is a monad and operations on them can be chained together. This is an incredibly powerful concept, but chaining should be used sparingly. In particular:
791 |
792 | - Avoid chaining (and/or nesting) more than 3 operations.
793 | - If it takes more than 5 seconds to figure out what the logic is, try hard to think about how you can express the same functionality without using monadic chaining. As a general rule, watch out for flatMaps and folds.
794 | - A chain should almost always be broken after a flatMap (because of the type change).
795 |
796 | A chain can often be made more understandable by giving the intermediate result a variable name, by explicitly typing the variable, and by breaking it down into more procedural style. As a contrived example:
797 | ```scala
798 | class Person(val data: Map[String, String])
799 | val database = Map[String, Person]
800 | // Sometimes the client can store "null" value in the store "address"
801 |
802 | // A monadic chaining approach
803 | def getAddress(name: String): Option[String] = {
804 | database.get(name).flatMap { elem =>
805 | elem.data.get("address")
806 | .flatMap(Option.apply) // handle null value
807 | }
808 | }
809 |
810 | // A more readable approach, despite much longer
811 | def getAddress(name: String): Option[String] = {
812 | if (!database.contains(name)) {
813 | return None
814 | }
815 |
816 | database(name).data.get("address") match {
817 | case Some(null) => None // handle null value
818 | case Some(addr) => Option(addr)
819 | case None => None
820 | }
821 | }
822 |
823 | ```
824 |
825 |
826 | ## Concurrency
827 |
828 | ### Scala concurrent.Map
829 |
830 | __Prefer `java.util.concurrent.ConcurrentHashMap` over `scala.collection.concurrent.Map`__. In particular the `getOrElseUpdate` method in `scala.collection.concurrent.Map` is not atomic (fixed in Scala 2.11.6, [SI-7943](https://issues.scala-lang.org/browse/SI-7943)). Since all the projects we work on require cross-building for both Scala 2.10 and Scala 2.11, `scala.collection.concurrent.Map` should be avoided.
831 |
832 |
833 | ### Explicit Synchronization vs Concurrent Collections
834 |
835 | There are 3 recommended ways to make concurrent accesses to shared states safe. __Do NOT mix them__ because that could make the program very hard to reason about and lead to deadlocks.
836 |
837 | 1. `java.util.concurrent.ConcurrentHashMap`: Use when all states are captured in a map, and high degree of contention is expected.
838 | ```scala
839 | private[this] val map = new java.util.concurrent.ConcurrentHashMap[String, String]
840 | ```
841 |
842 | 2. `java.util.Collections.synchronizedMap`: Use when all states are captured in a map, and contention is not expected but you still want to make code safe. In case of no contention, the JVM JIT compiler is able to remove the synchronization overhead via biased locking.
843 | ```scala
844 | private[this] val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])
845 | ```
846 |
847 | 3. Explicit synchronization by synchronizing all critical sections: can used to guard multiple variables. Similar to 2, the JVM JIT compiler can remove the synchronization overhead via biased locking.
848 | ```scala
849 | class Manager {
850 | private[this] var count = 0
851 | private[this] val map = new java.util.HashMap[String, String]
852 | def update(key: String, value: String): Unit = synchronized {
853 | map.put(key, value)
854 | count += 1
855 | }
856 | def getCount: Int = synchronized { count }
857 | }
858 | ```
859 |
860 | Note that for case 1 and case 2, do not let views or iterators of the collections escape the protected area. This can happen in non-obvious ways, e.g. when returning `Map.keySet` or `Map.values`. If views or values are required to pass around, make a copy of the data.
861 | ```scala
862 | val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])
863 |
864 | // This is broken!
865 | def values: Iterable[String] = map.values
866 |
867 | // Instead, copy the elements
868 | def values: Iterable[String] = map.synchronized { Seq(map.values: _*) }
869 | ```
870 |
871 | ### Explicit Synchronization vs Atomic Variables vs @volatile
872 |
873 | The `java.util.concurrent.atomic` package provides primitives for lock-free access to primitive types, such as `AtomicBoolean`, `AtomicInteger`, and `AtomicReference`.
874 |
875 | Always prefer Atomic variables over `@volatile`. They have a strict superset of the functionality and are more visible in code. Atomic variables are implemented using `@volatile` under the hood.
876 |
877 | Prefer Atomic variables over explicit synchronization when: (1) all critical updates for an object are confined to a *single* variable and contention is expected. Atomic variables are lock-free and permit more efficient contention. Or (2) synchronization is clearly expressed as a `getAndSet` operation. For example:
878 | ```scala
879 | // good: clearly and efficiently express only-once execution of concurrent code
880 | val initialized = new AtomicBoolean(false)
881 | ...
882 | if (!initialized.getAndSet(true)) {
883 | ...
884 | }
885 |
886 | // poor: less clear what is guarded by synchronization, may unnecessarily synchronize
887 | val initialized = false
888 | ...
889 | var wasInitialized = false
890 | synchronized {
891 | wasInitialized = initialized
892 | initialized = true
893 | }
894 | if (!wasInitialized) {
895 | ...
896 | }
897 | ```
898 |
899 | ### Private Fields
900 |
901 | Note that `private` fields are still accessible by other instances of the same class, so protecting it with `this.synchronized` (or just `synchronized`) is not technically sufficient. Make the field `private[this]` instead.
902 | ```scala
903 | // The following is still unsafe.
904 | class Foo {
905 | private var count: Int = 0
906 | def inc(): Unit = synchronized { count += 1 }
907 | }
908 |
909 | // The following is safe.
910 | class Foo {
911 | private[this] var count: Int = 0
912 | def inc(): Unit = synchronized { count += 1 }
913 | }
914 | ```
915 |
916 |
917 | ### Isolation
918 |
919 | In general, concurrency and synchronization logic should be isolated and contained as much as possible. This effectively means:
920 |
921 | - Avoid surfacing the internals of synchronization primitives in APIs, user-facing methods, and callbacks.
922 | - For complex modules, create a small, inner module that captures the concurrency primitives.
923 |
924 |
925 | ## Performance
926 |
927 | For the vast majority of the code you write, performance should not be a concern. However, for performance sensitive code, here are some tips:
928 |
929 | ### Microbenchmarks
930 |
931 | It is ridiculously hard to write a good microbenchmark because the Scala compiler and the JVM JIT compiler do a lot of magic to the code. More often than not, your microbenchmark code is not measuring the thing you want to measure.
932 |
933 | Use [jmh](http://openjdk.java.net/projects/code-tools/jmh/) if you are writing microbenchmark code. Make sure you read through [all the sample microbenchmarks](http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/) so you understand the effect of deadcode elimination, constant folding, and loop unrolling on microbenchmarks.
934 |
935 |
936 | ### Traversal and zipWithIndex
937 |
938 | Use `while` loops instead of `for` loops or functional transformations (e.g. `map`, `foreach`). For loops and functional transformations are very slow (due to virtual function calls and boxing).
939 | ```scala
940 |
941 | val arr = // array of ints
942 | // zero out even positions
943 | val newArr = list.zipWithIndex.map { case (elem, i) =>
944 | if (i % 2 == 0) 0 else elem
945 | }
946 |
947 | // This is a high performance version of the above
948 | val newArr = new Array[Int](arr.length)
949 | var i = 0
950 | val len = newArr.length
951 | while (i < len) {
952 | newArr(i) = if (i % 2 == 0) 0 else arr(i)
953 | i += 1
954 | }
955 | ```
956 |
957 | ### Option and null
958 |
959 | For performance sensitive code, prefer `null` over `Option`, in order to avoid virtual method calls and boxing. Label the nullable fields clearly with Nullable.
960 | ```scala
961 | class Foo {
962 | @javax.annotation.Nullable
963 | private[this] var nullableField: Bar = _
964 | }
965 | ```
966 |
967 | ### Scala Collection Library
968 |
969 | For performance sensitive code, prefer Java collection library over Scala ones, since the Scala collection library often is slower than Java's.
970 |
971 | ### private[this]
972 |
973 | For performance sensitive code, prefer `private[this]` over `private`. `private[this]` generates a field, rather than creating an accessor method. In our experience, the JVM JIT compiler cannot always inline `private` field accessor methods, and thus it is safer to use `private[this]` to ensure no virtual method call for accessing a field.
974 | ```scala
975 | class MyClass {
976 | private val field1 = ...
977 | private[this] val field2 = ...
978 |
979 | def perfSensitiveMethod(): Unit = {
980 | var i = 0
981 | while (i < 1000000) {
982 | field1 // This might invoke a virtual method call
983 | field2 // This is just a field access
984 | i += 1
985 | }
986 | }
987 | }
988 | ```
989 |
990 |
991 | ## Java Interoperability
992 |
993 | This section covers guidelines for building Java compatible APIs. These do not apply if the component you are building does not require interoperability with Java. It is mostly drawn from our experience in developing the Java APIs for Spark.
994 |
995 |
996 | ### Java Features Missing from Scala
997 |
998 | The following Java features are missing from Scala. If you need the following, define them in Java instead. However, be reminded that ScalaDocs are not generated for files defined in Java.
999 |
1000 | - Static fields
1001 | - Static inner classes
1002 | - Java enums
1003 | - Annotations
1004 |
1005 |
1006 | ### Traits and Abstract Classes
1007 |
1008 | For interfaces that can be implemented externally, keep in mind the following:
1009 |
1010 | - Traits with default method implementations are not usable in Java. Use abstract classes instead.
1011 | - In general, avoid using traits unless you know for sure the interface will not have any default implementation even in its future evolution.
1012 | ```scala
1013 | // The default implementation doesn't work in Java
1014 | trait Listener {
1015 | def onTermination(): Unit = { ... }
1016 | }
1017 |
1018 | // Works in Java
1019 | abstract class Listener {
1020 | def onTermination(): Unit = { ... }
1021 | }
1022 | ```
1023 |
1024 |
1025 | ### Type Aliases
1026 |
1027 | Do NOT use type aliases. They are not visible in bytecode (and Java).
1028 |
1029 |
1030 | ### Default Parameter Values
1031 |
1032 | Do NOT use default parameter values. Overload the method instead.
1033 | ```scala
1034 | // Breaks Java interoperability
1035 | def sample(ratio: Double, withReplacement: Boolean = false): RDD[T] = { ... }
1036 |
1037 | // The following two work
1038 | def sample(ratio: Double, withReplacement: Boolean): RDD[T] = { ... }
1039 | def sample(ratio: Double): RDD[T] = sample(ratio, withReplacement = false)
1040 | ```
1041 |
1042 | ### Multiple Parameter Lists
1043 |
1044 | Do NOT use multi-parameter lists.
1045 |
1046 | ### Varargs
1047 |
1048 | - Apply `@scala.annotation.varargs` annotation for a vararg method to be usable in Java. The Scala compiler creates two methods, one for Scala (bytecode parameter is a Seq) and one for Java (bytecode parameter array).
1049 | ```scala
1050 | @scala.annotation.varargs
1051 | def select(exprs: Expression*): DataFrame = { ... }
1052 | ```
1053 |
1054 | - Note that abstract vararg methods does NOT work for Java, due to a Scala compiler bug ([SI-1459](https://issues.scala-lang.org/browse/SI-1459), [SI-9013](https://issues.scala-lang.org/browse/SI-9013)).
1055 |
1056 | - Be careful with overloading varargs methods. Overloading a vararg method with another vararg type can break source compatibility.
1057 | ```scala
1058 | class Database {
1059 | @scala.annotation.varargs
1060 | def remove(elems: String*): Unit = ...
1061 |
1062 | // Adding this will break source compatibility for no-arg remove() call.
1063 | @scala.annotation.varargs
1064 | def remove(elems: People*): Unit = ...
1065 | }
1066 |
1067 | // This won't compile anymore because it is ambiguous
1068 | new Database().remove()
1069 | ```
1070 | Instead, define an explicit first parameter followed by vararg:
1071 | ```scala
1072 | class Database {
1073 | @scala.annotation.varargs
1074 | def remove(elems: String*): Unit = ...
1075 |
1076 | // The following is OK.
1077 | @scala.annotation.varargs
1078 | def remove(elem: People, elems: People*): Unit = ...
1079 | }
1080 | ```
1081 |
1082 |
1083 | ### Implicits
1084 |
1085 | Do NOT use implicits, for a class or method. This includes `ClassTag`, `TypeTag`.
1086 | ```scala
1087 | class JavaFriendlyAPI {
1088 | // This is NOT Java friendly, since the method contains an implicit parameter (ClassTag).
1089 | def convertTo[T: ClassTag](): T
1090 | }
1091 | ```
1092 |
1093 | ### Companion Objects, Static Methods and Fields
1094 |
1095 | There are a few things to watch out for when it comes to companion objects and static methods/fields.
1096 |
1097 | - Companion objects are awkward to use in Java (a companion object `Foo` is a static field `MODULE$` of type `Foo$` in class `Foo$`).
1098 | ```scala
1099 | object Foo
1100 |
1101 | // equivalent to the following Java code
1102 | public class Foo$ {
1103 | Foo$ MODULE$ = // instantiation of the object
1104 | }
1105 | ```
1106 | If the companion object is important to use, create a Java static field in a separate class.
1107 |
1108 | - Unfortunately, there is no way to define a JVM static field in Scala. Create a Java file to define that.
1109 | - Methods in companion objects are automatically turned into static methods in the companion class, unless there is a method name conflict. The best (and future-proof) way to guarantee the generation of static methods is to add a test file written in Java that calls the static method.
1110 | ```scala
1111 | class Foo {
1112 | def method2(): Unit = { ... }
1113 | }
1114 |
1115 | object Foo {
1116 | def method1(): Unit = { ... } // a static method Foo.method1 is created in bytecode
1117 | def method2(): Unit = { ... } // a static method Foo.method2 is NOT created in bytecode
1118 | }
1119 |
1120 | // FooJavaTest.java (in test/scala/com/databricks/...)
1121 | public class FooJavaTest {
1122 | public static void compileTest() {
1123 | Foo.method1(); // This one should compile fine
1124 | Foo.method2(); // This one should fail because method2 is not generated.
1125 | }
1126 | }
1127 | ```
1128 |
1129 | - A case object (or even just plain companion object) MyClass is actually not of type MyClass.
1130 | ```scala
1131 | case object MyClass
1132 |
1133 | // Test.java
1134 | if (MyClass$.MODULE instanceof MyClass) {
1135 | // The above condition is always false
1136 | }
1137 | ```
1138 | To implement the proper type hierarchy, define a companion class, and then extend that in case object:
1139 | ```scala
1140 | class MyClass
1141 | case object MyClass extends MyClass
1142 | ```
1143 |
1144 | ## Testing
1145 |
1146 | ### Intercepting Exceptions
1147 |
1148 | When testing that performing a certain action (say, calling a function with an invalid argument) throws an exception, be as specific as possible about the type of exception you expect to be thrown. You should NOT simply `intercept[Exception]` or `intercept[Throwable]` (to use the ScalaTest syntax), as this will just assert that _any_ exception is thrown. Often times, this will just catch errors you made when setting up your testing mocks and your test will silently pass without actually checking the behavior you want to verify.
1149 |
1150 | ```scala
1151 | // This is WRONG
1152 | intercept[Exception] {
1153 | thingThatThrowsException()
1154 | }
1155 |
1156 | // This is CORRECT
1157 | intercept[MySpecificTypeOfException] {
1158 | thingThatThrowsException()
1159 | }
1160 | ```
1161 |
1162 | If you cannot be more specific about the type of exception that the code will throw, that is often a sign of code smell. You should either test at a lower level or modify the underlying code to throw a more specific exception.
1163 |
1164 | ## Miscellaneous
1165 |
1166 | ### Prefer nanoTime over currentTimeMillis
1167 |
1168 | When computing a *duration* or checking for a *timeout*, avoid using `System.currentTimeMillis()`. Use `System.nanoTime()` instead, even if you are not interested in sub-millisecond precision.
1169 |
1170 | `System.currentTimeMillis()` returns current wallclock time and will follow changes to the system clock. Thus, negative wallclock adjustments can cause timeouts to "hang" for a long time (until wallclock time has caught up to its previous value again). This can happen when ntpd does a "step" after the network has been disconnected for some time. The most canonical example is during system bootup when DHCP takes longer than usual. This can lead to failures that are really hard to understand/reproduce. `System.nanoTime()` is guaranteed to be monotonically increasing irrespective of wallclock changes.
1171 |
1172 | Caveats:
1173 | - Never serialize an absolute `nanoTime()` value or pass it to another system. The absolute value is meaningless and system-specific and resets when the system reboots.
1174 | - The absolute `nanoTime()` value is not guaranteed to be positive (but `t2 - t1` is guaranteed to yield the right result)
1175 | - `nanoTime()` rolls over every 292 years. So if your Spark job is going to take a really long time, you may need something else :)
1176 |
1177 |
1178 | ### Prefer URI over URL
1179 |
1180 | When storing the URL of a service, you should use the `URI` representation.
1181 |
1182 | The [equality check](http://docs.oracle.com/javase/7/docs/api/java/net/URL.html#equals(java.lang.Object)) of `URL` actually performs a (blocking) network call to resolve the IP address. The `URI` class performs field equality and is a superset of `URL` as to what it can represent.
1183 |
1184 | ### Prefer existing well-tested methods over reinventing the wheel
1185 |
1186 | When there is an existing well-tesed method and it doesn't cause any performance issue, prefer to use it. Reimplementing such method may introduce bugs and requires spending time testing it (maybe we don't even remember to test it!).
1187 |
1188 | ```scala
1189 | val beginNs = System.nanoTime()
1190 | // Do something
1191 | Thread.sleep(1000)
1192 | val elapsedNs = System.nanoTime() - beginNs
1193 |
1194 | // This is WRONG. It uses magic numbers and is pretty easy to make mistakes
1195 | val elapsedMs = elapsedNs / 1000 / 1000
1196 |
1197 | // Use the Java TimeUnit API. This is CORRECT
1198 | import java.util.concurrent.TimeUnit
1199 | val elapsedMs2 = TimeUnit.NANOSECONDS.toMillis(elapsedNs)
1200 |
1201 | // Use the Scala Duration API. This is CORRECT
1202 | import scala.concurrent.duration._
1203 | val elapsedMs3 = elapsedNs.nanos.toMillis
1204 | ```
1205 |
1206 | Exceptions:
1207 | - Using an existing well-tesed method requires adding a new dependency. If such method is pretty simple, reimplementing it is better than adding a dependency. But remember to test it.
1208 | - The existing method is not optimized for our usage and is too slow. But benchmark it first, avoid premature optimization.
1209 |
--------------------------------------------------------------------------------