Programming Language/Theory

[PLT/프로그래밍언어론] 12. Scala - Functional Programming Language

lumana 2024. 12. 22. 01:37

 

12. functional2

#PLT


함수형 프로그래밍 (Functional Programming)


프로그래밍 언어론 (Programming Language Theory)


1. 주제 (Topics)

  • 함수형 언어 기초 (Functional Language Fundamentals)
  • 패러다임 개요 (Paradigm Overview)
  • 표현식 평가 (Expression Evaluation)
  • Scala에서의 함수형 언어 주제 (Functional Language Topics in Scala)
  • 블록과 스코프, 패턴 매칭, 커링 (Blocks and Scope, Pattern Matching, Currying)
  • λ-calculus (λ-Calculus)

2. 함수형 언어 (Functional Language)

  • Scala에서의 주제 (Topics in Scala)

3. Scala 구문 사용 (Using Scala Syntax)

  • 이 강의의 개념 대부분은 다른 함수형 언어에서도 유효합니다.
    • 그러나 사용 방법은 언어에 따라 다릅니다.
  • 따라서 Scala로 작성된 코드와 함께 설명을 드리겠습니다.
  • 시작하기 전에, Scala의 기초를 이해하기 위해 지난 주 실습을 익히는 것을 강력히 권장합니다.



Scala Basic

1. 변수 정의

Scala에서 변수를 정의하는 방법에는 2개가 있다. 바로 val 키워드와 var 키워드이다. Scala는 타입 추론이 가능하므로 변수를 선언할 때 타입을 명시하지 않아도 된다.


val msg:String = "Hello World!"
val msg2 = "Hello World!

타입을 반드시 명시해야 하는 일부 케이스를 제외하면 타입 추론을 대부분 지원한다.


키워드

  • val : value의 준말이다. val은 자바의 final 변수와 유사하다. 초기화 하고 재할당할 수 없다. 불변이다.
  • var : variable의 준말이다. 변수에 해당하는 개념이다. 재할당이 가능하다.
  • 함수형 프로그래밍은 상태를 갖지 않는 것을 지향하기 때문에 var보단 val 사용을 권장한다.

2. 함수 정의


함수의 정의는 def로 시작한다.
그 후, 함수명이 온다.
그리고 그 뒤의 괄호에는 파라미터들이 콤마(,)로 구분되어 들어온다.
스칼라가 함수의 파라미터 타입을 추론하지 않기 때문에, 함수의 파라미터는 반드시 타입 지정을 해야한다.
그리고, 결과 타입을 지정해야 한다.
결과 타입 뒤에는 등호와 중괄호가 오고, 중괄호 안에는 함수의 본문이 들어간다.


def max(a: Int, b:Int): Int = {
 if(a>b) a
    else b
}

3. 컬렉션

  • 컬렉션은 문자 그대로 값들의 모음입니다.
  • 컬렉션의 여러 인기 있는 데이터 구조가 있습니다.
    • Tuple, List, Set, Map 등
  • 몇 가지 고유한 특징이 존재하지만, 공통된 기능을 공유합니다.
  • 이번 실습에서는 Tuple, List, Map만 다룰 것입니다.
    • 나머지는 기본을 알면 쉽게 응용할 수 있습니다.

4. 튜플 (Tuple)

val x = (1, "a")
val f = (x: Int) => x + 1
val g = (x: Int) => x + 2
val y = (f, g)

println(x._1, x._2) // 1, "a"
println(y._1(2), y._2(2)) // 3, 4

// Pattern Matching
val (add1, add2) = y // add1 = f, add2 = g
println(add1(1), add2(1)) // 2, 3

  • 여러 값을 하나로 결합합니다.
  • 동일한 타입일 필요가 없습니다.
    • 예: ("A", 2, f), f: Int => Int
  • _<인덱스>를 사용하여 접근합니다. 인덱스는 1부터 시작합니다.
  • 구조화된 데이터 타입(정형 데이터)을 사용하고 싶지만 정의하고 싶지 않을 때 매우 유용합니다.
    • ex) SQL create문 안쓰고 바로 빈 테이블에 Insert 날리는 느낌
  • Python을 알고 있다면 매우 유사합니다.

5. 리스트 (List)

// Creating List 
val list1 = List[Int]() 
val list2 = Nil 
val list3 = List(1, 5, 3) 
val list4 = (4 :: list1) :+ 2
val list = list3 ::: list4
  • 리스트는 값의 시퀀스를 나타내며, 값들의 순서가 존재함을 의미합니다.
  • 예제와 같은 다양한 방법으로 리스트를 생성할 수 있습니다.
  • '::' (헤드에 추가)':+' (테일에 추가)를 사용하여 요소를 추가할 수 있습니다.
  • ':::' 또는 '++'를 사용하여 두 리스트를 연결할 수 있습니다.

6. 요소 접근

val lst = List(2, 4) ++ List(1, 3)

println(lst)
println(lst(2))
println(lst.head)
println(lst.last)
// Output:
// List(2, 4, 1, 3)
// 1
// 2
// 3
  • 인덱스를 사용하여 리스트의 요소에 접근할 수 있습니다.
    • 리스트이름(인덱스)
    • 예: lst(2)lst의 3번째 요소, 인덱스는 0부터 시작합니다.
      • 튜플은 인덱스가 1부터 시작했다. 헷갈림 주의!
  • head, last를 사용하여 첫 번째와 마지막 요소에 접근할 수 있습니다.

7. 정렬

// Simple method
println(lst.sorted) 
// Output: List(1, 2, 3, 4)

// sortWith
val names = List("A. Turing", "A. Church", "H. Curry", "J. von Neumann")
println(names.sortWith(_.length < _.length))
// Output: List(H. Curry, A. Turing, A. Church, J. von Neumann)

// sortBy
def last(full: String): String = {
  full.substring(full.indexOf(' ') + 1)
}
println(names.sortBy(last))
println(names.sortWith(last(_) < last(_)))
// Output:
// List(A. Church, H. Curry, A. Turing, J. von Neumann)
// List(A. Church, H. Curry, A. Turing, J. von Neumann)
  • 다양한 방법을 사용하여 리스트의 요소를 정렬할 수 있습니다.
  • sorted: 현재 리스트를 정렬합니다.
  • sortWith: 요소의 특정 값을 사용하여 정렬합니다.
    • 예: 문자열 길이로 정렬
  • sortBy: 함수를 적용하고 그 결과를 사용하여 정렬합니다.
    • 예: 성(last name)으로 정렬

8. 일반적인 연산

// find
val list = List(1, 2, 3, 4)
println(list.find(_ == 2)) // Some(2)
println(list.find(_ == 5)) // None
  • find: 요소를 찾습니다.
    • 예: list.find(_ == 2) ➞ 2를 찾습니다.
    • 반환값은 Some(x) 또는 None입니다 ➞ 이는 나중에 다룰 예정입니다.

// map
println(List(1, 2, 3).map(_ + 2)) // List(3, 4, 5)
println(List(1, 2, 3).map(n => 2 * n + 1)) // List(3, 5, 7)

// Odd, Even, Odd
println(List(1, 2, 3).map(
  n => if (n % 2 == 0) "Even" else "Odd"
)) // List(Odd, Even, Odd)

  • map: 각 요소에 대해 expression을 적용합니다. 함수도 각 요소에 매핑할 수 있습니다.

// filter
println(List(1, 2, 3).filter(_ % 2 == 0)) 
// Output: List(2)

  • filter: 조건을 만족하는 요소만 필터링합니다.
    • 반환값은 여전히 하나의 요소를 가진 리스트입니다.

9. 폴딩 (Folding)

// fold
val x = List(1, 2, 3).fold(0)((acc, i) => acc + i)
val y = List(1, 2, 3).foldLeft("L") {
  (acc, i) => { println(acc); acc + i }
}
val z = List(1, 2, 3).foldRight("R") {
  (i, acc) => { println(acc); i + acc }
}
println(x, y, z)
println(List(1, 2, 3).sum)
// Output:
// L
// L1
// L12
// R
// 3R
// 23R
// (6, L123, 123R)
// 6

  • 리스트를 하나의 값으로 "폴딩"하고 싶을 때는 어떻게 할까요?
    • 예: 리스트의 합을 계산하고 싶을 때
  • fold(z: A)(op: (A, A) => A): A
    • z는 시작 값입니다.
    • op는 이항 연산자입니다.
    • A는 리스트의 타입입니다.
    • 타입 추론이 가능합니다.
      • 초깃값 z를 보고 타입추론이 되는 듯?
    • acc : accumulate
      • fold 함수 내부 로직에 의해 (acc, i) => acc + i 의 값이 acc에 자동으로 할당된다.
  • foldLeft()foldRight()에서 결합 방식이 어떻게 다른지 확인하세요.
    • foldLeft(): 초깃값이 제일 왼쪽에 위치하고, 그 후 모든 원소들이 순서대로 오른쪽에 붙는다.
      • tail-recursive로 구현되어 있음.
    • foldRight(): 초깃값이 제일 오른쪽에 위치하고, 그 후 원소들이 역순으로 초기값의 왼쪽에 붙는다.
  • .sum() 메서드를 통해 모든 원소를 하나의 값으로 폴딩할 수 있다.
  • fold()는 시작할 때 zop의 첫 번째 인수로 전달합니다.

List(1,2,3).fold(0)((acc, i) => acc + i)
// ➞ (0, 1) => acc + i // 첫 번째 요소 1.
// ➞ (1, 2) => acc + i // 두 번째 요소 2.
// ➞ (3, 3) => acc + i // 세 번째 요소 3.
// ➞ 6
  • 이는 실제로
List(1,2,3).sum

과 동일합니다.


11. 조건문과 재귀 (Conditional and Recursion)

// Recursive sum with @tailrec
def sum(n: Int): Int = {
  @tailrec
  def sum(n: Int, acc: Int): Int = {
    if (n == 0) acc
    else sum(n - 1, acc + n)
  }
  sum(n, 0)
}
  • 예제는 1부터 n까지의 합을 계산하는 sum(n)의 꼬리 재귀 버전입니다.
  • Java와 유사하게 if - else를 사용할 수 있습니다.
  • 편의를 위해 중첩된 서브 함수를 정의할 수도 있습니다.
    • sum(n, 0) 대신 단순히 sum(n)을 호출할 수 있습니다.
  • Scala에서는 @tailrec 애노테이션을 사용할 수 있습니다.
    • 정의한 함수가 실제로 꼬리 재귀가 아니면 경고가 발생합니다.

예시: 피보나치 수


4. Scala의 블록 (Blocks in Scala)

  • 블록은 명령형 언어에서 여러 문을 하나의 단위로 결합하는 복합 문(statement)입니다.
  • 더 중요한 것은, 블록은 환경의 단위이기도 하여 프로그램이 블록에 진입할 때마다 새로운 환경을 고려할 수 있습니다.
  • Scala에서 블록은 어떻게 다를까요?

5. Scala의 블록 (Blocks in Scala)

val t = 0
def f(x: Int): Int = t + g(x)
def g(x: Int): Int = x * x;

{
  val t = 5
  val s = f(2) // t + g(2) -> t + 2 * 2
  println(s - t) // t + 2 * 2 - t, which t?
}

val s = f(2)
println(s - t) // How about now?

결과

-1
4
  • 블록은 Scala에서 표현식(Expression)입니다.
  • 블록 내의 정의는 블록 내에서만 접근 가능합니다.
  • 지역 정의는 블록 외부의 같은 이름을 가립니다 (Shadowing).
  • 함수는 정의된 환경에서 평가됩니다 == 정적 스코프 (Static Scope), 호출되는 위치가 아님) .

6. 블록 내의 서브 함수 (Sub-function in Block)

// Factorial with @tailrec
def fact(n: Int): Int = {
  @tailrec
  def fact(n: Int, acc: Int): Int = {
    if (n == 0) acc
    else fact(n - 1, acc * n)
  }
  fact(n, 1)
}

println(fact(3))
  • sub-funcion이 필요할 경우, 블록 내에 넣을 수 있습니다.
  • 보다 깔끔하고 정돈된 코드를 작성할 수 있습니다.
  • 지역 함수 fact(Int, Int)fact(Int)에 의해서만 접근 가능합니다.
  • 일반적인 팩토리얼 함수와 같은 인터페이스를 제공하면서 implementation을 내부에 숨길 수 있습니다.

7. 패턴 매칭 (Pattern Matching)

// Factorial with pattern matching
def fact_p(n: Int): Int = {
  n match {
    case 1 => 1
    case _ => n * fact_p(n - 1)
  }
}

println(fact_p(3))
  • 현재 우리는 if-else를 사용하여 분기를 처리하고 있습니다.
  • 패턴 매칭을 사용하면 더 나은 방법이 있을까요?
    e match {
      case P1 => e1
      case P2 => e2
      ...
      case Pn => en
      case _ => e0
    }
    


8. 패턴 매칭 (Pattern Matching)

// Wrong order in pattern matching leading to StackOverflow
def fact_p(n: Int): Int = {
  n match {
    case _ => n * fact_p(n - 1)
    case 1 => 1
  }
}

println(fact_p(3)) // StackOverflow!!
  • Pattern Matching은 first case부터 시작하여 일치할 때 까지 계속됩니다.
  • match되는 경우, 표현식이 매칭되고 매칭 과정은 더 이상 계속되지 않습니다.
    • 이는 imperative의 일반적인 switch-case 동작과는 반대입니다.
  • default case_ 사용에 주의하세요.
    • 위 예시에서는 모든 case가 default case에 매칭되어 StackOverflow!! 발생

9. 패턴 매칭 (Pattern Matching)

// Pattern matching with multiple cases
def read(x: Any): String = {
  val nums = List(2, 5, 7)
  x match {
    case 1 => "One"
    case "2" => "Two"
    case n: Int if !nums.contains(n) => "Not on the list!"
    case n: Int => "Number"
    case s: String => "String"
  }
}

println(read(1))    // One
println(read(2))    // Number
println(read(3))    // Not on the list!
println(read("3"))  // String
  • Scala는 switch-case 문에 비해 더 general한 패턴 매칭을 제공합니다.
  • 특정 값이나 심지어 타입을 매칭할 수 있습니다.
  • 조건을 추가하기 위해 if <boolean expr.>을 추가할 수 있습니다.
    • 조건이 만족될 때만 케이스가 매칭됩니다.

10. 지연 평가 (Lazy Evaluation)

// Lazy evaluation
def f(c: Boolean, n: => Int): Int = {
  lazy val x = n
  if (c) 0
  else x * x
}

val m = f(true, { println("True"); 2 })
val n = f(false, { println("False"); 2 })
println((m, n))
// Output:
// False
// (0, 4)

m을 호출할 때는 x가 사용되지 않아 evaluate 되지 않으므로 println(“True”)가 호출되지 않는다.
반대로 n을 호출할 때는 x * x에서 x가 처음 사용되어 x = n이 evaluate된다. println(“False”) 호출


lazy 키워드 사용.

lazy val x = e

  • Expression은 처음 사용될 때 evaluate되고, 그 후에는 이름이 binding됩니다.
    • 예제에서 xcfalse가 될 때까지 사용되지 않습니다.
      • if (true) 0으로 반환되서 x가 evaluate 자체가 안됨.

n의 경우, =>를 사용하여 by-name paramenter로 만듭니다.

  • by-name parameter는 함수를 매개변수로 보내고 해당 함수를 lazy evaluation을 할 수 있는 기법이다.
  • 더 쉽게 말하면 매개변수의 계산을 지연하고 싶어할 때 사용할 수 있다.
  • 평가가 발생하는지 확인하기 위해 println()이 추가되었습니다.

참고 : by-value parameter : 인자로 전달된 값이 한 번 평가된 후 함수에 전달된다.


by-name vs lazy val

  • by-named의 경우 함수 호출 시 전달된 expression이 매 번 다시 평가된다
  • lazy val의 경우 첫 번째 참조 시 한 번만 평가되고, 이후에는 캐시된 값을 사용한다.
    • name에 값이 binding이 된다.

object ByNameLazy extends App {
  def byName(x: => Int): Int = x + x
  lazy val lazyValue = { println("Evaluating lazyValue"); 10 }

  println(byName(lazyValue))
  println(lazyValue) // Already evaluated -> Result: 10
}

결과

Evaluating lazyValue
20
10

println(byName(lazyValue))에서 이미 lazyValue에 binding이 되었으므로
println(lazyValue)에서는 println("Evaluating lazyValue”)이 호출되지 않는다.


11. 고차 함수 (Higher-Order Functions)

// High-order function example
def name(n: String, f: String => String): String = {
  "My name is " + f(n)
}

def first(full: String): String = {
  full.substring(0, full.indexOf(' '))
}

def last(full: String): String = {
  full.substring(full.lastIndexOf(' ') + 1)
}

println(name("Jindae Kim", first))
println(name("Jindae Kim", last))
// Output:
// My name is Jindae
// My name is Kim
  • Higher-Order Functions: 다른 함수를 매개변수로 사용하거나 반환 값으로 사용하는 함수.
  • Scala에서는 함수 타입을 =>를 사용하여 표현할 수 있습니다.

예:
f: String => String

  • 함수는 String 매개변수를 받아 String으로 평가됩니다.
  • Code Reusability을 높일 수 있습니다.

12. 장점 (What's good about it?)

  • 함수를 Runtime에 combine하여 프로그램이 dynamically하게 동작하도록 변경할 수 있습니다.
  • first-order function은 코드를 작성하면 그 동작이 pre-defined되어 다른 동작을 할 수 없습니다.
  • 그러나 Higher-order function를 사용하면 다른 함수를 전달하여 behaviour을 변경할 수 있습니다.

13. 익명 함수 (Anonymous Functions)


// Simplified name function examples
def name(n: String, f: String => String): String = {
  "My name is " + f(n)
}

println(name("Jindae",
  (n: String) => n.substring(0, 3)
))
println(name("Jindae",
  n => n.toLowerCase()
))
// Output:
// My name is Jin
// My name is jindae
  • 함수의 이름을 생략할 수도 있습니다.
    (p1: T1, ..., pn: Tn) => e
    (p1, p2, ..., pn) => e
    
  • 이러한 함수는 익명 함수 (Anonymous Function)라고 불립니다.

14. 함수 결합 (Combine Functions)

 


직접 name(n, first)를 호출하지 말고, 미리 name(n, first)을 호출하는 firstName 함수를 만들었다.
이런식으로 lastName 함수도 만들었다. 이렇게 함으로써 호출할 때 파라미터에 함수를 전달하지 않아도 된다. 하지만,


  • 다른 함수를 결합하여 새 함수를 정의할 수 있습니다.

Combine name() + first() to define firstName()


  • 그런 다음 firstName()을 사용할 수 있습니다.
  • 런타임에 함수를 생성합니다.
  • 매개변수는 어떨까요?
    • 너무 장황합니다.
    • firstName을 호출할 때 n을 전달하고, firstName에서 name을 호출할 때 또 n을 전달한다.
    • n은 너무 obvious한 파라미터다.

15. 커링 (Currying)


매개변수를 원래는 두 개를 전달 했지만, 이번에는 첫 번째 매개변수인 String => String 함수만 전달한다.
예제에서는 name(first)를 호출했다. 그러면 first에 해당하는 함수를 호출하는 nameP 함수를 반환한다. 이 반환된 nameP 함수에 파라미터로 (“Jindae Kim”)이 전달되고, 최종적으론 nameP에서 first 함수에 “Jindae Kim”을 전달하여 호출한다.


이렇게 하면 장점이 뭐냐?


fName은 name(First)의 결과로 함수를 받을 수 있기 때문에, fName(“Jindae Kim”) 이렇게 호출할 수 있다.


  • (T1, T2, ..., Tn) => Tr 타입의 함수는 T1 => (T2 => ...(Tn => Tr)...) 타입의 함수로 변환될 수 있습니다.
  • multiple parameters를 받는 함수를 하나의 매개변수를 받는 함수들의 sequence로 변환하는 것입니다.
    • 이러한 conversion을 Currying이라고 한다.
  • PL 이론에서 single parameter 함수에 대한 solution을 제공하면 충분합니다.

16. 익명 함수와 커링 (Currying with Anonymous Function)


deposit(0.154)(0.03)(5)(1000)을 보면
deposit(0.154)을 호출하고, t 대신 0.154가 들어간 deposit 함수[(r: double) => (n: int) => (a: int) => double]가 반환된다. 0.154를 고정시키는 거다.
그 다음에는 r = 0.03이 들어간 deposit 함수[(n: int) => (a: int) => double] 가 반환된다.
이런식으로 동작하여 최종적으로 1000이 전달되면 모든 매개변수가 고정되어 최종 계산이 수행된다.


savings1은 t = 0.154, r = 0.03을 고정시켜놓고, 인자로 나머지 n과 a를 받아 deposit 함수를 차례로 호출
savings2는 t = 0.154, r = 0.04로 고정하고, 특정 파라미터를 비워둔다. _는 n과 a가 나중에 입력됨을 말해준다.


  • anonymous function을 사용하여 currying을 할 수 있습니다.
  • 언더스코어 _를 사용하여 parameter name을 무시할 수 있습니다.
    • 나머지 파라미터들이 original function에 전달된다~
  • 함수를 보다 쉽게 Specialize할 수 있습니다.
  • 인수는 한 번에 evaluate될 필요가 없습니다.


Scala - Adv

리듀스 (Reduce)

  • 초기 값을 제공하는 것이 때때로 번거로울 수 있습니다.
  • 컬렉션을 단순히 하나의 값으로 “Reduce”하고 싶다면 어떻게 할까요?
  • reduce(op: (A, A) => A): A
    • 여전히 이항 연산자를 인수로 받는다는 점에 유의하세요.
    • 첫 두 요소에 op을 적용한 다음, 결과에 세 번째 요소를 적용하는 식으로 진행됩니다...
  • reduceLeft, reduceRight도 사용할 수 있습니다.
  • 이 리듀스는 max와 동일하게 동작합니다.
    • a max b는 더 큰 값을 취합니다.

맵 (Map)

  • Map은 key-value pair을 포함합니다.
    • Key는 고유하며 value는 그렇지 않습니다.
  • 인덱스 대신 키를 사용하여 해당 값을 접근할 수 있습니다.
  • 두 가지 타입이 필요합니다: Map[Key, Value].
  • ->를 사용하여 키를 값에 매핑할 수 있습니다.
  • keys, values를 사용하여 모든 키와 값을 얻을 수 있습니다.

맵 연산 (Map Operations)

  • +를 사용하여 요소를 추가하거나, ++를 사용하여 두 맵을 결합할 수 있습니다.
  • find, map, filter 메서드를 리스트와 유사하게 사용할 수 있습니다.
    • key를 index 1로, value를 index 2로 사용하자.
  • 맵의 element를 다룰 때는 key-value pairs를 처리해야 합니다.
    • 예: 리스트의 i 대신 맵에서는 (k, v).

폴딩 (Folding)


초기값: (" ", 0)

  • 튜플 형태로 초기값이 제공됩니다.
    • acc._1: 문자열로 누적.
    • acc._2: 숫자 값으로 누적.

연산 과정:

  • map의 각 요소가 (key, value) 형태의 튜플로 제공됩니다.
  • e._2는 value(즉, 키-값 쌍에서 값 부분)를 의미합니다.
    • e는 (key, value)의 tuple라서 value를 e._2로 접근한다.
  • 축약 과정:
    • acc._1: 문자열에 e._2 값을 계속 추가합니다.
    • acc._2: 누적 합계를 계산합니다.

  • 맵은 elements를 tuple로 제공하므로 주의해야 합니다.
  • foldLeftfoldRight에서는 key 타입이나 value 타입 중 하나의 값을 사용할 수 있습니다.
    • 예: 문자열을 사용하는 예제이지만, 합계를 계산하려면 0으로 시작할 수도 있습니다.
  • reduce를 유사하게 사용할 수도 있습니다.

옵션 (Option)

  • OptionSome 또는 None입니다.
  • 값이 존재하는지 여부를 나타내므로 안전하게 값을 사용할 수 있습니다.
    • java의 Optional을 생각하면 좋을듯
  • getOrElse 또는 패턴 매칭을 사용하여 Option을 처리할 수 있습니다.
    • getOrElse(x): 값을 가져오거나, 그렇지 않으면 x를 반환합니다.
  • s"..."는 string interpolation(문자열 보간법)입니다.
    • $name 또는 ${expr}을 사용할 수 있습니다.

모두 만족 / 각 요소에 적용 (forall / foreach)

  • forall(p): 모든 요소가 조건 p를 만족하는지 확인합니다.
  • foreach(f): 각 요소에 함수 f를 적용합니다 ➞ 부작용을 발생시킵니다.
    • foreach()map()의 차이를 생각해보세요.

map

  • 컬렉션의 각 요소에 대해 주어진 함수를 적용하여 새로운 컬렉션을 생성합니다.
  • 순수 함수형 연산입니다. 기존의 컬렉션은 변하지 않고, 반환값이 존재합니다.

foreach

  • 컬렉션의 각 요소에 대해 주어진 함수를 실행합니다.
  • Side Effect를 발생시키는 데 주로 사용됩니다. 함수의 결과는 무시된다.
  • 반환값이 없으며, 단순히 각 요소에 대해 작업을 수행합니다.

for 루프

  • for loop 또는 for comprehension이라고도 합니다.
  • yield를 사용하여 시퀀스를 생성할 수 있습니다.
    • 새로운 컬렉션(Vector)을 생성
  • 또는 함수를 호출하여 평가할 수 있습니다.
  • 컬렉션을 반복합니다.
    • for (e <- col) E
      • e는 요소, col은 컬렉션, E는 표현식입니다.
  • toList를 사용하여 컬렉션을 리스트로 변환할 수 있습니다.

선언형 언어 (Declarative Language)

  • 이러한 기능 대부분은 선언형 언어에서 비롯되었다는 점을 기억하세요.
  • 대부분의 코드는 "어떻게 할 것인지"보다 "무엇이 되어야 하는지"를 나타냅니다.
    • 예: list.map(x => x + 1).reduce((a, b) => a + b * 2)
      • 이 표현식은 리스트에서 x => x + 1을 사용하여 매핑하고, (a, b) => a + b * 2를 사용하여 값을 리듀스합니다.

맵 - 리듀스 (Map - Reduce)

  • 기본적으로 map을 사용하여 수정된 컬렉션을 표현한 다음, reduce를 사용하여 컬렉션의 요소를 값으로 집계할 수 있습니다.
  • 필요한 것이 무엇인지에 따라:
    • 요소를 수정한 컬렉션이 필요한가?map을 사용하세요.
    • 요소에서 계산된 값을 단일 값으로 집계하려는가?reduce를 사용하세요.
    • 컬렉션의 일부만 필요한가?filter 등을 사용하세요...
  • 핵심은 "어떻게"보다는 "무엇"에 대해 이해하는 것입니다.