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