본문 바로가기

함수형 자바

[JAVA] 일급 함수, 고차 함수, 커링 함수 (Currying Function) 이해해 보기 - Chapter 01

 

함수형 자바 프로그래밍 1장을 읽으면서 커링 함수가 잘 이해가 안되어서 직접 이것저것 해보았다.

커링 함수나 고차 함수의 예제들의 계시물은 JAVA로 구현한 것이 많이 없어 슬펐다... (대부분 Javascript였다...)

 

먼저 커링 함수 (Currying Function)에 들어가기 앞서서, 함수형 프로그래밍을 이해하기 위해 일급 함수와 고차 함수를 이해해 보자.

일급 함수

함수 자체를 다른 함수에 인수로 전달 하거나 반환값으로 사용될 수 있다.

변수에 할당 할 수 있는 함수이다.

 

고차 함수

함수를 인수로 받을 수 있다.

함수를 반환 할 수 있다.

 

고차함수는 수학의 범함수와 느낌이 비슷하다고 한다. (함수를 입력받는 함수. 정의역이 함수집합, 공역이 실수나 복소수)

예제에서 어떤게 일급함수이고 고차함수인지 구분해 보자

 

커링 함수 (Currying Funciton)

커링 함수는 여러 개의 인수를 받는 함수를 분리하여 인수를 하나씩만 받는 함수의 체인으로 변환하는 것이라 한다.

package chapter1;

import java.util.function.Function;
import java.util.function.BiFunction;
public class curryingFunction {

    public static BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
    public static BiFunction<Integer, Integer, Integer> minus = (a, b) -> a - b;

    public static BiFunction<BiFunction<Integer, Integer, Integer>, Integer, Function<Integer, Integer>> curry = (fx, valueX) -> valueY -> fx.apply(valueX, valueY);

    public static BiFunction<Integer, Integer, Function<BiFunction<Integer, Integer, Integer>, Function<BiFunction<Integer, Integer, Integer>, Integer>>> curry2 = (x, y) ->
            fn -> fn2 -> fn2.apply(fn.apply(x, y), y);

    public static Function<BiFunction<Integer, Integer, Integer>, Function<BiFunction<Integer, Integer, Integer>, Function<Integer, Function<Integer, Integer>>>> curry3  =
            fn -> fn2 -> x -> y -> fn.apply(x, y) + fn2.apply(x, y);

    public static void main(String[] args) {

        //curry
        System.out.print("curry에 fx: add, valueX : 10, valueY : 5 == ");
        System.out.println(curry.apply(add, 10).apply(5));

        System.out.print("curry에 fx: minus, valueX : 10, valueY : 5 == ");
        System.out.println(curry.apply(minus, 10).apply(5));

        //curry2
        System.out.print("(x, y) : (3, 4), fn : add, fn2 : minus == ");
        System.out.println(curry2.apply(3, 4).apply(add).apply(minus));

        //curry3
        System.out.print("fn : add, fn2 : minus, x : 3, y : 5 = ");
        System.out.println(curry3.apply(add).apply(minus).apply(3).apply(5));
        System.out.println(curry3.apply(add) instanceof Function<BiFunction<Integer, Integer, Integer>, Function<Integer, Function<Integer, Integer>>>);
        System.out.println(curry3.apply(add).apply(minus) instanceof Function<Integer, Function<Integer, Integer>>);
        System.out.println(curry3.apply(add).apply(minus).apply(3) instanceof Function<Integer, Integer>);
        System.out.println(curry3.apply(add).apply(minus).apply(3).apply(5) instanceof Integer);

    }

}

 

===============curry1=================
fx: add, valueX : 10, valueY : 5 == 15
fx: minus, valueX : 10, valueY : 5 == 5
===============curry2=================
(x, y) : (3, 4), fn : add, fn2 : minus == 3
===============curry3=================
fn : add, fn2 : minus, x : 3, y : 5 = 6
true
true
true
true

 

체인 형태의 커링 함수를 3개 구현해 보았다.

 

curry1

BiFunction<BiFunction<Integer, Integer, Integer>, Integer, Function<Integer, Integer>> curry1 
	= (fx, valueX) -> valueY -> fx.apply(valueX, valueY);
    
//curry1
System.out.println("===============curry1=================");
System.out.print("fx: add, valueX : 10, valueY : 5 == ");
System.out.println(curry1.apply(add, 10).apply(5));

System.out.print("fx: minus, valueX : 10, valueY : 5 == ");
System.out.println(curry1.apply(minus, 10).apply(5));

===============curry1=================
fx: add, valueX : 10, valueY : 5 == 15
fx: minus, valueX : 10, valueY : 5 == 5

 

먼저 curry1.apply(add, 10) 을 통해 fx : add, valueX : 10 을 넣어주었다.

이렇게 인수를 지정 함으로써 (add라는 함수도 인수가 될 수 있고, 변수 valueX도 함수의 인수가 될 수 있다.) 결과는 Function<Integer, Integer>인 인수 valueY를 받는 함수로 변환되었다.

 

그 결과에 체이닝으로 .apply(5) 를 통해 valueY : 5 를 넣어주었다.

이렇게 되면 결과는 Function<Integer, Integer> -> Integer로 15 가 나온다.

 

잠깐 위의 일급 함수, 고차 함수의 개념도 섞어보자면,

add라는 함수는 curry1함수의 인수로 들어 갈 수 있으니 일급 함수라 볼 수 있고,

curry1함수는 함수를 인자로 호출 할 수 있고, 함수를 반환하니 고차 함수라 볼 수 있다.

 

그럼 curry1.apply(add, 10)은 일급 함수일까? 고차 함수일까?

이 함수는 인수에 함수를 받지 않고, 반환 값도 함수가 아니니 일급 함수이다.

 

curry1.apply(minus, 10).apply(5) 는

fx : minus, valueX : 10, valueY : 5 로, minus(10 , 5) = 10 - 5 = 5 로 볼 수 있다.

curry2

BiFunction<Integer, Integer, Function<BiFunction<Integer, Integer, Integer>, Function<BiFunction<Integer, Integer, Integer>, Integer>>> curry2 
	= (x, y) -> fn -> fn2 -> fn2.apply(fn.apply(x, y), y);
    
    
 //curry2
System.out.println("===============curry2=================");
System.out.print("(x, y) : (3, 4), fn : add, fn2 : minus == ");
System.out.println(curry2.apply(3, 4).apply(add).apply(minus));

===============curry2=================
(x, y) : (3, 4), fn : add, fn2 : minus == 3

이번에는 순서를 살펴보자.

1. Integer, Integer : x, y 를 받아 BiFunction<Integer, Integer, Integer>인 fn에 넣어주고 ((x, y) -> fn)

2. Bifunction<integer, integer, Integer> : fn 을 받아 Bifunction<Integer, Integer, Integer> 인 fn2에 적용하고 (fn -> fn2)

3. Bifunction<integer, integer, integer> : fn2 를 받아 Integer를 return하는 함수인 fn2.apply(fn.apply(x, y), y)에 적용해준다.

 

1.  x : 3, y : 4 를 넣는다. (curry2.apply(3, 4))

2. fn : add (함수)를 넣는다. (curry2.apply(3,4).apply(add))

3. fn2 : minus (함수)를 넣는다. (curry2.apply(3,4).apply(add).apply(minus)

= minus(add(3, 4), 4)

= minus(3 + 4, 4)

= 3 + 4 - 4 = 3

 

체이닝으로 각각의 인수를 지정하면서 함수의 인수를 줄이는 것이라 생각하였다.

curry3

Function<BiFunction<Integer, Integer, Integer>, Function<BiFunction<Integer, Integer, Integer>, Function<Integer, Function<Integer, Integer>>>> curry3  
	= fn -> fn2 -> x -> y -> fn.apply(x, y) + fn2.apply(x, y);
    
//curry3
System.out.println("===============curry3=================");
System.out.print("fn : add, fn2 : minus, x : 3, y : 5 = ");
System.out.println(curry3.apply(add).apply(minus).apply(3).apply(5));
System.out.println(curry3.apply(add) instanceof Function<BiFunction<Integer, Integer, Integer>, Function<Integer, Function<Integer, Integer>>>);
System.out.println(curry3.apply(add).apply(minus) instanceof Function<Integer, Function<Integer, Integer>>);
System.out.println(curry3.apply(add).apply(minus).apply(3) instanceof Function<Integer, Integer>);
System.out.println(curry3.apply(add).apply(minus).apply(3).apply(5) instanceof Integer);

===============curry3=================
fn : add, fn2 : minus, x : 3, y : 5 = 6
true
true
true
true

 

확인차 각각의 단계에서 return type을 print 해 보았다.

curry2의 과정을 생각하며 고민해보자.

인수를 지정해 줄 수록 함수의 변수들이 주는 간단한 함수들로 변한다.

 

부분 함수 적용

커링이 여러 개의 인자를 받는 함수를 단일 인자를 받는 함수들의 연속된 체인으로 분해하는 것이라면,

부분 함수 적용은 여러 개의 인자를 받는 함수를 인자의 일부를 고정시킨 새로운 함수로 변환하는 것이라 한다.

 

좀 더 풀어서 설명하자면,

커링은 인자를 하나씩 전달하면서 체인 형태로 함수를 여러번 호출하는 것이라면,

부분 함수 적용은 특정 인자를 고정시킨 새로운 함수를 생성하는 것이다.

 

BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

Function<Integer, Integer> addTen = b -> add.apply(10, b);
int result = addTen.apply(5);

 

여기서의 addTen 처럼 하나의 인자를 10으로 고정한 새로운 함수를 생성하는 것이라 생각하면 되는 듯 하다.

 

 

 

이것 저것 시도해 보면서 커링 함수의 타입을 선언해 줄 때 에러도 많이 나고 감이 안잡혔는데,

앞에서 부터 차근차근 인수로 들어가는 타입이 뭔지에 대해 적어주면 좀 쉽게 선언 할 수 있을 것 같다.