코틀린의 표준 라이브러리에는 객체의 컨텍스트 내에서 코드 블록을 실행하기 위해서만 존재하는 몇가지 함수가 포함되어 있는데 이를 스코프 함수라고 부릅니다.
스코프 함수를 제대로 사용하면 불필요한 변수 선언이 없어지며 코드를 더 간결하고 읽기 쉽게 만들어줍니다.
스코프 함수의 코드 블록 내부에서는 변수명을 사용하지 않고도 객체에 접근할 수 있는데 그 이유는 수신자 객체에 접근할 수 있기 때문입니다.
수신자 객체는 람다식 내부에서 사용할 수 있는 객체의 참조입니다.
스코프 함수를 사용하면 수신자 객체에 대한 참조로 this 또는 it을 사용합니다.
코틀린은 총 5개의 유용한 스코프 함수를 제공하며 각 스코프 함수들은 본질적으로 유사한 기능을 제공합니다.
함수명 | 수신자 객체 참조 방법 | 반환 값 | 확장 함수 여부 |
---|---|---|---|
let | it | 함수의 결과 | O |
run | this | 함수의 결과 | O |
with | this | 함수의 결과 | X |
apply | this | 컨텍스트 객체 | O |
also | it | 컨텍스트 객체 | O |
fun main() {
val str: String? = null
str?.let { // 실행 조차 되지 않음!!
println(it)
println("hi")
// 아무것도 출력되지 않음
}
}
val str: String? = "안녕"
str?.let {
println(it)
// 안녕
}
fun main() {
val str: String? = "안녕"
val result = str?.let {
println(it)
1234
// return 1234와 같은 의미이며 return이 생략된 것!!
}
println(result)
// 1234 출력, let 함수 마지막 코드가 결과로 반환
}
let을 쓸때 호출이 중첩되면 코드가 복잡해지므로 이런 경우엔 if를 사용하는 편이 낫습니다.
val str: String? = "안녕"
val result = str?.let {
println(it)
val abc : String? = "abc"
abc?.let {
val def : String? = "def"
def?.let {
println("abcdef가 null이 아님")
}
}
1234
}
println(result)
val str: String? = "안녕"
val result = str?.let {
println(it)
val abc : String? = "abc"
val def : String? = "def"
if (!abc.isNullOrEmpty() && !def.isNullOrEmpty()) {
println("abcdef가 null이 아님")
}
1234
}
println(result)
수신 객체의 프로퍼티를 구성하거나 새로운 결과를 반환하고 싶을때 사용합니다.
class DatabaseClient {
var url: String? = null
var username: String? = null
var password: String? = null
// DB에 접속하고 Boolean 결과를 반환
fun connect(): Boolean {
println("DB 접속 중 ...")
Thread.sleep(1000)
println("DB 접속 완료")
return true
}
}
fun main() {
val config = DatabaseClient()
config.url = "localhost:3306"
config.username = "mysql"
config.password = "1234"
val connected = config.connect()
println(connected)
// DB 접속 중 ...
// DB 접속 완료
// true
}
fun main() {
val connected = DatabaseClient().run {
url = "localhost:3306" // this.url과 같음!! this. 생략
username = "mysql"
password = "1234"
connect()
}
println(connected)
// DB 접속 중 ...
// DB 접속 완료
// true
}
위와 같이 run을 사용하면 불필요한 변수 선언을 할 필요가 없고 중복되는 코드의 양이 줄어드는 효과가 있습니다.
확장 함수 형태로 실행을 하다보니 this 키워드로 수신자 객체를 참조 할 수 있고 생략도 가능한 것을 볼 수 있습니다.
val connected = DatabaseClient().let {
it.url = "localhost:3306"
it.username = "mysql"
it.password = "1234"
it.connect()
}
println(connected)
다른 스코프 함수와 다른 점은 with는 확장 함수가 아닙니다.
val str = "안녕하세요"
with (str) {
println("length = $length") // this.length에서 this. 생략!!
}
val str = "안녕하세요"
val length = with(str) {
length // this.length에서 this. 생략!!
}
println(length) // 5
fun main() {
val connected = with(DatabaseClient()) {
url = "localhost:3306"
username = "mysql"
password = "1234"
connect()
}
println(connected)
}
수신 객체의 프로퍼티를 구성하고 수신 객체를 그대로 결과로 반환하고 싶을때 사용합니다.
//반환 타입이 수신 객체
val client: DatabaseClient = DatabaseClient().apply {
url = "localhost:3306"
username = "mysql"
password = "1234"
connect()
}
앞서 공부한 let, run, with는 함수의 결과가 반환타입으로 변환되는데 반해서 apply는 수신 객체 그대로 반환됩니다.
부수 작업을 수행하고 전달받은 수신 객체를 그대로 결과로 반환하고 싶을때 사용합니다.
class User(val name: String, val password: String) {
fun validate() {
if (name.isNotEmpty() && password.isNotEmpty()) {
println("검증 성공!")
} else {
println("검증 실패!")
}
}
}
fun main() {
val user: User = User(name = "tony", password = "1234")
user.validate()
}
fun main() {
User(name = "tony", password = "1234").also {
it.validate()
}
}
이와 같은 스코프 함수는 모두 기능이 유사하기 때문에 실무에선 섞어쓰는 경우도 많습니다.
"this는 키워드", 키워드는 사전에 정의된 예약어이기 때문에 다른 의미로 사용할 수 없지만 it은 특정 용도에서만 작동하는 소프트 키워드이기 때문에 다른 용도로 사용할 수 있습니다.
val this: String? = null // this라는 변수명을 가질 수 없음!!, 컴파일 오류
val it: String? = null // it은 변수명을 가질 수 있음!!, 작동
중첩으로 사용하는 경우 this, it에 대해 혼동하기 쉽습니다.
fun main() {
val hello = "hello"
val hi = "hi"
hello.let {
println(it.length)
hi.let {
println(it.length)
}
}
}
fun main() {
val hello = "hello"
val hi = "hi"
hello.let { a ->
println(a.length)
hi.let { b ->
println(a.length)
println(b.length)
}
}
}
함수형 프로그래밍,FP(Functional-Programming)는 수학의 함수적 개념을 참고해 만든 패러다임의 하나로 깔끔하고 유지보수가 용이한 소프트웨어를 만들기 위해 사용합니다.
함수형 패러다임은 부수효과가 없고 똑같은 input이 들어오면 항상 동일한 output을 내놓는 순수함수의 개념을 기반으로 람다, 고차함수, 커리, 메모이제이션, 모나드 등의 개념을 포함합니다.
함수형 프로그래밍에서 함수는 1급 객체로 분류됩니다.
1급 객체란 함수를 인자에 넘기거나 변수에 대입하거나 함수를 반환하는 개념 등을 통틀어 1급 객체라고 합니다.
val func: () -> Unit = {}
함수는 값이 될 수 있고 역으로 값은 함수가 될 수 있습니다.
그러므로 함수에 인자로 넘기거나 데이터 구조에 저장할 수 있습니다.
val printHello: () -> Unit = { println("Hello") }
fun main() {
val list = mutableListOf(printHello)
list[0]() // 함수 호출을 위해 () 사용
// Hello
}
val list = mutableListOf(printHello)
val func = list[0]
func()
// Hello
fun call(block: () -> Unit) {
block()
}
fun main() {
call(printHello)
// Hello
}
fun printNo() {
println("No!")
}
fun main() {
val list = mutableListOf(printNo) // 컴파일 오류
}
val printMessage : (String) -> Unit = { message : String -> println(message) }
// 타입 생략
val printMessage : (String) -> Unit = { message -> println(message) }
// it 사용
val printMessage : (String) -> Unit = { println(it) }
val plus: (Int, Int) -> Int = { a, b -> a + b }
fun main() {
val result = plus(1, 3)
println(result)
// 4
}
고차 함수는 함수를 인자로 받거나 결과로 돌려주는 함수를 의미합니다.
컬렉션의 filter, map, forEach 등도 함수를 인자로 받아서 결과를 반환하므로 고차함수입니다.
// collection과 function(함수)를 인자로 받음
fun forEachStr(collection: Collection<String>, action: (String) -> Unit) {
for (item in collection) {
action(item)
}
// a
// b
// c
}
fun main() {
val list = listOf("a", "b", "c")
val printStr: (String) -> Unit = { println(it) }
forEachStr(list, printStr)
}
함수형 프로그래밍에선 이름 없는 무명 함수를 익명함수라고 합니다.
fun outerFunc(): () -> Unit {
return fun() { // 변수명이 없다!!
println("이것은 익명함수!")
}
}
fun main() {
outerFunc()()
// 이것은 익명함수!
}
fun outerFunc(): () -> Unit {
return { println("이것은 람다함수!") }
}
fun outerFunc(): () -> Unit = { println("이것은 람다함수!") }
함수의 마지막 인자가 함수인 경우 후행 람다 전달을 사용할 수 있습니다.
fun forEachStr(collection: Collection<String>, action: (String) -> Unit) {
for (item in collection) {
action(item)
}
}
fun main() {
val list = listOf("a", "b", "c")
// action(funtion, 함수)을 매개볏수로 넘기지 않고 후행 람다 전달을 사용
forEachStr(list) {
println(it)
}
}
람다 함수의 파라미터가 1개인 경우에만 it 을 사용할 수 있습니다.
fun arg1(block: (String) -> Unit) {}
fun arg2(block: (String, String) -> Unit) {}
fun main() {
arg1 {
it.length
}
arg2 {
it.length // 컴파일 오류
it.length // 컴파일 오류
}
arg2 { a, b ->
a.length
b.length
}
}
람다 레퍼런스를 사용하면 좀 더 가독성 좋게 함수를 인자로 넘길 수 있습니다.
탑-레벨 또는 로컬 함수는 (::함수명)을 사용하고 클래스의 멤버이거나 확장 함수는 클래스 지시자를 사용합니다. (클래스명::함수명)
val callReference: () -> Unit = { printHello() }
callReference()
// Hello
val callReference = ::printHello
callReference()()
// Hello
val numberList = listOf("1", "2", "3")
numberList.map { it.toInt() }.forEach { println(it) }
numberList.map(String::toInt).forEach(::println)
직접 함수를 제작하는 방법도 있지만 고급 기능은 이미 만들어져 있고 검증된 라이브러리를 사용하는 것이 좋습니다.
애로우는 코틀린에서 함수형 프로그래밍을 위한 라이브러리입니다.
애로우는 함수형 프로그래밍의 다양한 개념 및 기능들을 제공하고 있고 점점 사용사례가 많아지고 있습니다.
출처 : fastcampus