= Scala = * https://docs.scala-lang.org/ * 기본적으로 Java를 다뤄봤다는 전제하에 실용적인 측면으로만 기술. * 2.12 이상 버전을 기준으로 기술. {{tag>Language Scala JVM Object_Oriented_Programming Functional_Programming}} = 환경 설정 = * JDK 1.8 이상 설치 == Command Line == https://www.scala-lang.org/download/ === SBT (Scala Build Tools) === Scala의 Ant/Maven/Gradle와 비교될 수 있음. 직접으로 프로젝트 생성, 배포 관여. ==== Create templates ==== https://www.scala-sbt.org/1.x/docs/sbt-new-and-Templates.html sbt new scala/scala-seed.g8 ==== Fat Jar ==== https://github.com/sbt/sbt-assembly # Build (Fat Jar 생성) ## build.sbt에 ## addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.6") ## 추가 필요 ### 생성된 jar 위치 : target/scala-[version]/*.jar sbt assembly === Scala Binaries (선택 사항) === * REPL(scala), 단독 Compile(scalac)을 이용하기 위해 필요 # 실행 scala scala> 1 res0: Int = 1 # 종료 scala> :quit ## 혹은 scala> :q == IDE == - Intellij에 기본적으로 SBT 내장. Intellij만 설치하면 된다. === REPL 실행 방법 === 왼쪽 Project Tree에서 아무 Class에 오른쪽으로 누르고 "Run Scala Console". = Hello, world! = * 가변 인자 args를 입력 받아 "Hello, world!"를 출력하는 예제 작성 == Script == * Compile 하지 않고, Shell script 처럼 사용 // class, main 함수를 선언하지 않고도 가변인자 args 사용 가능! args.foreach(print) println() > scala script.scala Hello , world ! Hello, world! == Compile == https://www.scala-lang.org/documentation/your-first-lines-of-scala.html === App trait 상속 O === object HelloWorld extends App { // class, main 함수를 선언하지 않고도 가변인자 args 사용 가능! args.foreach(print) println() } > scalac HelloWorld.scala > scala HelloWorld Hello , world ! === App trait 상속 X === object HelloWorld { def main(args: Array[String]): Unit = { args.foreach(print) println() } } > scalac HelloWorld.scala > scala HelloWorld Hello , world ! = 문법 = == Identifier == 식별자 규칙 및 관례적 명명법 서술. * 연산자 정의(Operator Defination) Method와 암시적 변환([[language:scala#Implicit Parameters and Conversions|Implicit Parameters and Conversions]])는 간결하고 유연한 표현을 정의할 수 있지만, 잘못 사용하면 가독성을 떨어뜨리는 결과를 낳으니 주의가 필요하다. === Alphanumeric Identifier === # Java와 동일하게 영숫자 혹은 '_', '$'가 가능하나 '$' Scala Compiler가 내부적으로 생성하는 식별자에 사용하는 예약 문자이므로 충돌 가능성이 있기 때문에 사용해서는 안된다. # 상수는 첫글자만 대문자로, Camel Case 표현을 한다. (Java 표현으로 MAX_VALUE라면, Scala는 MaxValue) === Operator Identifier === * Java와 가장 큰 차이점 중 하나. 이를 이용하면 Operator Overloading이 가능. Method에만 적용 가능. * +, :, ?, ~, #, +, ++, ::(List에서 요소 덧붙임), :::(List에 다른 List 추가), , :-> 등 사용 가능. 이 때 길이 제한이 없다. * Overloading 예시 def *** (v: Int) = math.pow(v, 3) ==== 연산자 우선순위와 결합법칙 ==== **:**(Cons; Construct)로 끝나는 연산자는 right-associative. 이외 나머지는 left-associative // left-associative a * b * c = (a * b) * c = a.*(b).*(c) // right-associative a ::: b ::: c = a ::: (b ::: c) = b.:::(c).:::(a) === Mixed Identifier === Alphanumeric + Operator. 예를 들어 myvar_=. Scala Compiler가 Property를 지원하기 위해 내부적으로 생성. === Literal Identifier === 역따옴표(`)를 사용하며 Runtime에서 인식할 수 있는 문자열 생성. 이를 이용하면 예약어도 변수나 Method 이름으로 지정 가능. // 아래 코드는 Compile 및 실행에 문제가 없다. val `val` = 3; def show(`var` : Int) = println(`var`); show(`val`) == 변수 == * val : Immutable * var : mutable == Type == === Type Hierarchy === {{https://docs.scala-lang.org/resources/images/tour/unified-types-diagram.svg}} |<100%>| ^ Any |모든 type의 supertype. equals, hashCode, toString 메서드가 정의됨. | ^ AnyVal |value type을 표현, non-nullable. Double, Float, Long, Int, Short, Byte, Char, Unit, Boolean의 상위 타입. | ^ AnyRef |referenced type을 표현, java.lang.Object에 대응. | ^ Nothing |모든 type의 subtype (i.e. bottom type)으로 value를 가지고 있지 않다. | ^ Null |모든 reference type의 subtype. keyword literal 'null'을 값으로 가지고 있다. | === Literals === ==== Integer Literals ==== * C/Java와 달리 8진(0으로 시작하는 정수형)을 지원하지 않음. 10진과 16진만 지원. ==== String Literals ==== ===== Raw Sring ===== 큰따옴표 3개(""")를 이용하여 그 내부 문자열을 "날 것" 그대로 표현하는 문자열. (공백, 개행 전부 포함) object Test extends App { // Java에서의 일부 특수문자 표현. 가독성이 좋지 않음. val str1 = "Hello\n\"world\""; println(str1) /** * Hello * "world" */ // 공백, 개행 모두 그래도 출력 val str2 = """Hello, "world"! """ println(str2) /** * Hello * "world" */ // |를 넣으면 문자열의 시작을 알림. 이후 stripMargin을 하면 이전 공백 제거 val str3 = """ |Hello, |"world!" """.stripMargin println(str3) /** * Hello * "world" */ } ===== String interpolation ===== 문자열 내부에 표현식(expression)을 내장시키는 것. 이로써 문자열을 이어붙이지 않고 가독성이 좋아짐. * s : "$val" 혹은 "${expression}"과 함께 쓰여 mapping. * raw : 문자열 그대로 출력. * f : printf 형태의 형식 지정. |<100%>| ^ ^ Description ^ Example ^ | s | "$val" 혹은 "${expression}"과 함께 쓰여 mapping. | val str = "9 * 9 = " println(s"$str ${9 * 9}") /** * 9 * 9 = 81 */ | | raw | 문자열 그대로 출력. | println(raw"A\nB\\C\tD\bE") /** * A\nB\\C\tD\bE */ | | f | printf 형태의 형식 지정. \\ ${expression}%[format] | println(f"${math.Pi}%.7f") println(f"${3 * 3}%03d") /** * 3.1415927 * 009 */ | ==== Symbol literals ==== * 작은 따옴표(') + (알파벳 + 숫자) * 단순 식별자가 필요할 때 사용. Java의 enum이나 [[language:clojure#keyword|Clojure의 Keyword]]와 비슷. * Scala의 Strong Type 언어이기 때문에 Weak Type 형태로 사용할 때 사용. * [[language:clojure#keyword|Clojure의 Keyword]]처럼 자기 자신을 반환 * .name 필드로 문자열 변환 가능. === Array === * 모든 것을 Method가 있는 객체로 다루기 때문에 Parameter에 해당하는 호출은 **()**을 사용한다. val fruits = new Array[String](2) // val fruits: String = new Array[String](2) fruits(0) = "apple" // fruites.update(0, "apple") fruits(1) = "banana" // fruites.update(1, "banana") val fruits = Array("apple", "banana") // val fruits = Array.apply("apple", "banana") == Operator == === Operator = Method! === Scala에서 모든 연산자는 Method!! 실질적으로 Scala는 Method를 호출한다. /** Binary Operator **/ val sum = 1 + 2 // 1.+(2) "Hello, world!" indexOf 'o' // "Hello, world!".indexOf('o') "Hello, world!" indexOf ('o', 5) // "Hello, world!".indexOf('o', 5) /** Unary Operator **/ /*** 전위 표기법 ***/ // +,0,!,~ 4가지. val a = -2.0 // (2.0).unary_- /*** 후위 표기법 ***/ // 인자를 취하지 않는 Method를 '.'이나 괄호 없이 호출 val str = "Hello, world!" toLowercase // val str = "Hello, world!".toLowercase() === == & eq === Java와 동일하다. \\ ==, !=은 원시 타입에서 값(value)이 같은지 비교, eq/ne는 JVM Heap에서 참조(reference) 비교 scala> List(1, 2, 3) == List(1, 2, 3) res8: Boolean = true scala> List(1, 2, 3) eq List(1, 2, 3) res9: Boolean = false scala> List(1, 2, 3) ne List(1, 2, 3) res10: Boolean = true // 주의! scala> ("He" + "llo") == "Hello" res11: Boolean = true === Wrapper === scala> 111 max 222 res0: Int = 222 scala> 111 min 222 res1: Int = 111 scala> -math.Pi abs res2: Double = 3.141592653589793 scala> -math.Pi round res3: Long = -3 scala> (1.0 / 0) isInfinity res4: Boolean = true scala> 1 to 100 res5: scala.collection.immutable.Range.Inclusive = Range 1 to 100 scala> "hello" capitalize res6: String = Hello scala> "Hello, World" drop 7 res7: String = World == 내장 제어 구문 == **전체적으로 재귀나 내장함수(reduce, map, filter, reduce)로 대체가 가능하기 때문에 사용을 자제하자. 제어 구문은 함수형 표현에 적합하지 않다.** * 키워드로써 break, continue는 존재하지 않는다. (함수형 언어에 어울리지 않으므로.) * 굳이 break를 쓰고 싶다면 scala.util.control.Breaks 에 있는 것을 사용할 수 있다...만... * 제어 구문들도 함수이므로 var/val에 할당할 수 있다. (이 때, 기본적으로 바로 계산한다. lazy loading이 필요하면 lazy를 맨 앞에 붙이자.) * while은 대체로 중간 결과를 저장하는 변수 var가 필요하기 때문에 사용을 자제하자. === for === foreach 문만 존재한다. 그러나 대체로 반복하는 경우 내장 함수(foreach, reduce, map, filter 등)이 존재하기 때문에 특별한 경우가 아니면 큰 필요성이 없다. /* Collections */ //10 ~ 100 for (i <- 10 to 100) println(i) //10 ~ 99 (최대값 제외) for (i <- 3 until 10) println(i) /* Iterator Guard / Filter. if 표현식이 true일 때만 반복 */ for (file <- new java.io.File(".").listFiles() if file.isFile if file.getName.endsWith(".scala")) println(file) /* 중첩 및 임시 변수 Binding */ // 구구단 출력 for {x <- (1 to 9) y <- (1 to 9) result = x * y} println(f"$x * $y = $result%02d") ==== for-yield ==== 의 값을 Collection로 반환. (//map// 과 같은 효과) for ( <- ) yield // 인자가 없기 때문에 ()를 생략한 형태. def timesTable = for { x <- (1 to 9) y <- (1 to 9) result = x * y} yield f"$x%2d * $y%2d = $result%2d" println(timesTable) // Vector( 1 * 1 = 1, 1 * 2 = 2, ... , 9 * 8 = 72, 9 * 9 = 81) === try-catch === * try, finally는 단 하나의 식만 포함한다면 중괄호 생략 가능. try { val f = new FileReader(".") } catch { case ex: FileNotFoundException => ex.printStackTrace() case ex: IOException => ex.printStackTrace() } finally println("done") * 다른 제어 구조, 함수와 마찬가지로 결과로 값을 반환한다. 이 때, finally 절의 값은 버려진다. * finally에는 값을 반환하지 말고 Side-effect(결과 완료 알림, 파일 저장등)만 이용하자. // 결과 : 2 // Java 형식으로 finally 절 안에 return문을 명시적으로 사용하면 try/cath의 결과를 덮어쓰게 된다. // 잘 생각해보면 Java에서 finally는 어떤 결과로든 항상 실행되는 구문이기 때문. def test1(): Int = try return 1 finally return 2 // 결과 : 1 // Scala 형식으로 작성하면 의도된대로 가장 처음에 만나는 값을 반환한다. def test2(): Int = try 1 finally 2 // 결론적으로 finally에는 값을 사용하지 말자. === match === case의 대안. Java와 달리 타입에 상관없이 어떤 상수값이라도 사용 가능. 이 때, match 앞에 오는 비교대상의 타입을 기본적으로 다른다. (그렇게 되면 case로 비교하는 값들도 그 타입을 따른다.) '_'은 default에 대응하며 "완전히 알려지지 않은 값"을 의미. object Client extends App { def matchTest(x: Any): Any = x match { case 1 => "one" case "two" => 2 case y: Int => "scala.Int" case _ => 'unknown } println(matchTest(3)) // scala.Int } == Functions == * 재귀 함수인 경우 반드시 Return Type 명시, 그외에는 생략 가능. * 함수 길이 긴 경우 사람이 읽기 쉽게 Return Type을 명시하는 게 좋음. * "return" 구문은 선택. 생략 가능. * 본론 문장이 하나면 괄호 생략 가능 * Retury Type의 "Unit"은 Java의 void. 즉, Side-effect를 위해서만 실행하는 함수. * parameter는 val이므로 값을 재할당할 수 없다. (예를 들어 Call-by-reference를 이용하려는 경우) * 함수 안에 함수를 정의할 수 있다. * 인자가 없는 메소드는 소괄호를 생략할 수 있다. 그러나 관례로서 Side-Effect가 있거나 함수 본래 작업 이외의 부가적인 작업이 존재할 경우 소괄호를 붙인다. def sum(x: Int, y: Int): Int = { x + y } // 위와 같은 표현 def sum(x: Int, y: Int) = x + y // Unit 함수. Return 값이 없음을 의미. // Type 추론이 되므로 Return Type에 Unit 생략 가능. def greeting(): Unit = println("Hello, world!") === Local functions === 함수의 중첩이 가능. def test1(x: Int) = { def test1(y: Int) = x * y test1(10) } println(test1(5)) // 50 === Placeholder syntax === 위치 표시자. /* 고차함수를 더 간략하게 작성하는 방법 */ val values = 1 to 10 values.filter((x: Int) => x > 5) // filter의 인자는 Int를 받도록 명시되어 있으므로 생략 가능 values.filter(x => x > 5) // 위치표시자(_)를 이용하면 더 간략한 표현 가능. values.filter(_ > 5) // 아래와 같은 응용도 된다. values.foreach(println _) // values.foreach(x => println(x)) // 그런데 foreach의 인자가 명확한 위치가 나타나기 때문에 생략 가능 values.foreach(println) val func1 = (_:Int) + (_:Int) println(func1(1, 1)) // 2 === Partitially applied function === def sum(a: Int, b: Int, c: Int) = a + b + c // sum 함수를 대입. 이 때, _의 의미는 sum의 모든 인자를 의미. val a = sum _ println(a(1, 2, 3)) // = a.apply(1, 2, 3) // 아래와 같이 부분적인 인자 적용도 가능. val b = sum(1, _:Int, 3) println(b(2)) // = b.apply(2) === Closure === /* Java의 경우 final(immuitable)로 선언해야만 접근 가능한 것에 비해 var(mutable)도 사용 가능. */ /* Javascript와 동일. */ /* 변수 바인딩 */ var sum = 0 (1 to 10).foreach(sum += _) ;; 바깥 범위(scope)에 있는 sum의 값에 누적. println(sum) // 55 /* 함수 바인딩 */ def scope1(x: Int) = { /*def scope2(y: Int) = x + y scope2 _*/ // 위와 같은 표현 x + (_: Int) } // 각 scope1에 바인딩된 바깥쪽 x를 다르게 설정 val closure1 = scope1(1) val closure2 = scope1(2) println(closure1(10)) // 11 println(closure2(10)) // 12 === Paramter & Argument === /* repeated parameter. 가변 인자(variable paramter)와 동일 개념 */ def test1 (args: String*) = args.foreach(println) test1("Hello", "world", "!") test1(Array("Hello", "world", "!"): _*) /* named argument. 순서 상관없이 인자의 이름으로 전달 */ def test2 (v1: Int, v2: Int) = v1 + v2 println(test2(v2 = 1, v1 = 9)) /* default argument. 인자의 값이 없을 경우 default로 값을 미리 지정 */ def test3 (v1: Int = 1, v2: Int) = v1 + v2 println(test3(v2 = 1)) === Currying === 함수를 1급 객체로 취급하기 때문에 가능. 다중 인수를 갖는 함수를 단일 인수를 갖는 함수들의 함수열로 바꾸는 것. def curriedSum(x: Int)(y: Int) = x + y //def curriedSum(x: Int) = (y: Int) => x + y println(curriedSum(1)(2)) === By-name parameters === 값, 함수 모두 "값"으로 평가하면서 인자를 넘기고 싶은 경우 사용. 이 역시 함수를 1급 객체로 취급하기 때문에 가능. paramter의 Lazy evalution. 성능상의 이점을 가진다. def test1(b: Boolean) = b test1(2 > 1) // by-name parameter. lazy evaluation. def test2_1(b: () => Boolean) = b test2_1(() => 2 > 1) // 개선 def test2_2(b: => Boolean) = b test2_2(2 > 1) == Collections == * https://docs.scala-lang.org/overviews/collections-2.13/overview.html * https://docs.scala-lang.org/overviews/collections-2.13/performance-characteristics.html 기본적으로 불변(Immutable) 상태를 가진다. === Seq === 선형 자료 구조. * 아래 두 관점으로 적합한 자료구조를 선택. * "Indexing"이 필요한가? * "mutable state"가 필요한가? ==== Immutable ==== {{ :language:scala.collection.immutable.seq.png |}} * immutable.LazyList는 immutable.Stream을 대체. immutable.Stream 는 2.13부터 Deprecated. * immutable.ArraySeq 는 2.13부터 추가되었는데, 왠만하면 성능면에서 사실상 상수 시간(Constant time)을 가지는 Vector를 사용하는게 이득. ===== List ===== Singly Linked List. 앞부분에 추가/삭제 유리. 임의 접근은 불리. * ::: : List를 이어 붙여 새로운 List 반환 * :: : Cons(콘즈) 라고 부르며, 새 원소를 기존 List 앞에 삽입한 새로운 List 반환 [새 원소] :: [기존 List] * mutable 상태는 collecdtion.mutable.Buffer val test1 = List(1, 2, 3) val test1 = 1 :: 2 :: 3 :: Nil val test1 = List(1, 2) ::: List(3, 4) ::: List(5, 6) // List(1, 2, 3, 4, 5, 6) val test2 = 1 :: List(2, 3) // List(1, 2, 3) val test3 = List(1, 2) :: List(2, 3) // List(List(1, 2), 2, 3) ===== List vs. Vector ===== [[https://docs.scala-lang.org/overviews/collections-2.13/performance-characteristics.html|전체 Collections 성능 비교]] ^ ^ head ^ tail ^ apply ^ update ^ prepend ^ append ^ | List | C | C | L | L | C | L | | Vector | eC | eC | eC | eC | eC | eC | * C (Constant) : 연산에 상수 시간이 걸린다. (빠름) * eC (Effectively Constant) : 연산에 사실상 상수 시간이 걸리는데, 이는 Vector의 최대 길이나 Hash key 분포와 같은 일부 가정에 따라 달라질 수 있다. * L (Linear) : 연산에 선형 시간이 걸린다. 즉, Collections 크기에 비례하여 시간이 걸린다. **결론적으로 head가 임의 위치 접근이 필요한 경우 Vector 선택.** ==== Mutable ==== {{ :language:scala.collection.mutable.seq.png |}} === Set === * 같은 이름의 mutable, immutable 패키지가 있으니 주의. import scala.collection.mutable val set1 = Set("a", "b") // 원소 추가 // Collection이 val인 경우, mutable package의 import 필요! /// set1.+=("b") 와 같은 표현 set1 += "b" === Map === // immutable이기 때문에 원소를 추가할 수 없음. 이어붙이는 연산을 해야 함. val map1 = Map("A" -> 1, "B" -> 2, "C" -> 3) val map2 = Map("D" -> 4, "E" -> 5, "F" -> 6) val map3 = map1 ++ map2 println(map3) // 기본 구현체는 immutable.HashMap // HashMap(E -> 5, F -> 6, A -> 1, B -> 2, C -> 3, D -> 4) import scala.collection.mutable val map1 = Map[String, Int]() map1 += ("One" -> 1) map1 += ("Two" -> 2) map1("One") val map1 = Map("One" -> 1, "Two", 2) map1("One") ==== TrieMap vs. ConcurrentHashMap ==== * scala.collection.concurrent.TrieMap * java.util.concurrent.ConcurrentHashMap 둘 다 동시성을 보장하는 Thread-Safe Map. * Reference * https://stackoverflow.com/questions/29499381/what-is-a-triemap-and-what-is-its-advantages-disadvantages-compared-to-a-hashmap * https://www.researchgate.net/publication/221643801_Concurrent_Tries_with_Efficient_Non-Blocking_Snapshots === Tuple === 엄격히 말하면 Collections에 포함되지 않음. * Tuple22 까지, 즉 22개의 Argument까지 지원. val tp = (123, "Hello", 1.23) // val tp = Tuple3[Int, String, Double](123, "Hello", 1.23) print(tp._1) print(tp._2) print(tp._3) == Class & Object == === 기초적으로 알아 두어야 할 사항 === * Procedure : Side-effect만을 위해서 실행되는 Method (≒ Return Type이 없는, Unit인 Method) * static Member가 없다. 대신, Singleton Object를 제공한다. * Java는 Field, Method, Type, Package의 4가지 Namespace를 갖지만 Scala는 Field, Method를 2가지 Namespace 갖는다. * Field가 Parameter없는 Method를 Override할 수 있다. (서로 동일시 한다.) * 이 같은 성질로 같은 Class에 같은 이름의 Field와 Method를 정의할 수 없다. * Package도 Field 및 Method처럼 Namespace를 공유하는 이유는 Singleton Object에서 Field와 Method를 import하기 위해서다. === Access Modifier === |<100%>| ^ Modifier ^ Class ^ Companion ^ Subclass ^ Package ^ | public (default) | Y | Y | Y | Y | | protected | Y | Y | Y | N | | private | Y | Y | N | N | * Y : Field/Method 접근 가능 * N : Field/Method 접근 불가 === Class === * Java와 달리 Class 이름과 Class가 작성된 파일 이름이 같을 필요가 없으나 권장. class Person { // member 변수와 Getter/Setter 서술 } ==== Constructor ==== ===== Primary Constructor ===== * 주 생성자만이 Super Class의 생성자 호출 가능! // 아래와 같이 class를 선언하면 member 변수들은 private val // 이후 값을 변경할 수 없음. class Person (name: String, age: Int) { def getName: String = name def getAge : Int = age } // 아래와 같이 class를 선언하면 member 변수들은 public val /// val을 선언함으로써 Getter 생성. ".name", ".age"로 접근. // 바로 멤버 변수에 접근할 수 있고, 이후 값을 변경할 수 없음. // Class 뒤의 인자들을 "Class Parameter"라고 지칭한다. // Scala Compiler는 내부적으로 Class Parameter를 기반으로 Primary Constructor(주 생성자)를 생성한다. /// 기본적으로 Scala의 Constructor는 Class Parameter를 생성하지 않는다. val을 붙이면 생성한다. class Person (val name: String, val age: Int) // Scala Compiler는 // Class 내부에 있으면서 // Field나 Method 정의에 들어 있지 않은 코드를 // Primary Constructor에 삽입한다. /// 객체 생성과 동시에 println 메시지 출력. class Person (name: String, age: Int) { println("Created!" + name + " / " + age) } ===== Auxiliary Constructor ===== * 모든 보조 생성자는 __**반.드.시.** 같은 클래스에 속한 다른 생성자를 호출__하는 코드로 시작해야 한다. 결국 주 생성자를 호출하게 만드는 효과가 있다. * 주 생성자만이 Super Class의 생성자 호출 가능! class Person (val name: String, val age: Int) { def this(age: Int) = this("meteor", age); override def toString: String = name + " / " + age } ==== Checking Preconditions ==== * 유효성 검사 때문에 생성되는 try ~ catch, if ~ else 에서 벗어날 수 있다! * 조건을 만족시키지 못하면 IllegalArgumentException 발생. 메시지가 없으면 "requirement failed" class Person (name: String, age: Int) { require(name != null, "name is not nullable.") require(age > 0) override def toString: String = name + " / " + age } ==== Operator Defination ==== Operator Overloading. class Person (val name: String, val age: Int) { def +(p: Person) = this.toString + "\n" + p.toString override def toString: String = name + " / " + age } object Client extends App { val p1 = new Person("hyuk", 30) val p2 = new Person("meteor", 26) println(p1 + p2) } ==== Method Overloading ===== Java와 동일하나 Scala는 Type을 유추하기 때문에 적합한 Method의 Parameter Type 일치 결과가 없다면 'ambiguous reference' 오류를 출력. 그리고 Overloading보다 [[https://docs.scala-lang.org/tour/default-parameter-values.html|DEFAULT PARAMETER VALUES]]를 이용을 권장. Method 수를 줄일 수 있다. ==== 'apply' Method ==== 'Default Method' 또는 'Injector Method'라고도 불린다. 이는 __Method 이름없이 **괄호**__를 사용하여 호출하는 기능을 갖고 있다. val l = List(1, 2, 3) // 같은 의미 println(s"${l.apply(1)} ${l(1)}") 위 예제의 'List.apply(index)' 처럼 상식적으로 이해할 수 있는, 자연스러운 연산에 적용되어야 한다. === object === * class 대신 **object**로 시작. Java의 static class에 대한 대안. * Instance를 생성할 수 없다. 그러므로 당연히 Constructor도 선언 불가. * "Singleton"으로 작동하므로 첫 접근이 되지 않는 한 인스턴스가 만들어지지 않는다. * 대개 특정 객체에 상관없는 Side-effect Method들(대개 Service 목적의 Util 성질을 가진 공통 사용 Method)을 담아두고 사용하는 Method 사용. // Companion class class Person (val name: String, val age: Int) // Companion object object Person { def print(person: Person) :Unit = println(person.name + " / " + person.age) } object Client extends App { val person: Person = new Person("Amy", 11) Person.print(person) } /* Amy / 11 */ ==== Companion object ==== 어떤 class **이름**이 같은 object. 그 피대상인 class는 "Companion class"라고 한다. Companion object와 [[language:scala#'apply' Method|'apply' Method]]를 이용하여 Companion class의 [[design_pattern:factory_method_pattern|Factory Pattern]] 을 적용할 때 사용. 이 때, object의 field들이 "private"으로 선언되어 있더라도 접근 가능. Companion class와 Companion object는 **__반드시__ 같은 소스 파일**에 있어야 한다! object DBConnection { private val url = "jdbc://localhost" private val user = "franken" private val password = "berry" def apply() = new DBConnection() } class DBConnection { private val props = Map( "url" -> DBConnection.url, "user" -> DBConnection.user, "password" -> DBConnection.password ) } // val connection = DBConnection() === case class === * 자동으로 유용한 Method들 생성. (Kotlin의 'data class'와 동일한 개념) * Data를 관리하는데 유용. 즉, DTO로 유용. * 자동으로 [[language:scala#companion object|Companion Object]] 생성. "new" 필요하지 않음. * 기본적으로 생성자의 Parameter들은 'val'. * Compile하면 '**[소스이름].class(class)**', '**[소스이름]$.class(object)**'이 생성된다. * 자동으로 생성되는 Methods ^ 이름 ^ 위치 ^ 설명 ^ | apply | object |case class를 Instance로 만드는 Factory method | | unapply | object |Instance의 field들을 Tuple로 추출하여 **[[language:scala#match|Pattern Matching]]**에 case class instance를 사용할 수 있도록 함 | | copy | class | **Deep copy!** | | equals | class | Reference가 아닌 구조적 동등성(모든 필드가 일치)으로 비교. '=='로도 호출 할 수 있다. | | hashCode | class | Instance의 field들의 Hash code 반환. hash 기반 collections에서 동등성에 이용. | | toString | class | Field들을 String으로 전환 | object Practice extends App { val person = Person("Luke", 33) val copy = person.copy() // Equals? println(person == copy) person.name = "James" person.age = 11 // Deep copy? println(copy) // Pattern Matching val p = person match { case Person(_, 11) => "Original!" case Person(_, 33) => "Copy!" } println(p) } case class Person(var name: String, var age: Int) * Reference * https://docs.scala-lang.org/overviews/scala-book/case-classes.html * https://www.oreilly.com/library/view/scala-cookbook/9781449340292/ch04s15.html === Composition and Inheritance ==== ==== Override ==== Java의 경우 1.5 기준으로 @Override를 명시할 수 있으나 필수사항이 아닌데 반면, Scala는 강제한다. 이로 인해 "우연한 Override"로 "fragile base class"가 생성되는 문제를 줄여준다. class Person (val name: String, val age: Int) { override def toString: String = name + " / " + age } ==== Abstract Class ==== Java와 비교하여 * method에 abstract를 붙이면 안된다. * abstract는 class 혹은 abstract class를 mix-in한 trait의 method만 붙일 수 있다. * 본체가 없으면 abstract method로 인식한다. (이는 trait도 동일) * 본체가 있는 concrete method를 구현할 수 있다. * **Field가 파라미터 없는 메소드를 Override 할 수 있다.** abstract class Abstract { /* abstract가 필요하지 않다. */ def values: Array[Int] def sum = values.sum } class Detail(conts: Array[Int]) extends Abstract { /* 아래 동작은 같다. */ //override def values: Array[Integer] = conts val values: Array[Int] = conts } ==== Parameter field 정의 ==== 같은 이름의 생성자 Paramter와 Field를 동시 정의하는 단축 표기. 특정 Field에 할당하는 이름 중복을 피하기 위한 Boilerplace 제거. abstract class Abstract { def values: Array[Int] } // 아래와 같이 같은 목적이지만 중복된 이름을 피하기 위해 불필요한 부분이 작성된다. // 할당과 동시에 바로 getter 생성 class Detail(vals: Array[Int]) extends Abstract { override def values: Array[Int] = vals } // 이를 아래와 같이 작성할 수 있음. class Detail2(var values: Array[Int]) extends Abstract ==== Super class의 Constuctor 호출 ==== Java의 경우 상속을 받으면 반드시 부모 Class의 생성자와 같은 형태의 생성자를 호출하고 super()를 하는 과정을 아래와 같이 간략화할 수 있다. abstract class Abstract { def values: Array[Int] def sum = values.sum } class Detail(val values: Array[Int]) extends Abstract /* class Detail(vals: Array[Int]) extends Abstract { override def values: Array[Int] = vals } */ /* Super class(Detail)의 생성자 호출. */ class MoreDetail(x: Int) extends Detail(Array(x)) { } = Trait = Java의 Interface의 역할을 하는 한 단위. * 두 가지를 제외하고 Class가 할 수 있는 일을 다 할 수 있다. (Field 정의, 본체가 있는 concrete method 구현등등) * 할 수 없는 두 가지 : 생성자 Parameter 구현, super를 이용한 Method 정적 바인딩 * Mix-in 대상은 Trait뿐만이 아니라 Class, Abstract Class 모두 다 된다. * 첫 번째 상속/구성할 대상이 Class든 Trait든 무조건 extends를 사용한다. 단, Class와 Trait를 혼합사용(Mix-in)할 경우 Class를 먼저 기술한다. 이후 Trait를 with를 이용하여 Mix-in 한다. * 즉, Java의 Interface와 달리 Class도 상속이 가능하다! 이는 해당 부모 Class의 Method를 Override가 필요한 상황에서 사용한다. object Client extends App { val x:D = new D x.func // "안녕, 세상아!" } trait T1 { def func(): Unit } trait T2 { private val greeting = "Hello, world!" def func() = println(greeting) } class C1 { } class D extends C1 with T1 with T2 { override def func(): Unit = println("안녕, 세상아!") } == Stackable Modifications == object Client extends App { val queue = new BasicIntQueue with Doubling queue.put(10) println(queue.get()) // 20 } abstract class IntQueue { def get(): Int def put(x: Int) } class BasicIntQueue extends IntQueue { private val buf = new ArrayBuffer[Int] override def get(): Int = buf.remove(0) override def put(x: Int) = buf += x } trait Doubling extends IntQueue { // trait에서 super로 접근할 때는 override abstract가 필요. override abstract def put(x: Int): Unit = super.put(x * 2) } === 여러 Trait를 mix-in 했을 때 우선 순위 === 가장 오른쪽에 있는 Trait의 효과부터 적용된다. trait Base { override def toString: String = "Base" } trait A extends Base { override def toString: String = s"A->${super.toString}" } trait B extends Base { override def toString: String = s"B->${super.toString}" } trait C extends Base { override def toString: String = s"C->${super.toString}" } class MixIn extends A with B with C { override def toString: String = s"MixIn->${super.toString}" } // println(new MixIn()) /// MixIn->C->B->A->Base = Type parameterization = == Bounded Type == Type Parameter를 특정 Class나 거의 Sub type 또는 Base type으로 **제한**하는 방법. === Upper bound === Type을 Base type 또는 Sub type 중 하나로 제한한다. https://docs.scala-lang.org/tour/upper-type-bounds.html <: object Practice extends App { def check[A <: Parent](u: A): Unit = { println(s"${u.name}") } // Runtime Error // inferred type arguments [GrandParent] do not conform to method check's type parameter bounds [A <: Parent] check(new GrandParent("Jake")) check(new Parent("Fred")) check(new Child("Mike")) } class GrandParent(val name: String) class Parent(name: String) extends GrandParent(name) class Child(name: String) extends Parent(name) === Lower bound === Type을 해당 Type으로 제한하거나 또는 해당 Type이 확장되는 Base type 중 하나로 제한한다. >: == Type variance == Type parameter를 **덜 제한적**으로 만드는 방법. 타입 가변성(Type variance)은 Type parameter가 Base type이나 Sub type을 충족하도록 적응하는 방법을 지정한다. |<100%>| ^ Scala ^ Java ^ 설명 ^ 예시 ^ | +T | ? extends T | Covariance (공변성) | **X[Tsub]**는 **X[T]**의 Sub type | | -T | ? super T | Contravariance (반공변성) | **X[Tsuper]**는 **X[T]**의 Sub type | | T | T | Invariance (무공변성) | **X[T]**는 **X[T]**. X[Tsub] 혹은 X[Tsuper]가 X[T]를 대체할 수 없음. | * Effective Java에서 나오는 [[https://www.oracle.com/technetwork/server-storage/ts-5217-159242.pdf|PECS (Producer extends, Consumer super)]] 원리와 같은 개념. * Producer는 getter, Consumer는 setter * Scala의 [[https://www.scala-lang.org/api/2.12.1/scala/Function1.html|Function1]] 혹은 Java의 [[https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html|Function]]을 보면 쉽게 유추 가능. * 첫번째 인자 **-T1**는 Producer, 두번째 인자 **+R**는 반환값으로 Consumer. * 반환해야 하는(생산하는) 값이 Super type인데, 인자로 받은(제공하는) 값이 Sub type이면 Super type의 Method를 호출할 수 없음. * 이런 이유로 T1은 동등한 레벨의 타입이거나 상위 타입(Super type)이어야 한다. * "상위 추상화 타입 -> 하위 구체화 타입" // https://github.com/deanwampler/programming-scala-book-code-examples/blob/master/src/main/scala/progscala3/objectsystem/variance/FunctionVariance.scala class CSuper { def msuper() = println("CSuper") } class C extends CSuper { def m() = println("C") } class CSub extends C { def msub() = println("CSub") } object AbsTypes extends App { // 추상적인 것 -> 구체적인 것 val f1: C => C = (c: C) => new C val f2: C => C = (c: CSuper) => new CSub val f3: C => C = (c: CSuper) => new C val f4: C => C = (c: C) => new CSub // Error! /** 인자로 C를 받도록 타입을 정의했는데, C를 상속받은 CSub를 인자로 받는다면 * C가 알지못하는 Method가 호출될 수 있다! */ val f5: C => C = (c: CSub) => new CSuper } = Abstract types = https://docs.scala-lang.org/tour/abstract-type-members.html import java.io._ import scala.io.Source // https://github.com/deanwampler/programming-scala-book-code-examples/blob/master/src/main/scala/progscala3/typelessdomore/AbstractTypes.scala // 위 소스를 응용한 예제 abstract class BulkReader { type In // 구체적인 타입을 지정하지 않음. val source: In // 바로 위에서 지정한 Type인 "In"을 사용 def read: String } class StringBulkReader (val source: String) // 추상 타입의 실질 타입 정의 extends BulkReader { type In = String // 추상 타입 구체화 override def read: String = source } class FileBulkReader (val source: File) // 추상 타입의 실질 타입 정의 extends BulkReader { type In = File // 추상 타입 구체화 override def read: String = { val source1 = Source.fromFile(source) try source1.mkString finally source1.close() } } == Parameterized types vs. Abstract types == 어떤 상황에서 무엇이 적절한가? * Parameterized types : Type parameter가 Parameterized types와 관련이 없는 경우 * List[A]에서 A는 String, Int 무엇이든 상관 없음. * Abstract types : 타입 멤버가 객체의 **동작**과 일치하는 경우 * 위 예시를 보면 "읽는다"라는 동작이 일치한다. = Implicit Parameters and Conversions = == Implicit Parameters == Currying 혹은 Lazy 된 함수의 인자를 명시적으로 지정하지 않아도 자신의 네임스페이스의 기본값을 사용하는 방법. 변수나 함수 매개변수에 "implicit"를 앞에 붙인다. "Dependency Injection"의 시각으로 볼 수 있다. object Printer { def print(num: Double)(implicit format: String) = println(format.format(num)) } // fmt가 주입된다. object Practice extends App { implicit val fmt = "%.7f" Printer.print(1.11) } == Implicit Class == 암시적 Type 변환. 어느 Instance의 Type이 특정 Instance의 Type인 것처럼 자동 전환하여 그 특정 Instance의 Method를 가진 것처럼 보이게 할 수 있다. C#, Kotlin의 Extension Methods과 비슷한 기능. 서로를 고려하지 않고 독립적으로 개발된 소프트웨어/라이브러리를 하나로 묶으려고 할 때 사용. * 생성 규칙 * object/class/trait 안에 정의되어야 한다. * Implicit를 선언하지 않는 인자를 받아야 한다. * 같은 namespace에서 object/class/trait 이름이 같아서는 안된다. * case Class는 사용할 수 없다. (자동으로 생성되는 Companion Object 때문) object Client extends App { val uc = new UnaryCalculator(2) val op1 = uc ** 3 //val op1 = uc.**(3) val op2 = 3 ** uc // Error! //val op2 = {3}.**(uc), 정수 3은 ** Method를 가지고 있지 않음. println(op1 + " / " + op2) } class UnaryCalculator (val v: Int) { def ** (`pow`: Int) = math.pow(v, `pow`) } object Client extends App { implicit def intToUnaryCalculator (initVal: Int) = new UnaryCalculator(initVal) val uc = new UnaryCalculator(2) val op1 = uc ** 3 //val op = uc.**(3) val op2 = 3 ** uc //val op = {3}.**(uc) println(op1 + " / " + op2) // 8.0 / 9.0 } class UnaryCalculator (val v: Int) { def ** (`pow`: UnaryCalculator) = math.pow(v, `pow`.v) }