고차함수
어떻게 보면 메소드 체이닝과 같이 코드의 중복을 없애고 변수를 만들고 그 변수로 로직이 돌아가는 그런느낌이 아니니라 함수에서 변수가 생기면 그 변수를 바로바로 람다를 통해서 작업할 수 있다 고차함수란 다른 함수를 인자로 받거나 함수를 반환해주는 함수이다 -> 함수 참조를 사용해서 함수를 값으로 표현할 수 있다 그래서 고차함수는 람다나 함수 참조를 인자로 넘길 수 있거나 람다나 함수 참조를 반환하는 함수이고 함수를 인자를 받는 동시에 함수를 반환하는 것도 가능하다
고차 함수 정의
코틀린에서는 타입 추론을 통해서 변수 타입을 지정하지 않아도 람다를 변수에 대입할 수 있다
이렇게 파라미터로 들어가는 타입을 괄호 안에 넣어주고, 그 뒤에 화살표를 통해서 리턴 타입을 명시해준다 익명함수처럼 리턴값이 따로 존재하지 않는 케이스에서는 자바에서의 void와 같은 의미인 Unit을 통해서 명시해줄 수 있다 없다고해서 명시를 안해주면 안된다 추가로 nullable 타입을 리턴하는 것도 가능한데 ((Int, Int)->Int)? = null 이렇게 널러블 타입을 리턴하는이 가능하다는 점이다
그래서 고차함수를 어떻게 선언하냐
이 함수는 2,3에 대해서 인자로 받은 연산을 수행하고 결과를 내보낸다 이렇게 인자로 받은 함수를 호출하는 구분은 일반 함수를 호출하는 것과 동일하다 심지어 자바에서도 코틀린 함수 타입을 사용할 수 있다 어짜피 컴파일된 코드 안에서 함수 타입은 일반 인터페이스로 변경되기 때문에 함수 타입의 변수는 FunctionN 인터페이스를 구현하는 객체를 저장 한다 코틀린 표준 라이브러리 함수는 인자의 갯수에 따라 Function0 (인자가 없는 함수), Function1<P1, R> (인자가 하나 있는 함수) 이렇게 여러개 인터페이스를 제공하고 이러한 인터페이스 내부에는 invoke 메소드가 있고 이걸 통해서 함수를 실행한다 그래서 함수 타입인 변수는 인자 갯수에 따라 적당한 FunctionN 인터페이스를 구현하는 클래스의 인스턴스를 저장하고 invoke 메소드에 람다의 본문이 들어가는 방식이다 위에서 코틀린에서 사용한 것 처럼 사용하되, Unit 대신 void 으로 수정이 되어야함
함수에서 함수를 반환하는 경우도 있다 이런건 이제 상태나 조건에 따라서 로직이 달라지는 경우에 사용할 수 있다 책에서 예시로 들어준 부분은 자신이 선택한 배송 수단에 따라서 배송비를 계산하는 로직이 다른 케이스를 적절한 로직을 선택해서 함수로 반환하는 함수를 만드는 것을 보여주었다
이렇게 다른 함수를 리턴하는 함수를 정의하기 위해서는 함수의 리턴 타입으로 함수 타입을 지정해주어야 한다
함수타입과 람다식은 중복 제거에 스페셜리스트이다 람다를 사용하니까 복잡한 구조의 코드도 간결하게 사용될 수 있다 그리고 코틀린스럽게 만드는 것이 람다를 적극 활용한다는 의미이다 중복을 없앤다는 것은 즉 중복되는 코드를 람다로 만드는 방식으로 없앤다는 의미이다... 일단 많은 예시를 눈으로 익히고 봐보자
인라인 함수
근데 람다는 가독성도 그렇고 코드를 간단하게 잘 만들어주는 방식이다 근데 좋은건 알겠는데 실제로 성능도 그럼 좋을려나? 일단 코틀린이 람다를 익명 함수로 컴파일하지만 그렇다고 새로운 클래스를 만드는 것은 아니다 하지만 람다가 변수를 잡는다면 람다가 생성될때마다 새로운 익명 클래스가 생성되긴 한다 이런 경우에 실행될 때 그 익명 클래스를 만드는데 추가적인 비용이 들긴 한다 그래서 사실 람다를 사용하면 그냥 로직을 함수로 뺴서 그 함수를 사용하는 것 보다 효율성에서 떨어진다 그래서 반복되는 로직을 라이브러리로 빼되, 컴파일러가 자바의 일반 코드처럼 생성할 수는 없을까 -> 이걸 inline 이 해준다 inline 을 함수에 붙히면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 변경해준다
함수에 inline을 선언하면 함수를 호출하는 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트 코드로 컴파일해주는 것이다 그니까 간단하게 함수를 호출하는 코드에 함수를 호출하는놈이 오는게 아니라 실제 함수본문인 바이트코드화되어서 넘어온다는 것이구나 물론 람다의 본문도 넣어줄 수 있어서 해당 inline 함수 내부에서 원하는 작업을 진행하는 것도 가능하다 여기서 람다의 본문으로부터 만들어지는 바이트코드는 그 람다를 호출하는 코드 정의의 일부분으로 간주해서 코틀린 컴파일러는 그 람다를 함수 인터페이스를 구현하는 익명 클래스로 감싸지 않는다
그래서 인라인 함수의 단점은 뭐가 있을까 인라인은 람다를 사용하는 모든 함수를 기준으로 인라인할 수 없다 함수가 인라이닝될 때 그 함수에 인자로 전달된 람다 식의 본문은 결과 코드에 직접 들어가는 것이 가능하지만 결국 함수 본문이 가서 펼쳐지는 방식이기 때문에 사용하는 방식이 한정될 수 밖에 없다 함수 본문에서 파라미터로 받은 함수를 호출하면 그 호출을 쉽게 람다로 변형할 수 있지만 파라미터로 받은 람다를 다른 변수에 저장하고 그 다음에 그 변수를 사용하면 람다를 바로 인라이닝할 수 없다 근데 일반적으로 인라인 함수의 본문에서 람다 식을 바로 호출하건 람다 식을 인자로 전달받아서 바로 호출하던 그 람다를 바로 인라이닝 할 수 있다 애초에근데 이런 경우가 아니면 컴파일러가 에러로 잡아주었다 만약에 함수에서 특정 일부 람다만 인라이닝으로 끄집어서 쓰고 싶고 나머지는 필요 없는 경우에는 내부 파라미터를 선언해줄 때 해당 익명 함수 부분에서 noinline 키워드를 통해서 인라이닝을 빼고 진행할 수 있다
뭔가 이렇게 보고있으면 그냥 사용하면 할수록 좋은 거? 라고 생각하긴하는데 막 아무런 생각 없이 쓴다고해서 좋은 것만은 아니다 결국 성능적 향상을 받는건 람다를 인자로 받는 함수 성능만 좋아진다 실제로 일반적인 함수 호출의 경우에는 JVM에서 어느정도는 지원해준다 이놈이 알아서 코드를 실행하면 바이트코드>기계어 변형과정(JustInTime)에 가장 베스트인 방향으로 호출을 인라이닝을 해주기 때문에 세세하게 들여다보고 성능적 차이를 확인해야 한다 이미 인라이닝을 해준다고 했을 때 바이트 코드에서는 각 함수 구현이 한 번만 있으면 되고, 그 함수를 호출하는 부분에서 따로 함수 코드를 중복할 필요가 없다 하지만 코틀린의 인라이닝 함수는 결국 있는 함수 바이트코드를 그대로 긁어와서 사용하기 떄문에 어떻게 보면 코드 중복이 일어나는 것이다 그래도 람다를 인자로 받는 함수를 인라이닝하면 좋은 점이 많다고 한다 일단 인라이닝을 통해서 없앨 수 있는 부가 비용이 많다 -> 함수 호출 비용을 줄이고, 람다로 표현하는 클래스와 람다 인스턴스를 생성할 필요가 없다 그리고 지금 JVM이 함수 호출과 람다를 인라이닝 해줄 정도로 스마트하지는 않다고 한다 마지막으로는 몇 가지 추가적인 기능이 있다는데 이걸 나중에 본다네? 뭐지? 그래서 결론은 이거다 -> inline 쓸 때는 함수의 크기를 확인하자 결국은 이게 모오오든 코드에 함수를 녹여서 사용하는거니까 큰 함수에다가 인라인 달고 사용하면 매우 불편할듯하다 필요하면 noinline으로 뺄껀 빼고 사용하자
참고? 예시?로 있는게 자원 관리를 위해 인라인된 람다 사용이 추가로 있었다 람다로 중복을 없앨 수 있는 일반적인 패턴 중 한 가지는 어떤 작업을 하기 위해 자원을 획득 > 작업완료 후 자원 해제 이런 흐름이다 자원의 예시로는 역시 트랜잭션, 락, 파일 이런것들이 있는데 일반적은 try-finally로 처리한다 try 이전에 자원을 잡고 finally에서 자원을 해제하는 방식으로 처리한다 코틀린에서는 withLock 이라는 라이브러리를 제공해주는데 이게 Lock 인터페이스의 확장 함수인데 사용법은 해당자원.withLock{ 자원을 사용한 로직 } 이렇게 사용하는 방식이다
고차 함수 안에서 흐름 제어
내부에 return 문에 있는 루프문을 filter 와 같은 람다를 호출하는 함수로 바꾸고 인자로 전달하는 람다 안에서 return 을 사용하면 어떨까?
이렇게 람다 안에서 return을 사용하면 람다로부터 변환되는 것이 아니라 그 람다를 호출하는 함수가 실행을 끝내고 리턴된다 즉 자신을 둘러싸고 있는 블록이나 그 밖의 블록을 리턴시키는 리턴문을 보고 넌로컬 리턴이라고 부른다 하지만 이렇게 리턴이 밖의 블록까지 한꺼번에 리턴시키는 경우는 람다를 인자로 받는 함수가 인라인 함수인 경우뿐이다 위의 예시인 forEach 같은 경우도 인라인 함수이기 때문에 람다 본문과 함께 인라이닝되기 때문에 리턴식이 바깥쪽 블록을 리턴할 수 있는 것이였다
물론 람다에서도 로컬 리턴을 사용할 수 있다, 만약에 리턴자체를 그렇게 외부쪽에도 영향이 가지 않고 해당 블록 내에서만 리턴이 되게 하고 싶다면 사용하는 것이 로컬 리턴이다 이렇게 보면 로컬 리턴은 약간 break 문과 같은 느낌이라고 볼 수 있을 것 같다 로컬 리턴은 람다의 실행을 끝내고 람다를 호출한 함수를 이어서 진행하게 된다 로컬 리턴과 넌로컬 리턴의 구분은 label으로 구분을 짓는다 -> 리턴으로 실행을 종료하고 싶다면 람다 식 앞에 label을 붙히고, return 뒤에 붙힌다
이렇게 람다식 뒤에 해당 람다 식에 label 을 붙히고, 리턴할 떄 @레이블이름 이렇게 특정 이름의 label을 넘겨주는 방식으로 사용한다 물론 이렇게 이름을 정해줘서 하는 방식도 있지만 default 으로는 return@ 이렇게 까지만 해줘도 ide 에서 알아서 forEach를 명시해주더라고 그른데 만약에 로직이 길어지고 여러가지의 람다가 중첩되면 많은 위치에 return 이 들어가야한다 그리고 이런건 좀 그렇게 보이니까 익명함수를 통해서 조금 쉽게 작성하는 것이 가능하다
익명함수도 일반 함수와 같은 리턴 타입 지정 규칙이 따라가기 때문에 기존에 lable로 지정해서 리턴해줄 필요 없이도 그냥 리탄이 가능하다 넌로컬 리턴 처럼 모든 것을 리턴하는 것이 아닌 익명 함수만 리턴하는 방식이다 사실 리턴에 적용되는 규칙은 단순히 가장 안쪽의 함수를 리턴한다는 규칙이다 이렇게 익명함수를 통해서 구현하면 애초에 가장 안쪽에 있는 람다식, 함수가 가장 안쪽에 있는 함수이니 해당 리턴은 익명함수만을 리턴하는 것이다 뭐 이렇게도 사용할 수 있다고 알아두쟈
Last updated
Was this helpful?