코딩의 간결성과 직관적 가독성을 위해 프로그램 작성법이 조금씩 변천되어 왔는데,
특히 함수 표현에 있어 획기적이라고 할 수 있는 표현법이 익명함수와 람다식 입니다.
그리고 프로그래밍 기법에 있어서도 특히 비동기 프로그래밍을 위해
고차함수, 콜백함수들이 도입되었습니다.
여기에서 개선됐다는 기준은 80, 90년대의 프로그램 기법에서 개선됐다는 말입니다. ;;
그러니까 이 기술들에 대한 지원이 시작된 것은 2000년대 부터 였던 것 같네요.
그 이후 지금까지 커다란 변화는 없는 것 같습니다.
각 용어와 표현 방법에 대해 간략하고 쉽게 정리해봤습니다.
람다식
함수를 간결화한 형태.
익명 클래스나 익명 함수를 간결하게 표현할 수 있어 편리함.
한편 코드가 간결해져서 좋긴하지만, 남발할 경우 가독성이 떨어져 디버깅이 힘들어 질 수도 있음.
아래 세 가지 표현은 모두 같은 의미임.
① 일반 함수
fun add(x: Int, y: Int) : Int { // 함수 선언부 (2개의 int형 인자를 받아 int형 값을 리턴)
return x + y // 함수의 내용 본체
}
val sum: Int = add(3, 4)
val sum = add(3, 4) // 이렇게 줄임 표현도 가능
② 문법적으로 허용된 생략을 한 일반 함수
fun add(x: Int, y: Int) = x + y // 함수 선언부에 인자 목록은 명시. 리턴 타입은 생략
// 함수 본체의 제일 마직막 표현식에 따라 자동으로 리턴 타입이 추론됨
val sum = add(3, 4)
③ 람다식
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y } // '{ 인수들 -> 함수본체 }' 부분이 람다식임
val sum: (Int, Int) -> Int = {x, y -> x + y } // 이렇게 줄여 표현 하든
var sum = {x: Int, y: Int -> x + y } // 이렇게 줄여 표현하든 모두 가능
// (전달인자든 람다식 안에서든 데이터형 명확히 지정됐으므로)
// 그리고 람다식이 저장된 변수 sum은 일반 함수처럼 호출이 가능함
sum(1,2) // 결과: 3
고차 함수 (High-order Function)
함수의 전달인자나 반환 값의 데이터형에 함수가 포함된 형태.
fun printResult( s: String, r: Float, ff: (num: Float) -> Float) { // *1
val result = ff(r)
println("$s 는 $result")
}
printResult("원의 넓이", 3.0f, {x -> 3.14f * x*x }) // *2
printResult("구의 체적", 3.0f, {x -> 4.0f/3.0f * 3.14f * x*x*x }) // *3
실행 결과)
원의 넓이 는 28.26
구의 체적 는 113.04001
*1 함수의 전달인자를 표시하는 방식은,
문자 : 전후에 함수내에서 사용할 전달인자 이름, 그 전달인자의 데이터형을 명기한 형태입니다.
결국 전달인자로 사용된 함수 부분은,
함수명은 ff, 데이터형은 (num: Float) -> Float) 라고 해석할 수 있습니다.
(num: Float) -> Float)
이 부분이 람다식으로 표현되어 있네요.
Float 형 인자 하나를 받고 Float 형으로 리턴하는 형태의 함수가
전달인자로 사용될 수 있음을 의미합니다.
*2, *3
그림으로 좀 더 쉽게 설명을 해 보자면,...
①,② 각각의 함수가 *1의 전달인자 선언을 통해 printResult()내에서 result 값을 계산하는데 사용되었습니다.
참고로 ①,②는 호출한 함수 printResult에 의해 거꾸로 호출되는데, 이와 같은 함수를 '콜백함수'라고 합니다.
한편, 이번 예에서는 이 함수들을 이름 없이 (이름이 없으므로 '익명(Anonymous) 함수'라고 부릅니다)
함수 본체만을 람다식 형태로 전달했지만, 여러 부분에서 이 함수들을 반복해서 참조한다면
이름을 갖는 함수를 정식으로 정의하고 그 함수명을 전달해줘도 됩니다.
참고) 람다식을 사용할 때 몇몇 표현의 융통성
*2, *3의 표현은 (눈치를 챘는지 모르겠지만) 아래의 표현에서 전달인자 데이터형을 생략한 것 입니다.
printResult("원의 넓이", 3.0f, {x: Int -> 3.14f * x*x })
// (데이터형이 printResult()선언부에 이미 작성되어 있으므로 생략 가능)
*2, *3처럼 함수를 호출할 때 제일 마지막 전달인자가 람다식인 경우,
람다식을 ()밖에 적어도 됩니다.
printResult("원의 넓이", 3.0f) {x -> 3.14f * x*x } // 람다식만 밖으로 꺼내 작성했음
*2, *3처럼 심지어 람다식의 전달인자가 단 하나 뿐이라면, 람다식의 전달인자를 아예 생략해도 됩니다.
그리고 그 전달인자는 키워드 it으로 참조할 수 있습니다.
printResult("원의 넓이", 3.0f) {3.14f * it*it }
fun sum( x: Int, y: Int, prt: (num: Int) -> Unit) { // Unit는 리턴형이 없다는 의미임(void와 같음)
val total: Int
total = x + y
prt( total )
}
sum(3,4) {println(it)} // 결과: 7 (람다식의 표현이 거의 깡패 수준이죠?...)
뿐만아니라, 고차함수의 전달인자가 람다식 하나 뿐이라면, 고차함수를 호출할 때 ()를 생략할 수도 있습니다.
fun sum(prt: (num: Int) -> Unit) { // 전달인자가 람다식 하나 뿐이면,
prt(100)
}
sum() {print(it)}
sum {print(it)} // 호출 때 빈 ()를 생략 가능함
※ 콜백함수의 활용
비동기 프로그래밍에서 작업시간이 오래 걸리는 함수에게 콜백함수를 전달해주고,
메인 프로그램은 다음 동작을 계속 진행.
예)
fun longTimeJob( ..., callback() ) {
(오래걸리는 작업 - DB, 통신, 사용자이벤트, ...)
callback() // 오래걸리는 작업 완료
}
메인프로그램작업1
longTimeJob( ..., cBackFun() ) // 콜백함수를 넣어 longTimeJob() 호출 후 다음 작업 계속
메인프로그램작업2
...
...
참고로, 이런 활용이 UI등에서 사용자 응답 이벤트를 다루는 경우에 이뤄진다면,
언제 발생할 지 모르는 응답에 대해 대기를 해야 하는 '이벤트 리스너' 함수에게
그 응답을 처리할 작업을 담고 있는 '이벤트 핸들러(= 이벤트 처리기)' 함수(콜백함수)를 전달하게 됩니다.
말하자면 다음과 같은 모양이 됩니다.
fun 리스너(핸들러) {
(센서등 응답 대기...)
핸들러()
}
메인프로그램작업1
리스너(핸들러)
메인프로그램작업2
...
...
읽을 거리) 그럼 함수의 전달인자로 함수를 받을 수 없었던 예전에는 버튼 이벤트를 어떻게 처리했나?
(함수가 객체는 받을 수 있었다. 자바에서 변수는 곧 객체)
이벤트 핸들러 구현용 인터페이스를 개발팀 공용으로 만들어 놓고,
각 팀원들이 그 인터페이스를 구현한 자신들만의 클래스를 만들고 객체화하여 리스너 함수에 전달함으로써
콜백함수를 구현했습니다.
아래 버튼 이벤트 처리의 경우, 인터페이스의 추상메서드 onClick()을 구현한 이벤트 핸들러 객체
'new onClickListener()'를 전달하고 있습니다.
Button button = findViewID(R.id.button)
button.setOnClickListener( new onClickListener() {
@override
public void onClick(View view) {
// 이벤트 처리
}
}
간략화된 표현식에 대해서는 오히려 혼동을 주는 경우도 있을 수 있습니다.
각자 편한대로 코딩하면 됩니다. 그래도 내용은 알고 있는 게 좋겠죠. 다른 프로그램을 해독하려면요.
그럼, 이만~