카테고리 없음

코틀린 기본 3 - (함수, 함수형 프로그래밍)

누알라리 2020. 4. 9. 18:56

1. 함수 간략하게 만들기 ( 대박 )

fun sum(a : Int, b : Int) : Int {
	return a+b
}

fun sum(a : Int, b : Int) : Int = a + b

fun sum(a : Int, b : Int) = a + b

 

2. 인자와 매개변수

fun sum (a: Int, b:Int) = a+b

fun main() {
	println($sum(5,3))
}

a,b = 매개변수
5,3 = 인자

함수를 선언할 때는 매개변수라고 부르고 함수를 호출할 때는 인자라고 부른다.

 

3. 함수와 스택 프레임

함수의 각 정보는 프레임(Frame)이라는 이름으로 스택 메모리의 높은주소에서부터 낮은주소로 거꾸로 자라듯이 채워져 간다.

호출된 순서대로 스택 프레임에 쌓이게되며, 사라질 때는 스택의 특성상 선입후출로 처음 들어온 main()이 제일 늦게 소멸된다.

이 때, 낮은 주소 -> 높은주소로 내려오는 힙 영역과 스택 영역이 만나면 스택오버플로가 발생한다.

 

4. Unit

Unit은 코틀린에서 다루는 특수한 자료형 중 하나로 반환값이 없을 때 사용한다.

자바의 void형과 대응하지만, void는 정말로 아무것도 반환하지 않고 Unit은 특수한 객체를 반환한다는 차이점이 있다.

 

5. 매개변수 기본값 세팅 기능

함수를 만들 때 매개변수에 default값을 세팅할 수 있다.

모든 매개변수에 기본값이 지정되면 인자를 전달하지 않아도 함수를 호출할 수 있다.

fun sum( a : Int = 200, y : Int = 300, myName : String = "HYUNNDY" ) {}

매개변수가 매우 많아서 헷갈리는 경우를 위해 매개변수의 이름과 함께 인자를 전달하는 방법도 제공한다.

sum(myName="Seula") // a,b는 기본값으로 넘어감

 

6. 가변 인자 = 가변형 매개변수 = Variable Argument = vararg

변수앞에 vararg 키워드 + : 자료형을 붙이면 그 매개변수는 자료형 배열이 된다.

fun normalVarargs(vararg counts : Int) {
	for(i in counts) {
    	println("$i")
    }
}

 

7. 함수형 프로그래밍

코틀린은 함수형 프로그래밍 + 객체지향 프로그래밍을 둘 다 지원하는 다중 패러다임 언어이다.

 

함수형 프로그래밍은 <<순수 함수>>를 작성하여 프로그램의 부작용을 줄이는 프로그래밍 기법이다.

 

함수형 프로그래밍에서는 <<람다식>> 과 <<고차 함수>>를 사용함.

7-1. 순수 함수

순수함수의 조건
  1. 같은 인자에 대하여 항상 같은 값을 반환 한다. ( = 부작용이 없다 )
  2. 함수 안에서 함수 외부의 어떤 상태도 바꾸지 않는다.
1. 순수 함수의 예

fun sum ( a : Int, b : Int ) = a + b

동일한 인자인 a,b를 입력받아 항상 a+b를 출력해서 부작용이 없고,
함수 안에서 함수 외부의 어떤 상태도 바꾸지 않는다.

2. 순수 함수가 아닌 경우

fun check() {
	val test = User.grade()
    if(test != null) process(test)
}

check()함수 안에 없는 외부의 User 객체를 사용.
변수 test는 User.grade()의 실행 결과에 따라 달라짐.

check()함수만 놓고보면 User는 뭔지, test는 뭐가 들어가는지, process()는 뭘하는 함수인지 알 수 없다.
따라서 check()의 실행 결과를 예측할 수 없다.

즉, check()는 순수 함수가 아니다.

 

7-2. 람다

{ x, y -> x + y }  이름이 없는 함수 형태 

람다식이란 

  • 다른 함수의 인자로 넘기는 함수
  • 함수의 결과값으로 반환하는 함수
  • 변수에 저장하는 함수

7-3. 일급 객체

함수형 프로그래밍에서는 함수를 <<일급 객체>>라고 생각한다.

일급 객체의 조건
   1. 일급 객체는 함수의 인자로 전달할 수 있다.
   2. 일급 객체는 함수의 반환값에 사용할 수 있다.
   3. 일급 객체는 변수에 담을 수 있다.

함수가 일급 객체라면 일급 함수라고하며, 일급 함수에 이름이 없는 경우 람다식 함수 혹은 람다식이라고 부른다.

즉, 람다식은 일급 객체의 조건을 가진 이름 없는 함수이다.

 

7-4. 고차 함수

고차 함수(High - order Function) 란 <<다른 함수를 인자로 사용>>하거나 <<함수를 결과값으로 반환>>하는 함수를 말한다.

일급 객체 혹은 일급 함수를 서로 주고받을 수 있는 함수.

fun main( ({x,y} -> {x+y}), 10, 20)) -> 람다식 함수를 인자로 넘김

fun highFunc( sum : (Int, Int) -> Int, a: Int, b: Int ) : Int = sum(a,b) -> 람다식 결과값을 반환
fun highFunc( sum : (Int, Int) -> Int, a : Int, b : Int) : Int = sum(a,b)

sum = 자료형이 람다식인 람다식 매개변수.
(Int, Int) -> Int = 자료형이 람다식으로 선언되어 {x,y -> x+y} 형태로 인자를 받는 것이 가능.

-> 함수를 호출하는쪽에서 sum 람다식 매개변수가 할 일을 정해줄 수 있음.

a,b = Int형 매개변수
sum(a,b) = 함수의 결과값을 람다식으로 리턴.

 

7-5. 함수형 프로그래밍의 정의와 특징

순수 함수를 사용해야 한다.
람다식을 사용할 수 있다.
고차 함수를 사용할 수 있다.

 

8. 기타 다양한 함수 종류

8-1. 익명 함수

일반 함수이지만 이름이 없는 함수.
(람다식도 이름은 없지만 이건 <<일반 함수>> 이다)
1. 익명 함수
fun(x:Int, y:Int) : Int = x+y

2. 익명 함수를 변수 선언에 사용 -> 위에걸 썻다는게 아니라 익명 함수로 변수 선언을 할 수 있다는 것.
val add : (Int, Int) -> Int = fun(x,y) = x+y
val result = add(3,5)

3. 자료형 생략 ver.
val add = fun(x : Int, y : Int)= x+y

4. 람다식 ver
val add = { (x: Int, y: Int) -> (x + y) }

람다식 쓰면 되는데 왜 익명 함수 쓰는거임?

람다식에서는 return , break, contine처럼 제어문을 사용하기 어렵기 때문에.
(라벨표기법으로 쓸 수는 있음)

함수 본문 조건식에 따라 함수를 중단하고 반환해야 하는 경우엔 익명 함수 사용 권장~

8-2 .인라인 함수

함수 앞에 inline 키워드를 붙여 사용한다.
이 함수가 호출되는 곳에 함수 본문의 내용을 모두 복사해서 분기 없이 처리되는 함수

분기 없이 처리되기 때문에 인라인 함수를 짧게 작성하면 코드의 성능을 높일 수 있다.

람다식 매개변수를 가지고 있는 함수에서 동작한다.

 

보통 함수는 호출되었을 때 지금까지의 내용을 저장하고 그 함수로 분기해서 다시 돌아와야되기 때문에 CPU와 메모리에 오버헤드가 좀 발생한당.

 

inline fun shortFunc(a : Int, out : (Int) -> Unit) {
	println("Before Calling out()")
    out(a)
    println("After Calling out()")
}


fun main() {

    // 인라인 함수의 내용이 모두 main에 복사해서 들어오기 때문에 분기 안함.
	shortFunc(3) { println(it -> "First Call : $it") }
    shortFunc(5) { println(it -> "Second Call : $it") }
}

실행 결과
Before Calling out()
First Call : 3
After Calling out()
Before Calling out()
Second Call : 5
After Calling out()

8-2-1) 역컴파일

IntelliJIDEA의 Tools -> Kotlin -> Show Kotlin ByteCode 메뉴를 통해 Decompile. 즉, 역컴파일된 내용을 볼 수 있다. 

이걸 확인하면 실제로 inline 키워드를 붙이면 함수가 invoke되지 않은걸 알 수 있다.

 

8-2-2) noinline

인라인 함수에서 람다식을 너무 남발하는 바람에 너무 많은 코드가 복사되면 안되니까 분기하고 싶은 람다식 앞에는 noinLine 키워드를 붙여서 분기시킬 수 있다.

inline fun shortFunc( a : Int, noinline out : (Int) -> Unit ) { }

8-2-3) 비지역 반환

crossinline쓰면 람다식의 비지역 반환을 금지시킬 수 있다.

fun shortFunc( a : Int, out : (Int) -> Unit ) { 
	println("Start")
    out(a)
    println("End")
}

fun main() {

	shortFunc(3) { println("집에가고싶다 $") return; }

}

람다식에서 return해줬으므로 비지역 반환되어

실행 결과
Start
집에가고싶다 3

이 된다.

 

8-3. 중위 함수

중위 표현법을 이용해 일종의 연산자를 구현할 수 있는 함수.

중위 표현법: 클래스의 멤버를 호출할 때 사용하는 점(.)을 생략하고 함수 이름 뒤에 소괄호를 붙이지 않아 직관적인 이름을 사용할 수 있는 표현법
중위 함수의 조건
  1. 멤버 메서드 또는 확장 함수여야 한다.
  2. 하나의 매개변수를 가져야 한다.
  3. infix 키워드를 사용하여 정의한다
자료형 클래스 Int에 multiply 라는 확장 함수 추가 후 중위 표현법으로 사용하기.

infix fun Int.multiply(x:Int) : Int = return this * x

fun main() {
	
    val original = 3.multiply(10)
    
    val changed = 3 multiply 10
    println("중위표현법 적용 = $changed")
}

8-4. 꼬리 재귀 함수

trailrec 키워드를 사용해 선언한다.

기존 재귀 함수는 자기 자신을 계속 호출하기 때문에 계속 분기 된다.
이 때 돌아갈 메모리 주소를 스택에 저장해두기 때문에 탈출 조건을 만들지 않거나, 메모리를 너무 많이 쓰는 코드를 짜면 스택 오버플로우가 발생한다.

그래서 스택에 계속 쌓이는 방식이 아닌 꼬리를 무는 재귀의 탈을 쓴 반복문인 꼬리 재귀 함수가 등장한다.
팩토리얼을 재귀, 꼬리재귀로 만들어본다.


1. 재귀
fun factorial(n: Int) : Long {
	if(n==1) return n.toLong();
    else return n * factorial(n-1)
}

반환값에서 새로운 함수가 호출되기 때문에 전에 호출된 factorial() 함수 주소값을 계속 스택에 저장해둔다.

1. 꼬리 재귀 함수
trailrec fun factorial(n : Int, rss : Int = 1) : Long {
	if(n==1) {
    	return rss
    } else {
    	return factorial(n-1, rss*n)
    }
}

반환값에서 함수를 호출하지 않고 (rss*n) 계산 부터 한 후 다음 함수를 호출하기 때문에
스택 메모리를 낭비하지 않는다.
이 코드를 컴파일러는 재귀의 탈을 쓴 반복문으로 해석한다.
int factorial(int n){
    int res = 1;
    for (; n > 0; n--){
        res = res * n;
    }
    return res;
}

 

9. 확장 함수 

기존 클래스에 메서드를 추가할 수 있는 기능.
코틀린의 모든 클래스에 메서드를 추가하려면 최상위 클래스인 Any에 확장 함수를 구현하면 된다.

추가하고 싶은 클래스에 점 표기(.)로 새로운 메서드를 생성할 수 있다.
fun String.getHyunndyString(target : String) : String = 
	if( target == "Hyunndy" ) target else this
    
fun main() {
	val source = "Hello World"
    val target = "Hyunndy"
    
    println(source.getHyunndyString(target))
    println(source.getHyunndyString(Choond))
}

실행 결과
Hyunndy
Hello World

-> String 클래스에 확장 함수 getHyunndyString()을 추가함.