어제 하루종일 쓴 게 날아갔다.. 😂 덕분에 함수형 프로그래밍 제대로 익혔다 .. !! 🤪
함수 : 여러 값(인자)을 입력받아 기능을 수행하고 결괏값을 반환하는 코드의 모음이다.
fun sum(a: Int, b: Int): Int {
var sum = a + b
return sum
}
1️⃣
fun 키워드로 함수 선언 시작하기
fun
키워드로 시작한다.2️⃣
함수 이름 짓기 sum
3️⃣
매개변수 정의하기 (a: Int, b: Int)
4️⃣
반환값의 자료형 명시하기 : Int
5️⃣
함수의 본문 완성하기 var sum = a + b
6️⃣
값 반환하기 return sum
return
키워드를 사용한다.📍 대괄호로 감싼 부분은 생략 가능
fun 함수이름([변수 이름: 자료형, 변수 이름2: 자료형]): [반환값의 자료형]{
표현식
...
[return 반환값]
}
📍 덧셈 함수 간략하게 만들기
fun sum(a: Int, b: Int) = a + b
1️⃣
중괄호 안의 코드가 한 줄이면 대입연산자(=)를 사용하여 간결하게 표현이 가능하다.
2️⃣
매개변수에 자료형이 반환값의 자료형과 같으면 생략가능하다.
💥 코드의 양을 줄일 수 있고 읽기에도 좋아 실무에서 많이 사용한다.
fun main() {
var result1: Int = sum(3, 2)
var result2: Int = sum(6, 7)
println(result1)
println(result2)
}
fun sum(a: Int, b: Int): Int {
var sum = a + b
return sum
}
1️⃣
main()
함수 실행 2️⃣
sum에 인자 전달 3️⃣
sum()
함수 호출
📫 인자와 매개변수 차이
매개변수 : 함수를 선언할 때fun sum(a: Int, b: Int)
인자 : 함수를 호출할 때sum(3, 2)
fun main() { // 최초의 스택 프레임
var num1 = 10 // 임시변수 or 지역변수
var num2 = 3 // 임시변수 or 지역변수
var result: Int
result = max(num1, num2) // 두번째 스택 프레임
}
fun max(a: Int, b: Int) = if (a > b) a else b // a,b 는 max()의 임시 변수
📍 함수를 호출 ❶ 스택에 스택 프레임 생성 ❷ 생성한 순서의 반대로 소멸
Unit : 코틀린에서 다루는 특수한 자료형 중 하나로 반환값이 없을 때 사용한다.
: Unit
은 생략가능Unit
으로 추론한다. fun printSum(a: Int, b: Int): Unit {
println("sum of $a and $b is ${a + b}")
}
📫 Unit와 void 차이 - Unit은 자바의 void와 대응
- Unit : 특수한 객체를 반환한다.
- void : 정말로 아무것도 반환하지 않는다.
fun main() {
val name = "지묵"
val email = "hong@example.kr"
add(name)
add(name, email)
add("둘리", "dooly@example.kr")
defaultArgs() // 인자 없이 호출
defaultArgs(200) // 인자를 하나만 호출
}
fun add(a: String, b: String = "default") {
val output = "${a}님의 이메일 : ${b}"
println(output)
}
fun defaultArgs(x: Int = 100, y: Int = 200) {
println(x + y)
}
출력 :
지묵님의 이메일 : default
지묵님의 이메일 : hong@example.kr
둘리님의 이메일 : dooly@example.kr
300
400
fun main() {
namedParam(x = 200, z = 100)
namedParam(z = 150)
}
fun namedParam(x: Int = 100, y: Int = 200, z: Int) {
println(x + y + z)
}
출력 : 500 450
vararg
키워드 붙이면 된다. fun main() {
normalVararg(1, 2, 3, 4)
normalVararg(4, 5, 6)
}
fun normalVararg(vararg counts: Int) {
for (num in counts) {
print("$num ")
}
print("\n")
}
출력 :
1 2 3 4
4 5 6
📫 다중 패러다임 언어란?
다중 패러다임 언어 : 한 가지 구현 규칙에 얽매이지 않고 다양한 문법과 형식을 지원하는 언어
함수형 프로그래밍 : 순수 함수를 작성하여 프로그램의 부작용을 줄이는 프로그래밍 기법
fun sum(a: Int, b: Int): Int {
return a + b // 동일한 인자인 a,b를 입력받아 항상 a+b 를 출력 -> 부작용이 없다.
}
📍 순수함수가 아닌 함수의 예
fun check() {
val test = User.grade() // check에 없는 외부 함수 사용
if (test != null) process(test) // test는 User.grade()의 실행 결과에 따라 달라짐
}
{ x, y -> x+y } // 람다식의 예 (이름이 없는 함수 형태)
함수형 프로그래밍에서는 함수를 일급 객체라 생각한다.
람다식은 일급 객체의 특징을 가진다.
함수가 일급 객체면 일급 함수라 부른다.
일급 함수에 이름이 없는 경우 람다식 함수
, 람다식
이라고 부를 수 있다.
📫 람다식은 일급 객체의 특징을 가진 이름없는 함수이다.
고차함수 : 다른 함수를 인자로 사용하거나 함수를 결괏값으로 반환하는 함수
fun main() {
println(highFunc({ x, y -> x + y }, 10, 20))
}
fun highFunc(sum: (Int, Int) -> Int, a: Int, b: Int): Int = sum(a, b)
sum: (Int, Int) -> Int
highFun
은 람다식 함수를 인자로 받아들일 수 있는 고차함수가 된다.📫 함수형 프로그래밍의 정의와 특징
- 순수 함수를 사용해야 한다.
- 람다식을 사용할 수 있다.
- 고차 함수를 사용할 수 있다.
고차함수 : 인자나 반환값으로 함수를 사용한다.
📝 함수의 인자로 함수를 사용 mul(sum(3, 3), 3)
fun main() {
val result1 = sum(3, 2)
val result2 = mul(sum(3, 3), 3)
println("result1 : $result1, result2 : $result2")
}
fun sum(a: Int, b: Int) = a + b
fun mul(a: Int, b: Int) = a * b
출력 : result1 : 5, result2 : 18
📝 함수를 반환값으로 사용 return sum(2, 2)
fun main() {
println("funFun : ${funFun()}")
}
fun sum(a: Int, b: Int) = a + b
fun funFun(): Int {
return sum(2, 2)
}
출력 : funFun : 4
📝 람다식을 변수에 할당하기 val multi = { x: Int, y: Int -> x * y }
fun main() {
var result: Int
val multi = { x: Int, y: Int -> x * y } // 일반 변수에 람다식 할당
result = multi(10, 20) // 람다식이 할당된 변수는 함수처럼 사용 가능
println(result)
}
출력 : 200
📫 매개변수에 자료형이 지정되어 있다면 변수의 자료형은 생략할 수 있다.
val multi: (Int, Int) -> Int = { x: Int, y:Int-> x*y } // 생략되지 않은 전체 표현 val multi = { x: Int, y:Int-> x*y } // 선언 자료형 생략 val multi: (Int, Int) -> Int = { x, y-> x*y } // 람다식의 매개변수 자료형의 생략
💥 위 세 개 표현은 모두 같은 표현이다 !!
✏️ 람다식에 매개변수와 반환값이 없을 때
fun main() {
val greet: () -> Unit = { println("Hello World!") }
val greet = { println("Hello World!") } // 위의 식과 똑같은 표현
val square: (Int) -> Int = { x -> x * x }
val square = { x: Int -> x * x } // 위의 식과 똑같은 표현
}
greet
: 람다식의 매개변수와 반환값이 없음square
: 람다식을 보고 매개변수와 반환값을 추론할 수 있으면 람다식의 매개변수의 자료형 생략가능✏️ 람다식 안에 람다식 넣기
val nestedLambda: () -> () -> Unit = { { println("nested") } }
val nestedLambda = { { println("nested") } } // 위의 식과 똑같은 표현
📝 람다식을 매개변수에 사용하기 result = highOrder({ x, y -> x + y }, 10, 20)
fun main() {
var result: Int
result = highOrder({ x, y -> x + y }, 10, 20)
println(result)
}
fun highOrder(sum: (Int, Int) -> Int, a: Int, b: Int): Int {
return sum(a, b)
}
출력 : 30
📝 인자와 반환값이 없는 람다식
fun main() {
val out: () -> Unit = { println("Hello World!") } // 인자와 반환값이 없는 람다식의 선언
out() // 함수처럼 사용가능
val new = out // 람다식이 들어 있는 변수를 다른 변수에 할당
new()
}
출력 :
Hello World!
Hello World!
Hello World!
를 출력한다는 함수의 내용만 있을 뿐 매개변수가 없기 때문에 인자를 전달받지 않고 있다.📍 이름 없는 함수를 표현하기 위해 등장한 람다식 표현은 함수형 프로그래밍에서 아주 중요한 개념이다.
📍 람다식은 많은 코드들을 간략화하고 함수 자체를 인자나 매개변수로 이용할 수 있어 프로그램의 효율성도 높일 수 있다.
스택
에 할당된 값이 있다.스택
에 할당된 객체의 참조 주소가 있고 객체는 힙
에 있다.callByValue(lambda())
람다식 함수는 값으로 처리되어 그 즉시 함수가 수행된 후 값을 전달한다.fun main() {
val result = callByValue(lambda())
println(result)
}
fun callByValue(b: Boolean): Boolean {
println("callByValue function")
return b
}
val lambda: () -> Boolean = {
println("lambda function")
true
}
출력 :
lambda function
callByValue function
true
fun main() {
val result = callByName(otherLambda)
println(result)
}
fun callByName(b: () -> Boolean): Boolean {
println("callByName function")
return b()
}
val otherLambda: () -> Boolean = {
println("otherLambda function")
true
}
출력 :
callByName function
otherLambda function
true
값에 의한 호출
과의 차이는 매개변수 b가 람다식의 자료형으로 선언되었다는 것 !!callByValue :
val result = callByValue(lambda())
callByName :val result = callByName(otherLambda)
💥 함수 형태로 호출해야 비로소 람다식이 실행됨 !!
이 부분이 와닿지 않아 앞의 개념을 계속 해서 뒤적 거렸다 😵
어렵게 어렵게 이해한 방식을 그대로 서술하겠다..!
fun main() {
// 1. 인자와 반환값이 있는 함수
val res1 = funcParam(3, 2, ::sum)
println(res1)
// 2. 인자가 없는 함수
hello(::text)
// 3. 일반 변수에 값처럼 할당
val likeLambda = ::sum
println(likeLambda(6, 6))
}
fun sum(a: Int, b: Int) = a + b
fun text(a: String, b: String) = "Hi! $a $b"
fun funcParam(a: Int, b: Int, c: (Int, Int) -> Int): Int {
return c(a, b)
}
fun hello(body: (String, String) -> String): Unit {
println(body("Hello", "World"))
}
출력 :
5
Hi! Hello World
12
인자와 반환값이 있는 함수
1) ::sum
을 통해 sum()
함수를 람다식으로 사용 가능
2) funcParam(3, 2, ::sum)
실행
3) res1
에 sum(3,2)의 결과값이 저장 -> 5
출력
🚨인자가 없는 함수🚨
1) hello(::text)
실행
2) text()
함수 참조(= 람다식)를 인자로 갖는 hello()
함수 실행
3) body("Hello", "World")
실행 = text("Hello, "World)
-> body인즉슨 text!!!✨
4) 위의 결괏값 Hi! Hello World
반환
일반 변수에 값처럼 할당
1) likeLambda
에 sum
이라는 함수 대입
2) likeLambda(6,6)
를 실행하면 sum(6,6)
과 같이 12
출력
📫 콜론 2개(::)를 이용한 표기법
hello(::text) // 함수 참조 기호 hello({ a, b -> text(a, b) }) // 람다식 표현 hello { a, b -> text(a, b) } // 소괄호 생략
위의 3가지 표현은 모두 동일한 결과를 출력한다.
📍 매개변수와 인자 구조가 동일한 경우 람다식 표현법이 간략화된 참조기호인 ::을 사용하면 좀 더 편리하게 작성할 수 있다.
fun main() {
noParam({ "Hello World!" })
noParam { "Hello World!" }
}
fun noParam(out: () -> String) = println(out())
{...}
매개변수가 없는 람다식 표현 (->
를 사용 안 함)fun main() {
oneParam({ a -> "Hello World $a" })
oneParam { a -> "Hello World $a" }
oneParam { "Hello World $it" }
}
fun oneParam(out: (String) -> String) = println(out("OneParam"))
$it
으로 대체할 수 있다.$it
은 람다식 매개변수로 지정된 String 형과 매칭되어 "OneParam" 문자열로 바뀌며 최종적으로 Hello World OneParam
출력fun main() {
moreParam({ a, b -> "Hello World $a $b" })
}
fun moreParam(out: (String, String) -> String) = println(out("OneParam", "TwoParam"))
✏️ 특정 람다식의 매개변수를 사용하고 싶지 않을 경우
moreParam({ _, b -> "Hello World $b" }) // 첫번째 문자열은 사용하지 않고 생략
fun main() {
withArgs("Args1", "Args2", { a, b -> "Hello World $a $b" })
withArgs("Args1", "Args2") { a, b -> "Hello World $a $b" }
}
fun withArgs(a: String, b: String, out: (String, String) -> String) = println(out(a, b))
Hello World Args1 Args2
fun main() {
twoLambda({ a, b -> "First $a $b" }, { "Second $it" })
twoLambda({ a, b -> "First $a $b" }) { "Second $it" }
}
fun twoLambda(first: (String, String) -> String, second: (String) -> String) {
println(first("OneParam", "TwoParam"))
println(second("OneParam"))
}
twoLambda
에 람다식이 2개 정의 되어 있으니 중괄호를 생략하진 못하지만, 마지막 인자가 람다식이므로 마지막 인자의 중괄호는 소괄호 바깥으로 뺄 수 있다.({ex1},{ex2})
= ({ex1}) {ex2}
({ex1}, {ex2}) {ex3}
음 이런 사례들이 있구나!
익명함수 : 일반 함수이지만 이름이 없는 것
fun(x: Int, y: Int): Int = x + y // 함수 이름이 생략된 익명 함수
val add: (Int, Int) -> Int = fun(x, y) = x + y // 익명 함수를 사용한 add 선언
val add = { x: Int, y: Int -> x + y } // 위와 같은 표현
val result = add(10, 2) // add 사용
📫 람다식으로 표기할 수 있는 굳이 굳이 익명 함수를 쓰는 이유는 ?!
- 람다식에는
return
,break
,continue
와 같은 제어문을 사용하기 어렵다- 함수 본문 조건식에 따라 함수를 중단하고! 반환하는! 경우에 익명 함수를 사용한다.
인라인 함수 : 함수가 호출되는 곳에 함수 본문의 내용을 모두 복사해 넣어 함수의 분기 없이 처리되기 때문에 코드의 성능을 높일 수 있다.
fun main() {
shortFunc(3) { println("First call: $it") }
shortFunc(5) { println("Second call: $it") }
}
inline fun shortFunc(a: Int, out: (Int) -> Unit) {
println("Before calling out()")
out(a)
println("After calling out()")
}
출력 :
Before calling out()
First call: 3
After calling out()
Before calling out()
Second call: 5
After calling out()
📍 noinline
키워드를 사용해서 일부 람다식을 인라인되지 않게 한다.
-> 인라인으로 처리되지 않고 분기하여 호출된다.
fun main() {
shortFunc(3) { println("First call: $it") }
}
inline fun shortFunc(a: Int, noinline out: (Int) -> Unit) {
println("Before calling out()")
out(a)
println("After calling out()")
}
출력 :
Before calling out()
First call: 3
After calling out()
fun main() {
shortFunc(3) {
println("First call: $it")
return
}
}
inline fun shortFunc(a: Int, out: (Int) -> Unit) {
println("Before calling out()")
out(a)
println("After calling out()")
}
출력 :
Before calling out()
First call: 3
✏️ 비지역 반환을 금지하는 방법
fun main() {
shortFunc(3) {
println("First call: $it")
}
}
inline fun shortFunc(a: Int, crossinline out: (Int) -> Unit) {
println("Before calling out()")
nestedFunc { out(a) }
println("After calling out()")
}
fun nestedFunc(body: () -> Unit) {
body()
}
crossline
키워드를 사용한다.확장 함수 : 클래스처럼 필요로 하는 대상에 함수를 더 추가할 수 있다.
fun 확장대상.함수이름(매개변수 ... ): 반환값 {
..
return 값
}
📫 코틀린의 모든 클래스에 내가 만들어 놓은 확장 함수를 추가할 수 있을까?!
- 최상위 클래스인
Any
에 확장함수를 구현하면 가능🙆♀️
- 코틀린의 최상위 요소는
Any
이기 때문에Any
에 확장 함수를 추가하면 코틀린의 모든 요소에 상속되기 때문에 가능하다 !!
fun main() {
val source = "Hello World!"
val target = "Kotlin"
println(source.getLongString(target))
}
fun String.getLongString(target: String): String =
if (this.length > target.length) this else target
출력 : Hello World!
String
에 .
표기로 String.getLongString()
와 같이 선언해 기존에 없는 새로운 메서드를 만든다.this
는 확장 대상 자리에 있던 문자열 (여기서는 source
)📍 확장 함수 기법을 사용하면 기존 클래스의 선언 구현부를 수정하지 않고 외부에서 쉽게 기능을 확장할 수 있다.
📍 확장함수를 만들 때 확장하려는 대상에 동일한 이름의 멤버 함수 or 메서드가 존재할 경우 얘네가 확장함수보다 우선으로 호출된다.
중위함수 : 일종의 연산자를 구현할 수 있는 함수
infix
키워드를 사용하여 정의한다.📍 중위 표현법 (Infix Notaion) : 클래스의 멤버를 호출할 때 사용하는 .
을 생략하고 함수 이름 뒤에 소괄호를 붙이지 않아 직관적인 이름을 사용할 수 있는 표현법
fun main() {
val multi = 3 multiply 10 // 중위 함수 표현
println("multi : $multi")
val multi2 = 3.multiply(15) // 일반 표현법
println("multi2 : $multi2")
}
infix fun Int.multiply(x: Int): Int {
return this * x
}
📍 코틀린에서는 꼬리 재귀 함수를 통해 스택 오버플로 현상을 해결할 수 있다.
tailrec
키워드를 사용한다.📝 일반적인 팩토리얼 재귀 함수 vs 꼬리 재귀 함수
fun main() {
val num = 4
val result: Long = factorial(num)
println("Factorial : $num - > $result")
val num2 = 5
println("Factorial : $num2 -> ${tailFactorial(num2)}")
}
fun factorial(n: Int): Long {
return if (n == 1) n.toLong() else n * factorial(n - 1)
}
tailrec fun tailFactorial(n: Int, run: Int = 1): Long {
return if (n == 1) run.toLong() else tailFactorial(n - 1, run * n)
}
출력 :
Factorial : 4 - > 24
Factorial : 5 -> 120
n * factorial(n - 1)
tailFactorial(n - 1, run * n)
: 인자 안에서 팩토리얼의 도중 값을 계산하고 호출{}
을 가진다.📫 전역 객체는
소스의 전체 범위
에서 사용 가능! 지역 객체는선언된 내부
에서 사용 가능!
fun a() = b()
fun b() = println("b")
fun c() {
fun d() = e()
fun e() = println("e")
}
fun main() {
a()
e()
}
a()
b()
c()
는 최상위 함수이므로 선언 순서에 영향을 받지 않음d()
e()
는 지역 함수이므로 순서에 영향을 받음 -> c()
의 밖에선 호출❌fun main() {
val local1 = 20
val local2 = 21
fun nestedFunc() {
global += 1
val local1 = 30
println("nestedFunc local1 : $local1")
println("nestedFunc local2 : $local2")
println("nestedFunc global : $global")
}
nestedFunc()
outsideFunc()
println("main global : $global")
println("main local1 : $local1")
println("main local2 : $local2")
}
fun outsideFunc() {
global += 1
val outVal = "outside"
println("outsideFunc global : $global")
println("outsideFunc outVal : $outVal")
}
출력:
nestedFunc local1 : 30
nestedFunc local2 : 21
nestedFunc global : 11
outsideFunc global : 12
outsideFunc outVal : outside
main global : 12
main local1 : 20
main local2 : 21
global
의 값은 1씩 증가했다.outsideFunc()
안에 존재하는 outVal
은 여기 블록에서만 유효하다.