본문 바로가기

Language/Scala

[Scala] Getting started with functional programming in scala

반응형

2.1 스칼라 언어의 소개: 예제 하나


private def formabAbs(x: Int) = {
  val msg = "The absolute value of %d is %d."
  msg.format(x, abs(x))
}
  
private def formatFactorial(n: Int) = {
  val msg = "The factorial of %d is %d."
  msg.format(n, factorial(n))
}

// 꼬리 재귀가 아닌 예: stack-overflow 위험이 있다.
def factorial(n: Int): Int = {
  if (n <= 1) 1
  else n * factorial(n - 1)
}

// 꼬리 재귀의 예: stack-overflow 위험이 없다.
def factorial(n: Int): Int = {
  @annotation.tailrec // 꼬리 재귀 검출: 꼬리 재귀가 아니라면 컴파일러 오류가 발생한다.
  def go(n: Int, acc: Int): Int =
    if (n <= 0) acc
    else go(n - 1, n + acc)
  
  go(n, 1)
}

 

간단한 스칼라 프로그램


// 이것은 주석!
/* 이것도 주석 */
/** 문서화 주석 */
object MyModule { // 단일체 객체의 선언: 클래스와 클래스의 유일한 인스턴스를 동시에 선언한다.
  def abs(n: Int): Int = 
    if (n < 0) -n
    else n
  
  private def formatAbs(x: Int) = { 
    val msg = "The absolute value of %d is %d"
    msg.format(x, abs(x)) // 문자열의 두 %d 자리표를 각각 x와 abs(x)로 치환
  }
  
  def main(args: Array[String]): Unit = // Unit은 Java나 C의 void와 같은 목적
    println(formatAbs(-42))
}

이 프로그햄에서 MyModule이라는 이름의 객체(Object: module이라고도 한다)를 선언

스칼라의 코드는 반드시 Object로 선언되는 객체나 class로 선언되는 클래스 안에 들어가야 한다.

 

- def 키워드

- 메서드 이름

- 매개변수 목록 => [변수 명]: [변수 타입]

- 콜론(:) 뒤 리턴 값의 타임

- 등호(=) 

- { 함수 body }

- return 키워드 없이 마지막 라인의 객체가 곧 매서드의 반환 값

 

등호(=)의 왼쪽 코드는 좌변 / 서명(signature)라 부르고, 등호의 오른쪽 코드는 우변 / 정의(definition)라 부른다.

object 키워드

  • singleton 객체를 생성 / 인스턴스가 단 하나
  • Java의 정적 멤버(static member)를 가진 클래스를 만들만한 상황에 사용
  • Companion Object(클래스와 같은 이름을 가지는 object)를 만들 때 사용

MyModule의 세가지 함수

  • def abs(n: Int): Int
    정수(Int) 하나를 받고 결과 값으로 정수를 돌려주는 순수 함수
  • private def formatAbs(x: Int)
    private로 선언되어 MyModule 객체의 외부에서는 호출할 수 없다. 이 함수는 String을 우변으로 돌려준다. 반환 값의 타입은 선언되어 있지 않다.
    스칼라가 추론 가능한 반환 형식은 생략 가능하기 때문이다
    (그러나, 명시적으로 지정하는 것을 권장, formatAbs는 private으로 지정되었기 때문에 type annotation 생략 가능)
  • def main(args: Array[String]): Unit
    main메서드는 순수 함수적 핵심부를 호출하는 외부 계층(shell)로서 다른 말로는 절차(produce) 또는 불순 함수(impure function)라고 부르기도 함.
    스칼라는 프로그램을 실행할 때 main이라는 이름은 반드시 string array 객체를 매개변수로 받아야 하며, 리턴 타입은 Unit이어야 한다. 
    일반적으로, Unit이라는 것은 그 메서드에 부수효과가 존재함을 암시한다.

 

2.2 프로그램의 실행

scala 인터프리터를 이용하여 실행 or scala 프로그램 작성 후 scalac를 이용하여 컴파일

 

2.3 Module, Ocject, Namespace

  • 스칼라의 모든 값은 객체(object)다.
  • 각각의 객체는 0개 또는 하나 이상의 멤버(member)를 가질 수 있다.
  • namespace란 멤버가 정의된 공간을 뜻한다. 
    abs가 속한 namespace는 MyModule이다.
  • 모듈이란, 자신의 맴버들에게 namespace를 제공하는 것이 주된 목적인 객체를 뜻함
  • 멤버는 def로 선언된 메서드, val이나 object로 선언된 또 다른 객체일 수 있다.
    객체의 맴버에 접근할 때에는 " . " 을 이용하여 접근

    스칼라에서는 연산자라는 특별한 개념이 존재하지 않는다. +는 그냥 유효한 메서드 이름일 뿐, 인수가 하나인 메서드는 그 어떤 것이라도 마침표(.)와 괄호를 생략항 중위 표기법으로 호출 가능

MyModule.abs(-42) 
MyModule abs -42

 

  • import 키워드로 객체의 멤버를 현재 범위로 도입(importing)하면 객체 이름을 생략하고 사용할 수 있으며, 밑줄(underscore)을 이용하여 객체의 nonprivate 멤버를 import 할 수 있다.

 

2.4 고차 함수: 함수를 함수에 전달

  • 값으로서의 함수: 함수를 변수에 배정하거나 자료구조에 저장할 수 있고 인수로서 함수에 넘겨줄 수 있다는 개념
  • 고차 함수(higher-order function. HOF): 다른 함수를 인수로 받는 함수

2.4.1 잠깐 곁가지: 함수적으로 루프 작성하기

factorial 함수


def factorial(n: Int): Int = {
  /* 
	local definition 또는 내부 함수.
	스칼라에서는 한 함수의 본문 안에 또 다른 함수를 작성하는 일이 흔하다.
	함수형 프로그래밍에서는 이런 함수는 정수나 문자열 같은 변수와 다를 바 없는 값이다.
  */
  // 재귀적인 보조 함수 하나를 정의. 
  // 이런 보조 함수에는 go나 loop같은 이름을 붙이는 것이 관례이다. 
  def go(n: Int, acc: Int): Int =
    if (n <= 0) acc
    else go(n - 1, n * acc) 
    
  go(n, 1) 
}

루프를 함수적으로(루프 변수의 변이 없이) 작성하는 방법은 재귀 함수를 이용하는 것이다.

 

스칼라는 자기 재귀(self-recursion)를 검출해서 재귀 호출이 꼬리 위치(tail position)에서 일어난다면 꼬리 호출 제거(tail call elimination)를 적용하여 컴파일러 최적화를 통해 while 루프를 사용했을 때와 같은 종류의 바이트코드로 컴파일을 한다

// 꼬리 재귀가 아닌 예: stack-overflow 위험이 있다.
def factorial(n: Int): Int = {
  if (n <= 1) 1
  else n * factorial(n - 1)
}

// 꼬리 재귀의 예: stack-overflow 위험이 없다.
def factorial(n: Int): Int = {
  @annotation.tailrec // 꼬리 재귀 검출: 꼬리 재귀가 아니라면 컴파일러 오류가 발생한다.
  def go(n: Int, acc: Int): Int =
    if (n <= 0) acc
    else go(n - 1, n * acc)
  
  go(n, 1)
}

함수가 호출되면 stack에 push가 되는데, 최초에 호출된 함수가 끝나지 않은 상태에서  계속 재귀가 되기 때문에 push가 연속해서 이루어지고 재귀의 마지막까지 가면 그제서야 pop이 되면서 stack에 차지한 공간을 제거한다. 때문에 많은 재귀호출이 일어날 경우 stack overflow가 일어날 가능성이 있다. 
반면에 함수내에서 재귀의 결과를 이용
하는 것이 아닌 현재의 함수는 종료되고 새로운 함수를 호출하는 구조는 push와 pop이 교대로 일어나기 때문에 차지하는 stack의 공간이 일정 공간 이상은 필요하지 않는다. 당연히 구조상 Tail Recursion이 훨씬 좋은 구조이다.

 

2.4.2 첫 번째 고차 함수 작성

private def formabAbs(x: Int) = {
  val msg = "The absolute value of %d is %d."
  msg.format(x, abs(x))
}
  
private def formatFactorial(n: Int) = {
  val msg = "The factorial of %d is %d."
  msg.format(n, factorial(n))
}

// formatAbs와 formatFactorial은 거의 동일하다. 
// 두 함수에서 적용할 함수 이름과 함수 자체를 인수로 받는다면 다음과 같이 일반화할 수 있다.

def formatResult(name: String, n: Int, f: Int => Int) = {
  val msg = "The %s of %d is %d."
  msg.format(name, n, f(n))
}

formatResult 함수는 다른 함수를 받는 하나의 고차 함수이다. 다른 인수들처럼 f에도 형식을 지정.

이 매개변수의 타입은 Int => Int인데, int형 매개변수를 받아 int형 값을 리턴한다. 

 

 

2.5 다형적 함수: 형식에 대한 추상

  • 단형적 함수(monomorphic function): 한 형식의 자료에만 작용하는 함수
  • 다형적 함수(polymorphic function): 임의의 형식에 대해 작동하는 함수

2.5.1 다형적 함수의 예

def findFirst(ss: Array[String], key: String): Int = {
  @annotation.tailrec
  def loop(n: Int): Int = 
    if (n >= ss.length) -1
    else if (ss(n) == key) n
    else loop(n + 1)
  
  loop(0)
}

위의 findFirst 함수는 Array[String]에서 주어진 String과 일치하는 첫 번째 인덱스 값을 돌려주는 단형적 함수이다. 이를 임의의 형식 A에 대해 Array[A]에서 A를 찾는 다형적 함수로 변경하면 다음과 같다.

def findFirst[A](as: Array[A], p: A => Boolean): Int = {
  @annotation.tailrec
  def loop(n: Int): Int = 
    if (n >= as.length) -1
    else if (p(as(n)) n
    else loop(n + 1)
  
  loop(0)
}

generic function인 다형적 함수의 한 예이다. 이 함수는 형식에 대한 추상(abstraction over type)을 배열과 배열 안의 한 요소를 검색하는 함수에 적용한 결과이다.

 

다형적 함수

- 쉼표로 구분된 형식 매개변수(type parameter)들의 목록 ( [A] )

- 함수의 이름 명시

- 매개변수 선언

형식 매개변수 목록은 형식 서명의 나머지 부분에서 참고할 수 있는 형식 변수(type variable)들을 도입 한다. 이는 함수의 매개변수 목록이 함수의 본문에서 팜조할 수 있는 변수들을 도입하는 것과 마찬가지이다.

findFirst에서 형식 변수 A는 두 곳애서 참조된다. 배열의 원소들은 형식이 반드시 A여야 하며 함수 p는 반드시 A 타입의 값을 반환해야 한다. 형식 서명의 두 장소에서 동일한 형식 변수를 참조한다는 사실은 해당 두 인수의 형식이 동일해야 함을 의미. 컴파일러는 모든 findFirst 호출 장소에서 이 사실을 강제함. 즉 Array[Int]에서 String을 찾으려 하면 type 불일치 오류가 발생한다.

 

2.5.2 익명 함수로 고차 함수 호출

고차 함수를 호출할 때, 기존의 이름 붙은 함수를 인수로 지정해서 호출하는 것이 아니라 익명 함수(anonymous function) 또는 함수 리터럴(function literal)을 지정해서 호출하는 것이 편리한 경우가 많다.


findFirst(Array(7, 9, 13), (x: Int) => x == 9)

  • Array(7, 9, 13)
    배열 리터럴이다. 정수 세개를 담은 새 배열을 생성. 스칼라에서는 이처럼 new 키워드 없이 배열을 생성할 수 있다.
  • (x: Int) => x == 9
    함수 리터럴 또는 익명 함수이다. 함수의 인수들은 =>의 좌변에서 선언된다. 
    정수 두 개를 받아서 서로 같은지 판단하는 함수는 
    (x: Int, y: Int) => x == y로 정의한다.

스칼라에서 값으로서의 함수

함수 리터럴을 정의할 때 실제로 정의되는 것은 apply라는 메서드를 가진 하나의 객체이다. apply라는 메서드를 가진 객체는 그 자체를 메서드인 것처럼 호출할 수 있다. (a, b) => a < b는 사실 다음과 같은 객체 생성에 대한 syntactic sugar이다.

val lessThan = new Function2[Int, Int, Boolean] { 
       def apply(a: Int, b: Int) = a < b 
}

lessThan의 형식은 Function2인데, 통상적으로 쓴다면 (Int, Int) => Boolean이다. Function2 인터페이스(스칼라에서는 특질[trait]이라 부른다)에는 apply가 있다. ledssThe함수는 을 lessThan(10, 20) 형태로 호출하는 것은 apply메서드를 이용하여
 
val b = lessThan.apply(10, 20)

처럼 호출할 수 있지만 이는 syntactic sugar이다.

Function2는 표준 스칼라 라이브러리가 제공하는 보통의 특질(인터페이스)로 인수 두 개를 받는 함수 객체들을 대표하며 Function0~22의 trait을 지원한다.
함수는 보통의 스칼라 객체이며, 이 사실을 흔히 "함수의 일급(first-class)값이다"라고 말한다. 

 

2.6 형식에서 도출된 구현

다형적 함수를 구현할 때에는 가능한 구현들의 공간이 크게 줄어든다. 주어진 다형적 형식에 대해 단 하나의 구현만 가능해질 정도로 가능성의 공간이 축소되는 경우도 있다. 단 한 가지 방법으로만 구현할 수 있는 함수 서명의 예를 살펴본다.

 

부분 적용(partial application)

- 인자 목록이 둘 이상 있는 함수의 경우, 필요한 인자 중 일부만 적용해 새로운 함수를 정의

def partial1[A, B, C](a: A, f: (A, B) => C): B => C =
  (b: B) => f(a, b)

커링(currying)

- 여러 인자를 취하는 함수를 단 하나의 인자를 취하는 함수의 연속으로 변환

 

함수 합성(function composition)

- 한 함수의 출력을 다른 함수의 입력으로 공급

반응형

'Language > Scala' 카테고리의 다른 글

[Scala] Data Structures  (0) 2019.11.10
[Scala] Functional data structures  (0) 2019.10.29
[Scala] FP의 두 가지 개념  (0) 2019.10.12
[Scala] What is functional programming?  (0) 2019.10.04
ex  (0) 2019.03.05