[발표] 고차함수 정리
동기들과의 스터디에서 발표할 자료로써 정리해보고자 글을 작성합니다.
복습의 기회로 삼겠습니다.
람다
함수형 프로그래밍의 대표적 장점으로, 익명함수 정의 기법입니다.
아래는 람다의 기본적인 표현식입니다.
1. original format
(인자타입) -> (리턴타입) { 입력값 -> 반환값 }
(Int, Int) -> (Int) { a, b -> a + b }
2. simple format
{ 매개변수: 매개변수타입 -> 함수 본문 }
{ a: Int, b: Int -> a + b }
일반 함수
fun sum(a: Int, b: Int): Int {
return a + b
}
익명 함수 (람다)
{ a: Int, b: Int -> a + b }
람다의 반환은 함수 본문의 마지막 표현식입니다.
람다 정의에서는 일반함수와 다르게 fun 키워드도 없고 함수 이름도 없습니다
람다는 이름이 없으므로 함수명으로 호출할 수 없고, 변수에 대입해 사용할 수도 있습니다.
아래 두 코드의 기능이 같습니다.
a) 선언과 동시에 사용 가능
{ a: Int, b: Int -> a + b } (10, 20)
b) 변수에 대입해 사용 가능
val sum = { a: Int, b: Int -> a + b }
sum(10, 20)
람다의 다양한 표현식
매개변수가 1개인 일반적인 람다
val numbering = { num: Int -> println(num) }
numbering(10)
매개변수가 1개일땐 it으로 참조가 가능합니다.
val some: (Int) -> Unit = { println(it) }
some(10)
하지만 위 아래 차이점을 보시다시피 it은 매개변수의 타입을 식별할 수 있을 때만 가능합니다.
아래와 같은 코드는 it으로 참조가 불가능한데, 매개변수 타입을 알 수 없기 때문입니다.
val some = { println(it) } // X
매개변수가 2개 이상인 람다
매개변수가 두 개 이상일 때 부터는, it으로 참조가 불가능합니다.
val some = { no1: Int, no2: Int -> println(no1 + no2) }
some(10, 20)
람다의 장점으로는
1) 변수에 넣을 수 있고 2) 함수의 파라미터로 넘겨줄 수 있고 3) return 값으로 넘겨줄 수 있습니다.
이 장점들을 활용하면 고차함수를 사용해 볼 수 있습니다.
고차 함수
일반적으로 함수의 매개변수나 반환값은 데이터입니다.
함수를 매개변수나 반환값으로 이용하는 함수를 고차함수라고 합니다.
고차함수가 가능한건, 함수형 프로그래밍에서 함수를 변수에 대입하는게 가능하기 때문입니다.
fun high(arg: (Int) -> Boolean): String {
val result = if(arg(10)) {
"valid"
} else {
"invalid"
}
return "high result : $result"
}
fun main() {
val result = high( {no -> no > 0} )
println(result)
}
1. Int 를 인자로 받고 Boolean을 리턴하는 함수를 arg 에 받아서 사용하겠습니다.
2. high 를 호출할 땐 람다로 해당 함수를 정의해서 넣어줍니다.
람다로 익명함수 구문을 정의했고, 해당 함수를 high 에서 arg로 받아서 호출에 사용하겠다는 방향입니다.
arg의 인자로 넘겨준 10이 no로 가서 실행되는 거에요. 익명함수도 함수, 호출해서 실행하고 돌아 오는 겁니다.
high를 호출할 때 현재 다음과 같이 진행하고 있는데
val result = high( {no -> no > 0} )
아래와 같이 진행할 수 있습니다.
val result = high { no -> no > 0 }
저렇게 이용하기 위해서는 람다가 고차함수의 마지막 인자로 등장해야 합니다.
fun high(arg: (Int) -> Boolean): String {
val result = if(arg(10)) {
"valid"
} else {
"invalid"
}
return "high result : $result"
}
만약 인자가 여러개인 고차함수라면, 다음과 같이 사용하게 됩니다.
fun high(a: Int, b: Int, arg: (Int, Int) -> Int): String {
val result = arg(a, b)
return "high result : $result"
}
이렇게도 사용할 수 있지만
high(3, 4, ({ a, b -> a + b }))
이렇게도 사용할 수 있습니다.
high(3, 4) { a, b -> a + b }
또한 추후에 안드로이드에서 자주 볼 환경으로, 람다를 넘겨주고 싶은데 매개변수를 사용하지 않을 경우는 명시적으로 _ 기호를 통해 나타내줍니다.
nestedScrollView.setOnScrollChangeListener { _: NestedScrollView, scrollX: Int, scrollY: Int, _: Int, _: Int ->
Log.d("ScrollView", "Scrolled to $scrollX, $scrollY")
}
컨트롤러에서 도메인에게 일을 시키듯이, 도메인에서 람다를 받아서 람다 내의 출력 입력 문을 호출시킬 수도 있습니다.
도메인에서 사용하고 싶은 그 함수가 있다면
그것의 입력형 리턴형을, 고차함수의 입력 리턴으로 정해서
컨트롤러에게 넘겨주고, 도메인에서 갖다 쓰는 겁니다.
// InputView
fun readHitDecision(player: Player): Boolean {
println(HEADER_READ_ANSWER.format(player.name))
return when (val decision = readln().lowercase()) {
YES -> true
NO -> false
else -> throw IllegalArgumentException(ERROR_INVALID_FORMAT)
}
}
// Domain
inline fun playPlayers(
readDecision: (Player) -> Boolean,
showHand: (Player) -> Unit,
) {
getPlayers().players.forEach { player ->
playPlayer(readDecision, showHand, player)
}
}
// Controller
private fun play(game: Game) {
game.playPlayers(
{ player -> InputView.readHitDecision(player) },
{ player -> OutputView.showParticipantHand(player) },
)
}
그리고 함수를 인자로 받는, 고차함수에 만약 람다를 넘겨주는 것이 아니라, 이미 정의된 함수를 넘겨줄 것이라면 다음과 같이
:: 기호를 사용합니다.
fun high(a: Int, b: Int, arg: (Int, Int) -> Int): String {
val result = arg(a, b)
return "high result : $result"
}
fun add(a: Int, b: Int): Int {
return a + b
}
high(a, b, ::add)
이는 타입만 잘 맞아준다면, 같은 방식으로 클래스의 생성자를 넣는 것도 가능합니다 (ex. ::Calculator)
inline
고차함수는 성능상 이슈를 생각할 수 있기에, 고차함수에는 inline 키워드를 붙여줍니다.
경우에 따라서 고차함수는 무명 객체가 계속 생성되는 등의 성능상의 이슈가 있습니다.
그래서 해당 코드 자체를 갖다 붙여준다는 느낌 (별칭) 으로 사용하는게 inline이라고 보면 됩니다.
그래서 실제로 우리가 사용하고 있는 정의된 고차함수들은 대부분 inline을 붙이고 있습니다.
단 고차함수의 길이가 길면 컴파일 과정에서 내부적으로 처리하는 과정 자체가 길어지기 때문에
고차함수의 길이가 3 ~ 5 줄 내외 일 때에만 사용하는게 좋다고 합니다.