-
챕터[19] 함수형 프로그래밍 기법카테고리 없음 2020. 10. 22. 00:44
서론
일급 시민 고차원 함수 커링 부분 적용
영속 자료구조
자바 스트림을 일반화하는 게으른 평가와 게으른 리스트
패턴 매칭, 자바에서 패턴 매칭을 흉내 내는 방법
참조 투명성과 캐싱
일급 함수
-
함수형 프로그래밍에서 함수를 마치 일반값처럼 사용하여 인수를 전달하거나, 결과를 반환하거나 자료구조를 저장할 수 있어 일반값처럼 취급하는 함수를 일급 함수라고한다.
-
자바 8이 이전 버전과 구분될 수 있는 특징중 하나가 일급 함수를 지원한다는 점이 있다.
-
추가로 자바 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에서 말씀드린 참조 투명성에 위배가 되어 결국에는 부작용이 생기고 만다.
이러한 경우를 파괴적인 갱신 이라고 부르며 이를 보완하기 위해서 생긴 방법으로는 기존의 변수를 내비두고 새로운 변수를 생성하여 값을 복사한 후 결과를 반영시켜 반환해주는 부작용 없는 방식을 함수형 갱신이라고 한다.파괴적인 갱신 예제
서로 다른 목적지를 가진 열차를 연결을 해주는 예제
-
첫번째 열차는 A에서 B로 이동한다 (B가 목적지)
-
두번째 열차는 B에서 C로 이동한다 (C가 목적지)
-
첫번째 열차와 두번째 열차를 연결하게 될 경우 첫번째 열차의 목적지는 B가아닌 C로 갱신이 된다.
-
이렇게 값이 변경될 경우 파괴적인 갱신이 이루어지게 된다.
-
그래서 테스트는 어떻게하냐... 책으로 끝내줘 제발
-
// 파괴적인 갱신과 함수형 // 귀찬아서... @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()); }
트리를 사용한 다른 예제
-
자신에 결과를 반영 후 반환해주는 Update
-
자신이 갱신됨으로 참조 투명성이 위배된다.
-
영속자료구조 위배
-
- 새로운 객체에 결과를 반영 후 반환해주는 FUpdate
- 같은 인수를 집어넣을 경우 반환값이 동일한 참조 투명성 위배가 안되었으며
- 영속자료구조
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 }
-