반응형
본 포스트는 지인들과 스터디한 내용을 정리한 포스트입니다
http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788931463422
1. 함수형 스타일
- 함수형 스타일은 선언적 스타일에서 태어났다
- 선언적 스타일은 개발자가 원하는 곳을 말하면 호출한 함수가 원하는 것을 처리해준다
- 선언적 스타일에서는 레이어 아래에 캡슐화 되어있다
- 개발자는 중요하다고 생각하고 확인해 볼 필요가 있다면 디테일을 확인할 수 있고, 그게 아니면 더 이상 개발자를 귀찮게 하지 않는다 ( 확인할 필요가 없다 )
- 명령형 스타일 vs 함수형 스타일
//doubleofeven.kts // 명령형 스타일 var doubleOfEven1 = mutableListOf<Int>() for (i in 1..10) { if (i % 2 == 0) { doubleOfEven1.add(i * 2) } } // 함수형 스타일 var doubleOfEven2 = (1..10) .filter { e -> e % 2 == 0 } .map { e -> e * 2 }
- 명령형 스타일은 익숙하다. 하지만 복잡하다. 익숙함 때문에 쓰기는 쉽지만 읽기가 매우 어렵다
- 함수형 스타일은 좀 덜 친숙하다. 하지만 단순하다. 익숙하지 않기 때문에 작성하기는 어렵지만, 읽기가 쉽다
- 하지만 매번 함수형을 쓰는 게 명령형을 사용하는 것보다 좋은 것은 아니다
- 함수형 스타일은 코드가 연산에 집중하고 있을 때 써야 한다(변경가능성과 부작용 회피 가능)
2. 람다 표현식
- 람다는 고차함수에 아규먼트로 사용되는 짧은 함수이다
- 함수에 데이터를 전달하는 대신 람다를 이용해서 실행 가능한 코드를 함수에 전달할 수 있다
- 람다의 구조
- 람다 표현식은 이름이 없고 타입추론을 이용한 리턴 타입을 가지는 함수이다
- 파라미터 리스트와 바디 두가지만 가지고 있다
- 람다를 함수의 아규먼트 리스트로 전달할 때, 마지막 아규먼트로 전달하는 것이 좋다 ( 가독성 때문에 )
- 람다 전달
- 예제코드
// lambdaexample.kts fun isPrime(n: Int) = n > 1 && (2 until n).none { i: Int -> n % i == 0 }
- none() 함수를 주목하자
- none() 함수로 전달된 람다의 파라미터 리스트는 파라미터 i의 타입을 Int로 지정한다
- 코틀린이 파라미터에는 타입추론을 적용하지 않기 때문에 함수에 전달되는 각 파라미터의 타입이 필요하다
- 하지만 람다의 파라미터에는 타입을 필요로 하지 않는다 ( 메소드의 시그니처에서 추론 가능하다 )
- 예제코드
- 암시적 파라미터 사용
- 람다가 하나의 파라미터만 받는다면 파라미터 정의를 생략하고 특정 이름(it)을 부여하여 암시적 파라미터를 사용할 수 있다
- 예제코드
// lambdaexample.kts fun isPrime2(n: Int) = n > 1 && (2 until n).none { n % it == 0 }
- "it"를 이용하면 기존에 파라미터 정의를 없앨 수 있다
- 단 람다가 파라미터가 없는 람다인지 아니면 it를 사용하여 파라미터 하나를 취하는 람다인지 빠르게 파악하기 어렵다 ( 여러 줄로 이루어진 람다에서는 유지보수가 힘들기 때문에 사용을 자제해야 한다 )
- 람다 받기
- 여러 방식의 표현
// lambdaiterate.kts fun walk1To(n: Int, action: (Int) -> Unit) = (1..n).forEach { action(it) } walk1To(5, { i -> print(i) }) // 12345 walk1To(5) { i -> print(i) } // 12345 walk1To(5) { print(it) } // 12345 walk1To(5) { ::println } // 메서드 참조
- 메서드 참조
- 입력받는 파라미터와 전달되는 람다내부의 함수 파라미터와 일치하다면 '::'를 이용해서 메서드 참조를 이용할 수 있다
- 여러 방식의 표현
3. 람다와 익명함수
- 람다의 중복코드를 피하기 위해서는 2가지 방법이 있다
- 재사용을 위해서 람다를 변수에 담는 것
- 람다 대신 익명 하수를 사용하는 것
- 예제코드
// savelambdas.kts val names = listOf("Pam", "Pat", "Paul", "Paula") val checkLength5 = { name: String -> name.length == 5 } // (String) -> Boolean 타입추론 val checkLength6: (String) -> Boolean = { name -> name.length == 5 } val checkLength7 = fun(name: String): Boolean { return name.length == 5 } // 익명함수
4. 클로저와 렉시컬 스코핑
- 람다에는 상태가 없다
- 외부 상태에 의존하여 람다 내부에서 사용하고 싶은 경우가 있다 ( 클로저 )
- 람다는 스코프를 로컬이 아닌 속성과 메소드로 확장할 수 있기 때문
// clousures.kts val factor = 2 val doubleIt = { e: Int -> e * factor } println(doubleIt(3)) // 6
- doubleIt 메서드, 람다 시그니처를 정의하고 있는 해당 바디 부분에는 변수 또는 속성인 factor가 없다
- 컴파일러는 factor 변수에 대한 클로저의 범위(스코프)에서 참조되는 변수를 찾지 못한다면 점차 스코프를 확장시킨다 ( 렉시컬 스코핑 )
- 리스트 vs 시퀀스
// mutable.kts var factor = 2 val doubled = listOf(1, 2).map { it * factor } val doubledAlso = sequenceOf(1, 2).map { it * factor } factor = 0 doubled.forEach { println(it) } // 2, 4 doubledAlso.forEach { println(it) } // 0, 0
- 자세한 내용은 Chapter 11에서 다룰 예정
5. 비지역성(non-local)과 라벨(labeled) 리턴
- 람다는 리턴값이 있더라도 return 키워드를 가질 수 없다 ( 익명 함수와 차이점 )
- 람다 내부의 블록에서 return이 의미하는 바는 모호하다
- 즉시 람다에서 빠져나와 나머지 로직을 수행하는 것인지
- 람다를 호출하고 있는 부분에서 아에 빠져나오는 것인지
- 라벨 리턴
- 예제코드
//noreturn.kts fun invokeWith(n: Int, action: (Int) -> Unit) { println("enter invokeWith $n") action(n) println("exit invokeWith $n") } fun caller() { (1..3).forEach { i -> invokeWith(i) here@ { println("enter for $it") if (it == 2) { return@here } println("exit for $it") } } println("end of caller") } caller() println("after return from caller") //enter invokeWith 1 //enter for 1 //exit for 1 //exit invokeWith 1 //enter invokeWith 2 //enter for 2 //exit invokeWith 2 //enter invokeWith 3 //enter for 3 //exit for 3 //exit invokeWith 3 //end of caller //after return from caller
- 라벨리턴은 람다의 흐름을 제어해서 라벨 블록으로 점프하기 위해서 만들어졌다 ( 람다를 빠져나가기 위함 )
- 명령형 스타일 프로그래밍에서 반복문을 사용할 때 이후 과정을 생략하고 반복문의 마지막으로 가기 위해 사용하던 continue와 동일하다
- @here: 명시적인 라벨
- @{람다가 전달된 함수의 이름}: 암시적인 라벨
- 명시적 라벨이 의도를 명확하게 보이게 하고 코드를 쉽게 이해할 수 있게 도와주므로 명시적 라벨 사용을 권장
- 예제코드
- 정리
- return은 람다에서 기본적으로 허용이 안된다
- 라벨 리턴(@ 키워드)을 사용하면 현재 동작중인 람다를 스킵할 수 있다
- 논로컬 리턴을 사용하면 현재 동작중인 람다를 선언한 곳 바깥으로 나갈 수 있다. 하지만 람다를 받는 함수가 inline으로 선언된 경우만 사용 가능하다
- 람다를 빠져나가기 위해서 언제든지 라벨 리턴을 사용할 수 있다
- return을 사용할 수 있다면 람다에서 빠져나가거나 람다를 호출한 곳을 빠져나가는 것이 아니라 람다가 정의된 곳에서 빠져나간다는 사실을 기억해야한다
6. 람다를 이용한 인라인 함수
- 람다는 퍼포먼스를 신경써야한다
- 코틀린은 람다를 사용할 때 호출 오버헤드를 제거하고 성능을 향상시키기 위해서 inline 키워드를 제공한다
- 예제코드
// noinline.kts import kotlin.RuntimeException fun invokeTow( n: Int, action1: (Int) -> Unit, action2: (Int) -> Unit ): (Int) -> Unit { println("enter invokeTwo $n") action1(n) action2(n) println("exit invokeTwo $n") return { _: Int -> println("lambda returned from invokeTwo") } } fun report(n: Int) { println("") print("called with $n, ") val stackTrace = RuntimeException().stackTrace println("Stack depth: ${stackTrace.size}") println("Partial listing of the stack:") stackTrace.take(3).forEach(::println) } fun callInvokeTwo() { invokeTow(1, { i -> report(i) }, { i -> report(i) }) } callInvokeTwo() //enter invokeTwo 1 //called with 1, Stack depth: 31 //Partial listing of the stack: //Noinline.report(noinline.kts:18) //Noinline$callInvokeTwo$1.invoke(noinline.kts:26) //Noinline$callInvokeTwo$1.invoke(noinline.kts:26) // //called with 1, Stack depth: 31 //Partial listing of the stack: //Noinline.report(noinline.kts:18) //Noinline$callInvokeTwo$2.invoke(noinline.kts:26) //Noinline$callInvokeTwo$2.invoke(noinline.kts:26) //exit invokeTwo 1
- 인라인 최적화
- inline 키워드를 이용해서 람다를 받는 함수의 성능을 향상시킬 수 있다
- 함수가 inline으로 선언되어있으면 함수를 호출하는 대신 함수의 바이트코드가 함수를 호출하는 위치에 들어가게 된다
- 함수 호출의 오버헤드를 제거할 수 있지만 함수가 호출되는 모든 부분에 바이트코드가 위치하기 때문에 바이트코드가 커지게 된다 ( 일반적으로 긴 함수를 인라인으로 사용하는건 좋은 생각이 아니다 )
- 리팩토링 코드
// inline.kts import kotlin.RuntimeException inline fun invokeTow( n: Int, action1: (Int) -> Unit, action2: (Int) -> Unit ): (Int) -> Unit { println("enter invokeTwo $n") action1(n) action2(n) println("exit invokeTwo $n") return { _: Int -> println("lambda returned from invokeTwo") } } fun report(n: Int) { println("") print("called with $n, ") val stackTrace = RuntimeException().stackTrace println("Stack depth: ${stackTrace.size}") println("Partial listing of the stack:") stackTrace.take(3).forEach(::println) } fun callInvokeTwo() { invokeTow(1, { i -> report(i) }, { i -> report(i) }) } callInvokeTwo() //enter invokeTwo 1 // //called with 1, Stack depth: 28 //Partial listing of the stack: //Noinline.report(noinline.kts:18) //Noinline.callInvokeTwo(noinline.kts:26) //Noinline.<init>(noinline.kts:28) // //called with 1, Stack depth: 28 //Partial listing of the stack: //Noinline.report(noinline.kts:18) //Noinline.callInvokeTwo(noinline.kts:26) //Noinline.<init>(noinline.kts:28) //exit invokeTwo 1
- 기존 소스와 비교해보면 Stack depth가 31 -> 28로 줄었다
- inline이 될 함수가 매우 크거나, 많은 곳에서 호출한다면 해당 부분마다 바이트코드로 들어가기 때문에 사용하지 않을때에 비해 바이트 코드가 많아지게 된다 ( 주의사항 )
- 선택적 noinline 파라미터
- 어떤 이유로 람다 호출을 최적화하지 않을 수 있다
- 람다의 파라미터를 noinline으로 표시하여 최적화를 제거할 수 있다
- noinline 키워드는 함수가 inline인 경우에만 파라미터에 사용할 수 있다
- 정리
- 라벨이 없는 리턴은 항상 함수에서 발생하며 람다에서는 발생하지 않는다
- 라벨이 없는 리턴은 인라인이 아닌 람다에서 허용되지 않는다
- 함수명은 라벨의 기본 값이 되지만 이를 맹신해서는 안된다.
- 라벨 리턴을 사용할 거라면 항상 라벨명을 지어야 한다
- 일반적으로 코드 최적화를 하기 전에 성능 측정을 먼저 하라. 특히 람다를 사용하는 코드라면 성능 측정을 먼저 해야한다
- inline은 눈에 띄는 성능 향상이 있을 때만 사용하라
728x90
반응형
'Study > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글
Day 9. 델리게이션을 통한 확장 (0) | 2021.11.27 |
---|---|
Day 8. 클래스 계층과 상속 (0) | 2021.11.16 |
Day 7. 객체와 클래스 (0) | 2021.11.12 |
Day 6. 오류를 예방하는 타입 안정성 (0) | 2021.11.02 |
Day 5. 컬렉션 사용하기 (0) | 2021.10.31 |