카테고리 없음

챕터[19] 함수형 프로그래밍 기법

JeonInwoo 2020. 10. 22. 00:44

서론

일급 시민 고차원 함수 커링 부분 적용

영속 자료구조

자바 스트림을 일반화하는 게으른 평가와 게으른 리스트

패턴 매칭, 자바에서 패턴 매칭을 흉내 내는 방법

참조 투명성과 캐싱


일급 함수

  1. 함수형 프로그래밍에서 함수를 마치 일반값처럼 사용하여 인수를 전달하거나, 결과를 반환하거나 자료구조를 저장할 수 있어 일반값처럼 취급하는 함수를 일급 함수라고한다.

  2. 자바 8이 이전 버전과 구분될 수 있는 특징중 하나가 일급 함수를 지원한다는 점이 있다.

  3. 추가로 자바 8에서는 메서드 참조( :: ) 및 람다로 직접 함수값을 표현해 메서드를 함수값으로 사용할 수 있다.

고차원 함수

자바 8의 함수는 고차원 함수라 할 수 있으며 아래의 특징을 가진다.

  • 하나 이상의 함수를 인수로 받는다.

  • 결과값으로 함수를 반환한다.

  • 지역 변수로 할당을 할 수 있다.

  • 구조체로 삽입할 수 있다.

커링

커링이란 한 개의 인수를 갖는 변환 함수를 생성하는 팩토리 코드이다.

-> 인자를 여러개 받는 함수를 분리하여 하나의 인자를 받도록 함수를 체인으로 만드는 방법이다.

    // 커링이란 한 개의 인수를 갖는 변환 함수를 생성하는 팩토리 코드이다.

    // x : 변환하려는 값
    // f : 변환 요소
    // b : 기준치 조정 요소
    static double converter(double x, double f, double b) {
        return x * f + b;
    }

    // 기존 로직(위)을 이용하여 커링이라는 개념을 활용하녀 한 개의 인수를 갖는 변환 함수를 생성하는 팩토리 정의코드
    static DoubleUnaryOperator curriedConverter(double f, double b) {
        return (double x) -> x * f + b;
    }

    DoubleUnaryOperator convertCtoF = curriedConverter(9.0/5, 32);
    DoubleUnaryOperator convertUSDtoGBP = curriedConverter(0.6, 0);
    DoubleUnaryOperator convertKmtoMi = curriedConverter(0.6214, 0);

    // DoubleUnaryOperator는 applyAsDouble 이라는 메서드를 정의하므로 다음처럼 변환기를 사용할 수 있다.
    @Test
    public void curringTest() {
        double temperature = convertCtoF.applyAsDouble(32);
        double gbp = convertUSDtoGBP.applyAsDouble(500);
        double mile = convertKmtoMi.applyAsDouble(1000);

        log.info("temperature : {} / gbp : {} / mile : {}", temperature, gbp, mile);
    }

 

영속 자료구조

여기서의 영속은 함수형 프로그래밍에서 사용하는 자료구조를 의미하며, 데이터베이스에서 프로그램 종료 후 남아있음을 의미하는 영속과는 다른 의미이다.
ex) 반환해주는 x는 값을 더한 후 반환해준다.
예제와 같이 반환이 이루어질 경우 결국에는 기존의 변수(애시당초 불변 or 지역변수여야 함수형 프로그래밍이라고 할 수 있음)를 갱신 후 반환해주기에 ch18에서 말씀드린 참조 투명성에 위배가 되어 결국에는 부작용이 생기고 만다.
이러한 경우를 파괴적인 갱신 이라고 부르며 이를 보완하기 위해서 생긴 방법으로는 기존의 변수를 내비두고 새로운 변수를 생성하여 값을 복사한 후 결과를 반영시켜 반환해주는 부작용 없는 방식을 함수형 갱신이라고 한다.

 

파괴적인 갱신 예제

서로 다른 목적지를 가진 열차를 연결을 해주는 예제

  1. 첫번째 열차는 A에서 B로 이동한다 (B가 목적지)

  2. 두번째 열차는 B에서 C로 이동한다 (C가 목적지)

  3. 첫번째 열차와 두번째 열차를 연결하게 될 경우 첫번째 열차의 목적지는 B가아닌 C로 갱신이 된다.

    1. 이렇게 값이 변경될 경우 파괴적인 갱신이 이루어지게 된다.

    2. 그래서 테스트는 어떻게하냐... 책으로 끝내줘 제발

 // 파괴적인 갱신과 함수형
    // 귀찬아서...
    @Getter
    @ToString
    @NoArgsConstructor
    static class TrainJourney {
        public int price;
        public TrainJourney onward;
        public TrainJourney(int p, TrainJourney t) {
            price = p;
            onward = t;
        }
    }
    // 파괴적인 갱신 == 왜??????
    static TrainJourney link(TrainJourney a, TrainJourney b) {
        if (a == null)
            return b;
        TrainJourney t = a;
        while(t.onward != null) {
            t = t.onward;
        }
        t.onward = b;
        return a;
    }
    // 함수적
    static TrainJourney append(TrainJourney a, TrainJourney b) {
        return a == null ? b : new TrainJourney(a.price, append(a.onward, b));
    }

    // 테스트 어떻게함??
    @Test
    public void destroyFunctional() {
        TrainJourney number1 = new TrainJourney();
        TrainJourney bSite = new TrainJourney();
        TrainJourney aSite = new TrainJourney(1000, number1);
        TrainJourney newSite = link(aSite, bSite);

        log.info("newSite : {}", newSite);
        log.info("newSite.getOnward() : {}", newSite.getOnward());
        log.info("newSite.getOnward().getOnward() : {}", newSite.getOnward().getOnward());
    }

트리를 사용한 다른 예제

  1. 자신에 결과를 반영 후 반환해주는 Update

    1. 자신이 갱신됨으로 참조 투명성이 위배된다.

    2. 영속자료구조 위배

  2. 새로운 객체에 결과를 반영 후 반환해주는 FUpdate
    1. 같은 인수를 집어넣을 경우 반환값이 동일한 참조 투명성 위배가 안되었으며
    2. 영속자료구조
static class Tree {
        private String key;
        private int val;
        private Tree left, right;
        public Tree(String k, int v, Tree l, Tree r) {
            key = k; val = v; left = l; right = r;
		}

    public void setKey(String key) {
                this.key = key;
    }

    public void setVal(int val) {
                this.val = val;
    }

    public void setLeft(Tree left) {
                this.left = left;
    }

    public void setRight(Tree right) {
                this.right = right;
    }
}
    
    static class TreeProcessor {
        public static int lookup(String k, int defaultval, Tree t) {
            if(t == null)
                return defaultval;
            if(k.equals(t.key))
                return t.val;
            return lookup(k, defaultval, k.compareTo(t.key) < 0 ? t.left : t.right);
        }
    }
    // 현재 있는 Tree에 값을 반영 후 반환해주는 Update
    public static Tree update(String k, int newVal, Tree t) {
        if( t == null) {
            t = new Tree(k, newVal, null, null);
        } else if(k.equals(t.key)) {
            t.val = newVal;
        } else if(k.compareTo(t.key) < 0 ) {
            t.left = update(k, newVal, t.left);
        } else {
            t.right = update(k, newVal, t.right);
        }
        return t;
    }
    // 새로운 Tree를 생성하여 값을 반영 후 반환해주는 fUpdate
    public static Tree fUpdate(String k, int newVal, Tree t) {
        return (t == null) ?
                new Tree(k, newVal, null, null) :
                k.equals(t.key) ?
                        new Tree(k, newVal, t.left, t.right) :
                        k.compareTo(t.key) < 0 ?
                                new Tree(k, newVal, fUpdate(k, newVal, t.left), t.right) :
                                new Tree(k, newVal, t.left, fUpdate(k, newVal, t.right));
    }

캐싱 or 기억화

참조 투명성을 유지하며 간단하게 오버헤드를 피할 수 있는 방법이다.
메서드에 래퍼로 캐시(HashMap 같은)를 추가하는 기법이며, 래퍼가 호출되면 인수, 결과 쌍이 캐시에 존재하는지 먼저 확인한다. 캐시에 값이 졵해마녀 캐시에 저장된 값을 반환해준다.
단 순수 함수형 해결방식은 아니다.

 

	// 캐싱
    // 인자값이 동일하다면 반환값이 같은 참조투명성은 유지되지만 실질적으로 함수형프로그래밍은 아니다. == 값이 변경되기에 파괴적 함수형...??
    final Map<Object, Integer> numberOfNodes = new HashMap<>();

    Integer computeNumberOfNodesUsingCash(String c) {
        Integer result = numberOfNodes.get(c);
        if(result != null) {
            System.out.println("not null");
            return result;
        }
        result = 5;
        numberOfNodes.put(c, result);
        return result;
    }

    @Test
    public void computeNumber() {
        Integer firstNum = computeNumberOfNodesUsingCash("abc");
        Integer secondNum = computeNumberOfNodesUsingCash("abc");
        System.out.println("firstNum : " + firstNum + ", secondNum : " + secondNum);

        Integer testOne = computeNumberOfNodesUsingCash("bbc");
        Integer testTwo = computeNumberOfNodesUsingCash("bbc");
        System.out.println("testOne : " + testOne + ", testTwo : " + testTwo);

        // 5, 5... 두번째는 not null
    }

 

git : github.com/hodolee246/ModernJavaStudy/blob/master/src/test/java/com/example/modernjava/ch19/ch19functionalProgramming.java