-
[스칼라 문법편] CH05 FP의 기본! 함수24년 11월 이전/레거시-누구나 쉽게 스칼라+플레이 2019. 1. 29. 22:13반응형
* 이 포스팅은 책 "누구나 쉽게 스칼라 + 플레이 - 고락윤 한빛미디어" 를 읽고 정리한 것입니다.
CH05 FP의 기본! 함수
이번 장에서는 스칼라에서 중요한 철학 중 하나인 함수형 프로그래밍의 기본 함수에 대해서 알아보고, 추가로 몇 가지 더 알아보도록 하겠습니다.01. FP의 배경
사실, Functional Programming, FP는 근래에 나온 개념이 아닙니다. 굉장히 오래된 개념 중 하나이지요. 근데 15~16년 이후부터 재조명을 받게 되었을까요? 이유는 컴퓨터의 발전 과정에 있습니다. CPU가 엄청난 속도로 발전하면서, 그 발전의 한계가 점점 드러나고 있지요. 바꿔 말하면 CPU 1개의 성능을 향상시키기 위한 발전의 속도가 떨어졌다는 뜻입니다. 그 결과 우리는 컴퓨터를 더 빠르게 처리하기 위해서 CPU 개수를 늘리는 방식을 선택하였습니다.이러한 환경에서는 여러 CPU에서 효율적으로 동시에 계산하고 이 결과를 잘 합치는 것이 중요한데, 이러한 프로그래밍 방식을 멀티 스레딩이라고 하죠. 멀티 스레딩은 이른바, 악마의 프로그래밍이라고 불리울 정도로 엄청나게 어렵습니다. 그래서 이것을 해결하기 위해서, 데이터 불변성, side effect가 없는 순수 함수등을 이용한 함수형 프로그래밍이 서서히 대두되기 시작한 것입니다. 이제부터 FP의 기본 함수에 대해 알아봅시다!
02. 스칼라에서의 함수
스칼라에서 함수는 다음과 같이 정의할 수 있습니다.def 함수명(파라미터 명: 자료 형, ...) : 반환형 = { //코드 }
이번에는 이전에 말했던, 순수 함수에 대해서 알아보도록 하죠. side effect가 없는 순수 함수란 무엇을 말하는 것일까요? 이것은 동일한 입력에는 동일한 출력을 보장하는 함수를 뜻합니다. 예를 들어서 다음의 함수가 있다고 하죠.def add(x: Int) = x + 1
add 함수는 언제나2가 들어오면 3이 나옵니다. 순수 함수란 이처럼 내부 상태가 변화가 없는 함수를 지칭합니다. 이번에는 함수의 호출 방식을 살펴보죠. 스칼라에서는 대표적으로 호출 방식이 2가지가 존재합니다. 먼저 기존 메소드 호출 방식과 같은 Call-By-Value 방식입니다. 다음의 코드를 살펴보도록 하죠.
def dropship(n: Int) = { println("RIDE ON DROPSHIP") println(s"${n} people ridding!") } def people(n: Int) = { println("Ready to ride dropship") n } //main dropship( people(5) )
이렇게 했을 때 함수 호출이 people -> dropship 순입니다. 따라서 출력은 다음과 같죠Ready to ride dropship RIDE ON DROPSHIP 5 people ridding!
이번에는 Call-By-Name 방식의 호출을 봅시다. 다음 코드를 살펴보도록 하죠.def dropship(n: => Int) = { println("RIDE ON DROPSHIP") println(s"${n} people ridding!") } def people(n: Int) = { println("Ready to ride dropship") n } //main dropship(people(5))
이 경우 출력을 살펴보도록 하죠.RIDE ON DROPSHIP Ready to ride dropship 5 people ridding!
이 경우, n은 자료형이 함수인 객체입니다. 즉 함수 호출 순서는 dropship -> people 로 바뀌게 됩니다. n이 불리울 때 비로소 people 함수가 호출되는것이죠. 이러한 방식의 장점을 책에서는 다음과 같이 정리하고 있습니다."함수를 매개변수로 선언하고 이용하는 것은 함수의 공통적인 부분을 추출할 수 있다. 구체적인 로직은 따로 구현하고 공통적인 로직에서 이를 이용하게 되면 결론적으로 코드가 줄고 구조가 튼튼해진다."
이번에는 부분 적용함수에 대해서 알아봅시다. 부분 적용 함수 Partially Applied Function은 특정 함수에서 매개 변수 하나를 고정시켜서 동작시키게 하는 함수라고 생각하시면 편합니다. 예를 들어보겠습니다.def go(i: Int, str: String) = { println(s"${str} : ${i}") } //main val fixedYear = go(2019, _) fixedYear("TEST1") fixedYear("TEST2") fixedYear("TEST3")
이 코드의 결과는 어떻게 될까요? 출력은 다음과 같습니다.TEST1 : 2019 TEST2 : 2019 TEST3 : 2019
특정한 곳에서, 함수를 여러번 호출 할 때, 매개변수가 만약 같다면, 이를 계속 호출하기보단, 부분 적용 함수를 이용하여 호출하는 것이 더 직관적인 코드가 도리 것입니다. 실제로 fixedYear은 함수 객체로 apply()가 적용되어 그 결과를 반환하는 녀석입니다. apply()는 이 장 끝에서 다시 설명하도록 하겠습니다.Cuurying(커링)이란?
FP를 자주 하다보면 커링이란 단어가 자주 나옵니다. 쉽게 설명하면, 여러 개의 인수를 받는 함수를 하나의 인수를 받는 여러 개의 함수로 변경해주는 기법입니다. 코드를 살펴보죠.
def sum(x: Int, y: Int) = x + y def currying(x: Int)(y: Int) = x + y //main for (i <- 1 to 10){ println(sum(3, i)) } val threePlus = currying(3)_ for (i <- 1 to 10){ println(threePlus(i)) }
위 두개의 반복문은 같은 결과를 냅니다. 다만 적용 방식이 조금 다르죠. 이 때 sum은 기존 여러개의 인수를 받는 하나의 함수를 표현하며 currying은 하나의 인수를 받는 함수가 여러개 중첩되어 있는 형식을 된 것을 확인할 수 있습니다.03. 람다
람다는 일종의 함수 자체를 값으로 받을 수 있게 하는 표현식입니다. 예를 들어 다음의 함수가 있다고 가정하겠습니다.def calc(x: Int, y: Int)(f: (Int, Int) => Int) = f(x, y) def add(x: Int, y: Int) = x + y def sub(x: Int, y: Int) = x - y def mul(x: Int, y: Int) = x * y def div(x: Int, y: Int) = x / y //main val fourtwo = calc(4, 2)_ println( fourtwo(add) ) println( fourtwo(sub) ) println( fourtwo(mul) ) println( fourtwo(div) )
여기서 f는 (Int, Int) => Int 형의 함수 타입입니다. 2개의 Int형을 인수로 받아 Int를 결과를 반환하는 함수를 가리키는 것이죠. 그래서 커링 기법을 이용해, x, y에 각 각 4, 2로 고정시켜놓고 덧셈, 뺼셈, 곱셈, 나눗셈을 인자로 집어넣는 것이죠. 내부적으로는 함수 인수를 넣을 때, Function 임시 객체를 생성해서, apply()에 적용하는 것입니다.또한, fourtwo의 매개 변수로 정의해둔 함수를 써도 되지만 그 때 필요한 함수를 정의해서 쓸 수 있습니다. 파라미터와, 반환형만 맞다면 말이죠. 다음 코드를 살펴볼까요?
//main println( fourtwo( (x, y) => x * y + 2 ) )
이렇게 쓸 수 있다는 거죠. 결과는 10 (4 * 2 + 2)이 나옵니다. 즉 필요하면 로직을 손쉽게 변경할 수 있다는 것이죠. 저 위의 표현식을 람다라고도 부릅니다. 이는 함수가 언어에서 일급 시민일때, 가능하다는 것을 알아주셨으면 좋겠습니다. 일반적으로 FP를 지원하는 모든 언어는 함수 역시 일급 시민입니다.04. 기타
마지막으로 이번 파트에서 알아가면 좋은 것들 몇 가지만 소개하고 마치고자 합니다. 첫 번째로 매개 변수가 여러 개인 함수를 표현한 것입니다.
def printStrins(args: String*) = { for (arg <- args) println(arg) } //main printStrings("Hey", "Ho", "Ha")
printStrings 처럼 매개 변수를 T*로 받으면, T 인수를 여러 개 받을 수 있습니다. List()가 이와 동일하게 동작하죠. 두 번째로는 저번 장에서 언급했다시피 함수 파라미터의 기본 값을 지정해줄 수 있습니다. 바로 다음처럼 말이죠.
def default(x: Int = 5, y: Int = 5) = x + y //main println( default() )
이렇게 하면, x, y에 값을 넣어주지 않아도 기본 인자가 설정되어 있기 때문에, 각각 5가 들어가 10을 반환합니다. 이 때, default(1) default(1, 2)를 호출해보세요. 어떤 값이 출력되고 왜 이렇게 되는지 알아보는 것도 좋은 과제가 될 것 같습니다. 여기서 추가적으로 메소드 호출 시에 파라미터의 값을 직접 넣어줄 수 있습니다.val res = default(x=2, y=5) println(res)
이렇게 말이죠. 세 번째는 apply 메소드입니다. apply 메소드는 자주 등장하는 공통적인 함수입니다. 이것은 변수를 받아 함수에 적용시켜 결과를 반환하는 일종의 설정자 같은 역할을 합니다. 예제를 살펴보죠.
class Some { def apply(m: Int) = method(m) def method(i: Int) = i + i } //main val s = new Some println( s(2) )
이것의 출력 값은 무엇일까요? 출력은 4입니다. 다음의 3가지 코드는 동일하게 동작됩니다.
s(2) s.apply(2) s.method(2)
이제 이 장의 마지막 implict 키워드에 대해서 알아봅시다. implicit 키워드는 묵시적으로 어떤 특정한 동작을 제거하는데 쓰일 수 있습니다. 예를 들어서 웹 프로그래밍으 경우 사용자 요청으로부터 관습적으로 웹에 관련된 설정을 해주어야 하는데, 이를 생략할 때, 이 키워드를 쓸 수 있습니다. 이것은 추후에 더 자세히 다루도록 하죠. 지금은 한 가지 간단한 예를 들어봅시다.
//main implicit def doubleToInt(d: Double) = d.toInt val x: Int = 18.0 println(x)
이 코드는 놀랍게도 정상적으로 컴파일되고 실행됩니다. 원래대로라면, Int형 변수를 선언했을 때 18.0 Double에 의해서 타입 미스가 나서 컴파일 에러가 되어야 하지만, implicit 키워드를 붙인 내부 메서드가 스스로 동작시켜서 18.0을 18로 만들어버리기 때문입니다. 굉장히 똑똑하죠? ㅎㅎ 그러나 doubleToInt 같이 동작하는 함수 즉 Double => Int 함수형이 implicit로 2개 이상 정의된 경우, 어떤 것을 선택해야 할지 몰라 컴파일 에러가 뜬다는 것을 알아두시면 좋겠습니다.
이렇게 해서 스칼라의 FP의 기본 함수에 대해서 알아보았습니다. 어떠신가요? ㅎㅎ 스칼라에 대해서 저와 함께 더 공부하고 싶으시다면 다음 장 패턴 매칭에서 만나도록 합시다!
728x90'레거시 > 레거시-누구나 쉽게 스칼라+플레이' 카테고리의 다른 글
[스칼라 문법편] CH07 컬렉션 (0) 2019.01.29 [스칼라 문법편] CH06 패턴 매칭 (0) 2019.01.29 [스칼라 문법편] CH04 스칼라의 OOP (0) 2019.01.29 [스칼라 문법편] CH03 조건문과 반복문 (0) 2019.01.29 [스칼라 문법편] CH02 변수 다루기 (0) 2019.01.28