함수와 함수형 프로그래밍

장똑대·2022년 3월 25일
0

Do it! 코틀린 프로그래밍 [첫째마당, 코틀린 기본 익히기] 학습

✏️1. 함수란?✏️

  • 여러 값을 입력받아 기능을 수행하고 결괏값을 반환하는 코드의 모음
  • 함수를 사용하는 이유? 코드를 재사용할 수 있기 때문

⬇️ 함수의 구조

fun 함수 이름([변수 이름: 자료형]): [반환값의 자료형] {
	표현식 ...
	[return 반환값]
} // 대괄호([]) 안의 내용은 생략 가능

1-1. 함수 호출과 메모리

  • 프로그램이 실행되면 메모리에 프로그램을 위한 공간이 만들어짐
  • 함수의 각 정보는 프레임(Frame)이라는 정보로 스택 메모리의 높은 주소부터 채워지고, 생성된 순서의 반대 순서로 소멸됨

⬇️ 함수 호출의 원리

fun main(){
	val num1 = 10
    val num2 = 3
    val result: Int
    
    result = max(num1, num2)
    println(result)
}

fun max(a:Int, b:Int) = if (a>b) a else b


1-2. 반환값이 없는 함수와 매개변수

📌 반환값이 없는 함수

  • 함수의 반환값이 없을 때 함수의 자료형을 생략하거나 Unit으로 지정 가능
  • Unit은 특수한 객체를 반환함 (void는 아무것도 반환하지 않음)
fun printSum(a: Int, b:Int): Unit { } // (1)
fun printSum(a: Int, b:Int) { } // (2) Unit 생략

-> (2)에서 반환값의 자료형이 생략되었지만, 실제로 반환값의 자료형은 Unit임


📌 가변 인자(Variable Argument)

  • 함수는 하나만 정의해 놓고 여러 개의 인자를 받을 수 있음
  • <vararg> 키워드 사용
fun main(){
	normalVarargs(1, 2, 3, 4)
    normalVarargs(4, 5, 6)
}

fun normalVarargs(vararg counts: Int){ // counts는 Int형의 배열이 됨
	for (num in counts){
    	print("$num ")
    }
    print("\n")
}

✏️2. 함수형 프로그래밍✏️

  • 코틀린은 함수형 프로그래밍과 객체 지향 프로그래밍을 지원하는 다중 패러다임 언어
  • 함수형, 객체지향 프로그래밍은 코드를 간략하게 만들 수 있게 해줌
  • 함수형 프로그래밍의 장점
    • 코드 간략화
    • 테스트나 재사용성이 더 좋아지면서 개발 생산성이 늘어남

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

순수 함수를 작성하여 프로그램의 부작용을 줄이는 프로그래밍 기법으로 람다식과, 고차함수를 사용함

📌 순수 함수 (Pure Function)

  • 부작용이 없는 함수가 함수 외부의 어떤상태도 바꾸지 않는다면 순수함수 라고 부름
  • 스레드에 사용해도 안전하고 코드를 테스트하기에도 쉬움
  • 순수 함수의 조건
    • 같은 인자에 대하여 항상 같은 값을 반환
    • 함수 외부의 어떤 상태도 바꾸지 않음

2-2. 람다식과 고차함수

  • 고차 함수는 인자나 반환값에 함수를 사용해 대단히 유연함
  • 람다식은 많은 코드들을 간략화하고 함수 자체를 인자나 매개변수로 이용할 수 있어 프로그램의 효율성을 높일 수 있음

📌 일반 함수를 인자나 반환값으로 사용하는 고차 함수

fun main(){
	val res1 = sum(3,2)
	val res2 = mul(sum(3,3), 3) // 인자에 함수 사용
}

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

📌 변수에 할당하는 람다식 함수

  • 람다식이 할당된 변수는 함수처럼 사용 가능함
fun main() {
	var result: Int
    val multi = {x: Int, y:Int -> x * y } // 일반 변수에 람다식 할당
    result = multi(10,20) // 람다식이 할당된 변수는 함수처럼 사용 가능
    println(result)
}

📌 매개변수에 람다식 함수를 이용한 고차 함수

fun main() {
	var result: Int
    result = highOrder({x, y -> x + y}, 10, 20) // 람다식을 매개변수와 인자로 사용
    println(result)
}
fun highOrder(sum: (Int, Int) -> Int, a: Int, b: Int): Int {
	return sum(a, b)
}

📌 람다식 자료형 생략

val multi: (Int, Int) -> Int = {x: Int, y: Int -> x * y}
val multi = {x: Int, y: Int -> x * y}
val multi: (Int, Int) -> Int = {x, y -> x * y}
val multi = {x, y -> x * y} // !오류! 자료형 추론 불가

📌 반환 자료형이 없거나 매개변수가 하나일 때 람다식 표현

fun main() {
	val greet: () -> Unit = { } // 인자와 반환값이 없는 람다식 함수
	val square: (Int) -> Int = {x -> x * x} // 매개변수가 하나인 람다식 함수

	greet() // 함수처럼 사용 가능
}

📌 람다식의 매개변수가 1개인 경우

  • 매개변수가 1개인 경우 화살표 표기를 생략하고 $it 으로 대체 가능
fun main() {
	oneParam({ a -> "Hello World! $a"})
    oneParam {"Hello World! $it"}
}

fun oneParam(out: (String) -> string){
	println(out("OneParam"))
}

2-3. 람다식과 고차함수 호출하기

📌 값에 의한 호출

함수가 또 다른 함수의 인자로 전달된 경우 람다식 함수는 값으로 처리되어 그 즉시 함수가 실행된 후 값을 전달


📌 이름에 의한 람다식 호출

이름이 인자로 전달될 때 실행되지 않고 실제로 호출할 때 실행


📌 참조에 의한 호출 (::)

람다식이 아닌 일반 함수를 또 다른 함수의 인자에서 호출할 때 2개의 콜론(::) 기호를 함수 이름앞에 사용한다

⬇️ 인자와 반환값이 있는 함수

fun main() {
	val res1 = funcParam(3, 2, ::sum) // 참조에 의한 호출
    println(res1)
    
    // 일반 변수에 값처럼 할당
    val likeLambda = ::sum
    println(likeLambda(3, 2))
}

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

fun funcParam(a: Int, b: Int, c: (Int, Int) -> Int): Int {
	return c(a, b)
}

⬇️ 인자가 없는 함수

fun main() {
	hello(::text) // 반환값이 없음
    hello({a, b -> text(a, b)}) // 람다식 표현
    hello {a, b -> text(a, b)} //소괄호 생략
    // 위 3가지 표현은 모두 동일한 결과를 출력
}
fun text(a: String, b: String) = "Hi! $a $b"

fun hello(body: (String, String) -> String): Unit {
	println(body("Hello", "World"))
}

✏️3. 코틀린의 다양한 함수✏️

3-1. 익명 함수(Anonymous Function)

  • 이름이 없는 일반 함수
  • 함수 본문 조건식에 따라 함수를 중단하고 반환해야 하는 경우에 사용

⬇️ 익명 함수 예시

fun(x: Int, y: Int): Int = x + y

val add: (Int, Int) -> Int = fun(x, y) = x + y // (1)
val add = fun(x:Int, y:Int) = x + y // (2)
val add = {x: Int, y: Int -> x + y} // (3)

val result = add(10, 2)

-> (1) 처럼 선언 자료형을 람다식 형태로 써 주면 변수 add는 람다식 함수처럼 add() 와 같이 사용할 수 있다
-> (1),(2),(3) 은 모두 동일한 표현


3-2. 인라인 함수(Inline Function)

  • 함수가 호출되는 곳에 함수 본문의 내용을 모두 복사해 넣음
  • 함수의 분기 없이 처리되기 때문에 코드의 성능을 높임
  • 내용은 대게 짧게 작성
  • 인라인 함수는 람다식 매개변수를 가지고 있는 함수에서 동작
  • 인라인 함수의 제한
    • 인라인 함수의 매개변수로 사용한 람다식의 코드가 너무 길거나 인라인 함수의 본문 자체가 너무 길면 컴파일러에서 성능 경고를 할 수 있음
    • 인라인 함수가 너무 많이 호출되면 오히려 코드의 양이 늘어나 좋지 않을 수 있음

⬇️ 인라인 함수 예시

fun main() {
	shortFunc(3) { println("First call: $it") }
    shortFunc(5) { println("First call: $it") }
}

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

코드 상에서는 shortFunc() 함수가 2번 호출되는 것처럼 보이지만

fun main() {
	// shortFunc(3) {  }
	println("Before calling out()")
    println("First call: 3")
    println("After calling out()")
    
    // shortFunc(5) {  }
    println("Before calling out()")
    println("First call: 5")
    println("After calling out()")
}

역컴파일 해보면 shortFunc() 함수의 내용이 복사된 것이다. (위 코드는 역컴파일된 코드가 아님. inline 함수가 어떤식으로 동작하는지 대략적으로 나타냄)


3-3. 확장 함수(Extension Function)

  • 필요로 하는 대상에 함수를 더 추가할 수 있는 개념
  • 기존 클래스의 선언 구현부를 수정하지 않고 외부에서 손쉽게 기능을 확장
  • 동일한 이름의 멤버 함수 혹은 메서드가 존재한다면 항상 확장 함수보다 멤버 메서드가 우선으로 호출

⬇️ 확장 함수 정의

fun 확장 대상.함수 이름(매개변수, ...): 반환값 {
	...
    return}

⬇️ String 클래스에 확장 함수 추가하기

fun main() {
	val source = "Hello World!"
    val target = "Kotlin"
    println(source.getLongString(target))
}

// String 클래스를 확장해 getLongString() 함수 추가
fun String.getLongString(target: String): String = 
	if(this.length > target.length) this else target

3-4. 중위 함수(Infix Notation)

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

⬇️ 중위 표현법 예시

fun main() {
	// 일반 표현법
    val multi = 3.multiply(10)
    
    // 중위 표현법
    val multi = 3 multiply 10 
}

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

3-5. 꼬리 재귀 함수(Tail Recursive Function)

  • 꼬리 재귀 함수를 통해 스택 오버플로 현상을 해결할 수 있음
  • 재귀 함수처럼 스택에 계속 쌓이는 방식이 아닌 꼬리를 무는 형태로 반복
  • tailrec 키워드 사용

✏️4. 함수와 변수의 범위✏️

4-1. 함수의 범위

📌 최상위 함수(Top-level Function)

main() 함수의 앞이나 뒤에 선언해도 main() 함수 안에서 함수를 사용하는데 아무런 제약이 없음

📌 지역 함수(Local Function)

함수 안에 또 다른 함수가 선언되어 있는 경우

⬇️ 최상위 함수와 지역 함수

fun a() = b() // 최상위 함수이므로 b() 함수 선언 위치에 상관없이 사용 가능
fun b() = println("b")

fun c() {
	fun d() = e() // !오류! d()는 지역함수로 e()의 이름을 모름
    fun e() = println("e")
}

fun main() {
	a() //최상위 함수는 어디서든 호출 가능
    e() // !오류! e()는 c의 블록 밖에서 사용 불가능
}

4-2. 변수의 범위

📌 지역 변수(Local Variable)

  • 특정 코드 블록 안에 있는 변수
  • 블록을 벗어나면 프로그램 메모리에서 삭제

📌 전역 변수(Global Variable)

  • 최상위에 있는 변수
  • 프로그램이 실행되는 동안 삭제되지 않고 메모리에 유지
  • 코드가 길어지면 전역 변수에 동시 접근하는 코드는 프로그램의 잘못된 동작을 유발
  • 자주 사용되지 않는 전역 변수는 메모리 자원 낭비를 불러옴
profile
장똑대와 안드로이드

0개의 댓글