Today I Learned

코틀린에서 구현하는 유창성 (2) 본문

Kotlin/다재다능 코틀린 프로그래밍

코틀린에서 구현하는 유창성 (2)

하이라이터 2023. 10. 19. 23:36
728x90

함수 확장

  • 코틀린의 Function은 andThen() 메소드를 가지고 있지 않지만, 인젝트해서 사용할 수 있다.
fun <T, R, U> ((T) -> R).andThen(next: (R) -> U): (T) -> U =
  { input: T -> next(this(input)) }
fun increment(number: Int): Double = number + 1.toDouble()
fun double(number: Double) = number * 2
                               
val incrementAndDouble = ::increment.andThen(::double)
println(incrementAndDouble(5)) //12.0
  • ::구문을 이용한 increment()의 참조에 andThen() 메소드를 호추랬고, double() 메소드에 참조를 넘겼다.
  • 그 결과 increment()와 double()을 합친 함수가 된다.

 

influx를 이용한 중위 표기법

  • 'if(obj instanceof String)'처럼 연산자가 중간에 있거나 피연산자 사이에 있는 것을 중위표기법이라 한다.
  • Java에서는 정의된 연산자에서만 제한적으로 사용할 수 있지만, 코틀린에서는 코드에 중위표기법을 사용할 수 있다.
  • 연산자는 항상 자동으로 중위표기법을 사용하지만, 메소드는 infix 어노테이션을 사용해야 한다.
operator infix fun Circle.contains(point: Point) =
  (poin.x -cx) * (point.x - cx) + (point.y - cy) * (point.y - cy) < 
  radius * radius
  
println(circle.contains(point1)) //true
println(circle contains point1) //true

 

Any 객체를 이용한 자연스러운 코드

  • 코틀린에는 코드를 간결하고 표현력있게 만들어주는 4가지의 특별한 메소드들이 있다.
  • also(), apply(), let(), run()

4가지 메소드의 동작

val format = "%-10s%-10s%-10s%-10s"
val str = "context"
val result = "RESULT"

fun toString() = "lexical"

println(String.format("%-10s%-10s%-10s%-10s%-10s",
  "Method", "Argument", "Receiver", "Return", "Result"))
println("===============================================")

val result1 = str.let { arg ->                        
  print(String.format(format, "let", arg, this, result))
  result
}
println(String.format("%-10s", result1))

val result2 = str.also { arg ->                        
  print(String.format(format, "also", arg, this, result))
  result
}
println(String.format("%-10s", result2))

val result3 = str.run {
  print(String.format(format, "run", "N/A", this, result))
  result
}
println(String.format("%-10s", result3))

val result4 = str.apply {
  print(String.format(format, "apply", "N/A", this, result))
  result
}
println(String.format("%-10s", result4))
  • 4개의 메소드 모두 전달받은 람다를 실행시킨다.
  • let()과 run()은 람다를 실행시키고 람다의 결과를 호출한 곳으로 리턴한다.
  • also()와 apply()는 람다의 결과를 무시하고 컨텍스트 객체를 호출한 곳으로 리턴한다.
  • run()과 apply()는 run()과 apply()를 호출한 컨텍스트 객체의 실행 컨텍스트를 this로 사용하여 실행시킨다.

 

장황하고 지저분한 코드로부터

class Mailer {
  val details = StringBuilder()
  fun from(addr: String) = details.append("from $addr...\n")
  fun to(addr: String) = details.append("to $addr...\n")
  fun subject(line: String) = details.append("subject $line...\n")
  fun body(message: String) = details.append("body $message...\n")
  fun send() = "...sending...\n$details"
}

val mailer = Mailer()
    mailer.from("builder@agiledeveloper.com")
    mailer.to("venkats@agiledeveloper.com")
    mailer.subject("Your code sucks"
    mailer.body("details")
  
val result=mailer.send()
println(result)
  • 위의 장황한 코드를 4가지 메소드를 사용해서 줄여보자.

 

apply를 이용한 반복참조 제거

val mailer = 
  Mailer()
    .apply { from("builder@agiledeveloper.com") }
    .apply { to("venkats@agiledeveloper.com") }
    .apply { subject("Your code sucks") }
    .apply { body("details") }

val result = mailer.send()

println(result)
  • apply()를 사용해서 다중 호출을 사용할 수 있다.
val mailer = Mailer().apply { 
    from("builder@agiledeveloper.com")
    to("venkats@agiledeveloper.com")
    subject("Your code sucks")
    body("details") 
}

val result = mailer.send()

println(result)
  • apply() 메소드는 apply()를 마지막으로 호출한 객체의 컨텍스트에서 람다를 실행시킨다.
  • 그래서 apply()에 전달하는 람다에서 Mailer에 여러번의 메소드 호출을 사용할 수 있다.

 

run을 이용한 결과 얻기

val result = Mailer().run { 
    from("builder@agiledeveloper.com")
    to("venkats@agiledeveloper.com")
    subject("Your code sucks")
    body("details") 
    send()
}

println(result)
  • 메소드의 연속적인 호출 이후에 인스턴스를 더 이상 쓸 일이 없다면 run()을 사용할 수 있다.
  • apply()와는 다르게 run() 메소드는 람다의 결과를 리턴해준다.

 

let을 이용해 객체를 아규먼트로 넘기기

fun createMailer() = Mailer()

fun prepareAndSend(mailer: Mailer) = mailer.run {
    from("builder@agiledeveloper.com")
    to("venkats@agiledeveloper.com")
    subject("Your code suks")
    body("details")
    send()
  }

val result = createMailer().let {
  prepareAndSend(it)
}

println(result)
  • 함수에서 받은 인스턴스를 다른 메소드의 아규먼트로 전달하고 싶다면 let() 메소드를 사용할 수 있다.
  • createMailer()의 결과는 let()으로 전달되고, let()은 전달받은 파라미터를 다시 prepareAndSend() 메소드에 전달한다.
    그리고 함수가 리턴한 것을 호출자에게 다시 리턴한다.

 

also를 사용한 void 함수 체이닝

fun createMailer() = Mailer()

fun prepareMailer(mailer: Mailer):Unit {
  mailer.run {
    from("builder@agiledeveloper.com")
    to("venkats@agiledeveloper.com")
    subject("Your code suks")
    body("details")
  }
}

fun sendMail(mailer: Mailer): Unit {
  mailer.send()
  println("Mail sent")
}

val mailer = createMailer()
prepareMailer(mailer)
sendMail(mailer)

createMailer()
  .also(::prepareMailer)
  .also(::sendMail)
  • also()를 이용하면 함수를 호출할 때 체이닝을 사용할 수 있다. also()가 타깃 객체를 람다에 파라미터로 전달하고, 람다의 리턴을 무시한 후 타깃을 다시 호출한 곳으로 리턴하기 때문이다.

 

암시적 리시버

리시버 전달

var length = 100

val printIt: (Int) -> Unit = { n: Int ->
  println("n is $n, length is $length")
}

printIt(6) //n is 6, length is 100
  • 먼저 일반적인 람다 표현식을 살펴보자.
  • 이 코드에서 람다 내부의 스코프에 length는 없다. 그래서 컴파일러는 렉시컬 스코프의 변수에서 length를 찾는다.
var length = 100

val printIt: String.(Int) -> Unit = { n: Int ->
  println("n is $n, length is $length")
}
  • 람다의 시그니처를 약간만 바꾸면 람다에 리시버를 세팅할 수 있다.
  • 위 코드는 (Int) -> Unit 대신에 String.(Int) -> Unit을 사용했다.
  • String.(Int) 문법은 람다가 String 인스턴스의 컨텍스트에서 실행된다는 의미를 가지고 있다.
  • 변수의 스코프를 해결할 때 컴파일러는 리시버의 스코프를 먼저 확인하고, 찾을 수 없을 경우에 렉시컬 스코프를 찾게 된다.
printIt("Hello", 6) //n is 6, length is 5
  • 리시버를 사용하는 람다를 호출할 때 추가적인 아규먼트를 전달해야한다. 위와 같이 리시버를 첫 번째 아규먼트로 전달하고, 실제 람다의 파라미터를 두 번째 아규먼트로 사용할 수 있다.
"Hello".printIt(6) //n is 6, length is 5 //n is 6, length is 5
  • 하지만 코틀린에서는 람다를 리시버의 확장 함수처럼 사용하는 방식을 더 선호한다.
  • 출력을 보면 렉시컬 스코프의 값이 아니라 리시버인 String의 length를 보여준다.

 

리시버를 이용한 멀티플 스코프

  • 람다 표현식은 다른 람다 표현식에 중첩될 수 있다. 이때 내부의 람다 표현식은 멀티플 리시버를 가진 것처럼 보인다.
  • 람다 하나는 하나의 리시버만 가질 수 있지만, 중첩 레벨에 따라서 변수에 바인딩하기 위해 멀티플 스코프를 가질 수 있다.
  • 람다를 감싸고 있는게 클래스가 아니라 함수이기 때문에 this@OuterFunctionName 문법을 이용해서 외부 스코프를 참조한다.
fun top(func: String.() -> Unit) = "hello".func()

fun nested(func: Int.() -> Unit) = (-2).func()

top {
  println("In outer lambda $this and $length")
  
  nested {
    println("in inner lambda $this and ${toDouble()}")
    println("from inner through receiver of outer: ${length}")
    println("from inner to outer receiver ${this@top}")
  }
}
  • top() 함수는 "hello"를 리시버로 사용하고, nested는 (-2)를 리시버로 사용한다.
  • 중첩된 람다 표현식에서 toDouble()은 nested() 함수의 리시버인 Int (-2)에서 실행된다.
  • 하지만 Int에는 length 속성이 없기 때문에 length() 함수는 부모의 리시버로 라우팅된다.
  • 명시적으로 부모 리시버를 참조하기 위해서 this@top처럼 @ 문법을 사용한다.
728x90
Comments