함수형 프로그래밍

4 분 소요

절차지향, 객체지향처럼 프로그래밍 패러다임의 하나이다.

프로그래밍 패러다임

프로그래밍 패러다임은 개발자가 개발할때 대상을 어떠한 관점에서 바라봐야 하는지 기준을 제시하고 결정하는 역할을 한다. 예를 들어 객체지향 프로그래밍은 프로그램이란 서로 상호작용하는 객체의 집합이며, 객체는 책임과 역할에 따라 적절한 동작을 수행하는 것으로 정의한다. 개발자는 개발할때 이 관점에서 프로그램을 바라보기 때문에 객체를 정의하기 위한 class를 설계하게되며 메서드를 만들때 책임과 역할을 고민하여 만들게 된다.

패러다임은 소프트웨어를 바라보는 관점을 제시하는 것이기 때문에 서로 다른 모든 패러다임이 배타적인 관계인것은 아니다.

예를 들어 명령형 프로그래밍, 선언형 프로그래밍, 객체지향 프로그래밍이 있을때 명령형 프로그래밍과 선언형 프로그래밍은 대조관계 인것은 맞지만 명령형 프로그래밍과 객체지향 프로그래밍은 대조관계 는 틀린 말이다.

개발 언어가 하나의 패러다임만을 지원하는것은 아니다.
예를 들어 C++는 절차적 프로그래밍, 객체기반 프로그래밍, 객체지향 프로그래밍, 제네릭 프로그래밍 등의 요소들을 지원하도록 설계되었다.

또한, 언어가 지원하는 패러다임은 확장될 수 있다.
예를 들어 초창기 자바의 경우 함수형 프로그래밍을 지원하지 않았으나, 1.8 버전부터 함수형 프로그래밍을 지원한다.

1. 함수형 프로그래밍이란

공유 상태 (shared state), 변경 가능한 데이터(mutable data), 부수 효과(side effect) 없이 순수 함수(pure function)의 조합을 통해 프로그램을 구축하는 것. 이 때 함수의 조합은 선언적으로 표현된다.

2. 명령형 프로그래밍 vs 선언형 프로그래밍

명령형 프로그래밍과 선언형 프로그래밍은 서로 반대되는 개념이다.

  • 명령형 프로그래밍
    컴퓨터가 어떻게(How) 명령을 수행하는지를 순서대로 써 놓는 것.

  • 선언형 프로그래밍
    어떤 방법으로 해야 하는지(How)가 아니라 무엇(What)을 해야 하는지를 설명하는 것.

쉽게 말해 명령형 프로그램은 일반적으로 개발하는 방식으로 구현 동작이 어떻게 동작해야 하는지 방법(알고리즘)을 명시하는 것이라면, 선언형 프로그래밍은 방법(알고리즘)을 명시하는것이 아니라 무엇을 해야 하는지(목표)를 명시하는 것이다.

함수형 프로그래밍의 경우 선언형 프로그래밍의 하나이다.

3. 1급 객체(first class)

함수형 프로그래밍을 위해서 함수는 1급 객체여야 한다. 아래는 1급 객체가 무엇인지를 설명한다.

아래 조건을 만족하는 경우 1급 객체(first class) 라고 한다.

  1. 어떤 변수나 데이터에 할당할 수 있어야 한다.
  2. 파라미터로 전달할 수 있어야 한다.
  3. 리턴값 (return value)으로 사용할 수 있어야 한다.

JavaScript는 언어 자체에서 함수(Function)를 객체(Object)로 다루고있어 위 특징을 만족한다. 따라서 JavaScript에서 함수는 1급 객체이다.

3.1. 코틀린의 1급 객체 함수

코틀린은 아래처럼 1급 함수를 사용할 수 있도록 지원한다.

  1. 함수를 변수에 할당할 수 있음.

     val firstClassFunction: () -> Unit = {
         println("This is function") 
     }
    
     val a = firstClassFunction
    
  2. 파라미터로 전달할 수 있음.

     fun function(func: () -> Unit) {
         func.invoke()
     }
    
  3. 리턴값으로 사용할 수 있음.

     fun function(): () -> Unit {
         return { println("This is function") }
     }
    

3.2. 코틀린은 어떻게 1급 객체 함수를 지원하는가.

위 코드에 대해 자바코드로 변환해보면 코틀린이 함수를 1급 객체로 다루는 방식을 알 수 있다.

fun myFunction(func: () -> Unit) {
	func.invoke()
}
public final void myFunction(@NotNull Function0 func) {
	Intrinsics.checkParameterIsNotNull(func, "func");
	func.invoke();
}

위와 같이 1급 객체로 사용된 함수는 임의로 FunctionX 라는 클래스의 객체로 만든다. 즉, 함수를 객체화 시켜서 1급 객체의 특징을 가질 수 있게 만들었다.

4. High-order function (고차함수)

아래 조건을 하나라도 만족하는 함수를 High-order function 이라 한다.

  1. 함수를 파라미터로 전달 받는 함수
  2. 함수를 리턴하는 함수

고차함수 조건을 보면 알수있듯 고차함수가 되려면 기본적으로 함수가 1급 객체여야 한다.

5. 람다함수, 람다식

람다 함수는 프로그래밍 언어에서 사용되는 개념으로 이름 없는 함수인 익명 함수(Anonymous functions)를 지칭하는 용어이다.

람다식은 주로 고차 함수(High-order function)에 인자(argument)로 전달되거나 고차 함수가 돌려주는 리턴값으로 쓰인다. 즉, 람다 함수는 1급 객체 함수이다.

돌도 도는 말이 많은데.. 결국 함수형 프로그래밍은 “함수가 1급 객체” 라는것에서 시작한다. 함수를 파라미터로 받거나 리턴하는 함수를 고차함수라고 하고, 이 고차함수의 파라미터 함수나 리턴 함수를 표현하는 표현식을 람다식이라 한다.

람다를 함수형 인터페이스(functional interface)라고 표현하는 내용이 많은데 이는 람다식으로 표현가능한 본체는 하나의 함수만 가진 인터페이스로 표현되기 때문이다.

예를 들어 View.OnClickListener 는 아래와 같다.

public interface OnClickListener {
	void onClick(View v);
}

람다식을 사용하지 않고 View에 OnClickListener를 설정하는 코드는 아래와 같다.

button.setOnClickListener(object: OnClickListener {
	override fun onClick(v: View) {
		// todo
	}
})

람다식을 사용하면 아래와 같다.

button.setOnClickListener { v ->
	// todo
}

위와 같이 람다식을 사용하면 자잘한 코드를 작성하지 않아 간결해진다.

6. CallByName

파라미터로 전달 될때 평가(실행)되지 않고, 실제로 call이 될때 평가(실행)하는 것을 말한다.

fun callByValue(b: Boolean): Boolean {
	println("callByValue")
	return b
}

fun callByName(func: () -> Boolean): Boolean {
	println("callByName")
	return func()
}
    
val funA: () -> Boolean = {
	println("funA")
	true
}

fun main() {
	// CallByValue 에 의한 호출
	callByValue(funA())
	
	// CallByName 에 의한 호출
	callByName(funA)
} 

callByValue() 결과 : funA, callByValue
callByName() 결과 : callByName, funA

위 예처럼 파라미터로 전달되는 시점이 아니라 호출이 발생할때 실행되는 것을 callByName 이라고 한다.

고차 함수(High-order funtion)는 CallByName으로 실행된다.

7. 순수 함수(pure function)와 부수 효과(Side effect)

아래 조건을 만족하는 함수를 순수 함수(pure function)라고 한다.

  1. 입력이 같을때 항상 같은 출력을 리턴한다.
  2. 함수의 호출이 객체의 상태를 변경하거나 외부와 상호작용하지 않는다.
  3. 콘솔이나 로그로 출력(print)하지 않는다.
  4. 파일, 데이터베이스, 네트워크 어디에도 데이터를 쓰지(write)않는다.
  5. 예외가 발생하지 않는다.

함수형 프로그래밍은 이러한 순수함수의 조합으로 프로그래밍 하는 방식을 말한다.

순수함수는 외부와의 상호작용이 없기 때문에 외부 환경에 따라 변하는 side effect(부수 효과)가 없다. 그리고 외부와 상호작용이 없다는 것은 순수 함수가 mutable data가 아니라 immutable data를 사용한다는 것을 말한다.

순수함수로 만들어진 함수는 그 함수의 결과를 계산하는 것 외에 다른 효과가 없기 때문에, 버그의 주요 원인을 제거할 수 있고, 함수의 실행 순서가 덜 중요하게 된다.

7.1. 순수 함수 예제

var z = 10
fun pureAdd(x: Int, y: Int): Int {
    return x + y
}

위 함수는 순수 함수이다.

var z = 10
fun nonPureAdd(x: Int, y: Int): Int {
    return z + x + y
}

위 함수는 순수 함수가 아니다.

fun buyCoffe(card: Card, coffee: Coffee): Card {
	card.charge(coffe.price)
	return card
}

위 함수는 순수 함수가 아니다. 외부에서 전달받은 card 상태를 변경시켰기 때문이다. 위 코드를 순수 함수로 만들고자 한다면 card를 복사해야 한다.

fun buyCoffe(card: Card, coffee: Coffee): Card {
	val c = card.copy()
	c.charge(coffe.price)
	return c
}

위 함수는 순수 함수이다.

writeFile(fileName)
updateDatabaseTable(sqlCmd)
sendAjaxRequest(ajaxRequest)
openSocket(ipAddress)

위와 같은 함수들은 순수함수가 아니다. 외부 상태를 변경시키기 때문이다.


[참고 문서]

  1. 위키피디아
  2. 나무위키
  3. 참고 블로그 1
  4. 참고 블로그 2
  5. 참고 블로그 3
  6. 참고 블로그 4
  7. 참고 블로그 5
  8. 참고 블로그 6
  9. 참고 블로그 7
  10. 참고 블로그 8

카테고리:

업데이트:

댓글남기기