[함수형사고] 2. 전환

함수형 코드를 작성하기 위해서는, 함수형 언어인 스칼라나 클로저로의 전환이 필요한 것이 아니라 문제에 접근하는 방식의 전환이 필요하다.

1.일반적인 예로 말할 수 있는 것

= JVM

2. 명령형 처리

명령형 프로그래밍이란 상태를 변형하는 일련의 명령들로 구성된 프로그래밍 방식을 말한다. 전형적인 For루프가 명령형 프로그래밍의 훌룡한 예. 초기 상태를 설정하고, 되풀이할 때마다 일련의 명령을 실행.

명령형 프로그래밍과 함수형 프로그래밍의 차이는, 통상적인 문제와 그에 대한 명령형의 해답을 살펴보면된다. 어떤 이름 목록에서, 한 글자로 된 이름을 제외한 모든 이름을 대문자화해서 쉼표로 연결한 문자열을 구하려 한다고 해보자.

어떻게 작성할까?

package com.nealford.ft.trans;

import java.util.List;

public class TheCompanyProcess {
    public String cleanNames(List<String> listOfNames) {
        StringBuilder result = new StringBuilder();
        for(int i = 0; i < listOfNames.size(); i++) {
            if (listOfNames.get(i).length() > 1) {
                result.append(capitalizeString(listOfNames.get(i))).append(",");
            }
        }
        return result.substring(0, result.length() - 1).toString();
    }

    public String capitalizeString(String s) {
        return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
    }
}

목록 전체를 처리해야 하기 때문에, 각 이름마다 이름이 한 글자보다 긴가를 확인하고, 대문자화된 이름을 결과에 추가한다. 마지막 이름에는 쉼표를 포함하면 안 되므로, 마지막 결과에서 끝 글자를 잘라낸다.

다시 정리하면, 명령형 프로그래밍은 개발자로 하여금 루프 내에서 연산하기를 권장, 나는 이 예제에서 세가지를 실행,

한 글자짜리 이름을 필터했고, 목록에 남아 있는 이름들을 대문자로 변형하고, 이 목록을 하나의 문자열로 변환했다.

우선 이 세가지 작업을 목록에 적용할 “유용할 작업들”이라고 정의하자. 명령형 언어에서는 세 가지 작업에 모두 저수준의 메커니즘을 사용해야한다. 함수형 언어들은 이런 작업을 위한 몃몃 도우미들을 제공한다.

자, 이제 위 코드를 함수형으로 처리해보자.

3.함수형 처리

함수형 프로그래밍은 프로그램을 수학 공식을 모델링하는 표현과 변형으로 기술하며, 가변 상태를 지양한다. 함수형 프로그래밍 언어는 명령형 언어와는 다르게 문제를 분류한다. 앞에서 언급한 필터, 변형, 변환 등의 논리적 분류도 저수준의 변형을 구현하는 함수들이었다. 개발자는 고계함수에 매개변수로 주어지는 함수를 이용하여 저수준의 작업을 커스터마스징할 수 있다.

다음과같은 의사코드를 사용하여 이 문제를 개념화할 수 있다.

listOfEmps
 -> filter(x.length > 1)
 -> transform(x.capitalize)
 -> convert(x + "," + y)

함수형 언어는 이런 개념화된 해법을 세부사항에 구애받지 않고 모델링할 수 있게 해준다. 다음은 회사프로세스를 스칼라로 구현

 val employees = List("neal", "s","stu",) 

 val result = employees
 .filter(_.length( )> 1)
 .map(_.capitalize)
 .reduce(_ + "," + _)

자바 8에서는 다음과같이 작업한다.

 package com.nealford.ft.trans8;

 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;

 public class Process {

     // BEGIN java8_process
     public String cleanNames(List<String> names) {
         if (names == null) return "";
         return names
                 .stream()
                 .filter(name -> name.length() > 1)
                 .map(name -> capitalize(name))
                 .collect(Collectors.joining(","));
     }

     private String capitalize(String e) {
         return e.substring(0, 1).toUpperCase() + e.substring(1, e.length());
     }
 // END java8_process


     // BEGIN java8_process_parallel
     public String cleanNamesP(List<String> names) {
         if (names == null) return "";
         return names
                 .parallelStream()
                 .filter(name -> name != null)
                 .filter(n -> n.length() > 1)
                 .map(e -> capitalize(e))
                 .collect(Collectors.joining(","));
     }
 // END java8_process_parallel

 }

reduce()대신 collect()를 쓴이유는 자바 String클래스에 대해서는 좀 더 효율적이기 떄문이다. 자바 런타임은 똑똑해서 null 체크와 길이 필터를 하나의 연산으로 묶어주기 때문에, 개발자는 아이디어를 간단명료하게 표현하면서도 성능이 좋은 코드를 작성할 수 있다.

함수형 프로그래밍의 주요 개념은 다음을 포함한다. 함수형 사고로의 전환은 어떤 경우에 세부적인 구현에 뛰어들지 않고 이런 고수준 추상 개념을 적용할지를 배우는 것이다.

그렇다면 고수준의 추상적 사고로 얻는 이점은 무엇일까? 첫째로, 문제의 공통점을 고려하여 다른 방식을 분류하기를 권장한다는 것 둘쨰로, 런타임이 최적화를 잘할 수 있도록 해준다는 것이다. 어떤 경우에서는, 결과가 변화지 않는 한, 작업 순서를 바꾸면 더 능률적이 된다. 셋째로, 개발자가 엔진 세부사항에 깊이 파묻힐 경우 불가능한 해답을 가능하게 한다. 일례로 개발자가 저수준의 반복과정을 제어해야 하기 떄문에, 스레드 관련 코드가 문제 해결코드에 섞여 들어가게 된다.

2.2 사례 연구: 자연수의 분류

고대 그리스의 수학자 니코마코스는 자연수를 과잉수, 완전수, 부족수로 나누는 분류법을 고안. 완전수는 자신의 모든 양의 약수의 합과 같다. 여기서 양의 약수란 곱해서 대상의 값이 나오는 두 수 중 자신을 제외한 수들을 말한다. 예를 들자면 6은 약수가 1,2,3,6이고 6=1+2+ 이므로 완전수이다.

분류 조건
완전수 진약수의 합 = 수
과잉수 진약수의 합 > 수
부족수 진약수의 합 < 수

수학적 개념 하나인 진약수의 함을 사용하면 구현하는데 도움이 된다. 진약수의 합은 한 수의 모든 약수 중 자신을 제외한 합으로 정의된다. 즉 완전수의 조건과 비교할 때 sum-number = number라 하지 않고, aliquotSum == number로 더 쉽게 표현

코드

package com.nealford.ft.number_classifier;

// BEGIN imp_classifier
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class ImpNumberClassifierSimple {
    private int _number;                          //<1>
    private Map<Integer, Integer> _cache;         //<2>

    public ImpNumberClassifierSimple(int targetNumber) {
      _number = targetNumber;
      _cache = new HashMap<>();
    }

    public boolean isFactor(int potential) {
      return _number % potential == 0;
    }

    public Set<Integer> getFactors() {
        Set<Integer> factors = new HashSet<>();
        factors.add(1);
        factors.add(_number);
        for (int i = 2; i < _number; i++)
            if (isFactor(i))
                factors.add(i);
        return factors;
    }

    public int aliquotSum() {                     // <3>
        if (_cache.get(_number) == null) {
            int sum = 0;
            for (int i : getFactors())
                sum += i;
            _cache.put(_number, sum - _number);
        }
        return _cache.get(_number);
    }

    public boolean isPerfect() {
        return aliquotSum() == _number;
    }

    public boolean isAbundant() {
        return aliquotSum() > _number;
    }

    public boolean isDeficient() {
        return aliquotSum() < _number;
    }
}
// END imp_classifier

OOP 언어는 캡슐화를 이점으로 사용하기 때문에 객체지향적인 세계에서는 내부 상태의 사용이 보편적이며 권장된다. 상태를 분리해 놓으면 값을 삽입할 수 있기 떄문에 단위 테스팅 같은 엔지니어링이 쉬워진다.

이걸 함수형으로 변경하면?

위 코드의 목적은 테스트 용이성이다.

공유 상태를 최소화하고 싶다면 어떻게 할까? 그러려면 멤버 변수를 없애고 필요한 값들을 매개변수로 넘겨야 한다.

package com.nealford.ft.number_classifier;

// BEGIN functional_number_classifier
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class NumberClassifier {

    public static boolean isFactor(final int candidate, final int number) {   //<1>
        return number % candidate == 0;
    }

    public static Set<Integer> factors(final int number) {                    //<2>
        Set<Integer> factors = new HashSet<>();
        factors.add(1);
        factors.add(number);
        for (int i = 2; i < number; i++)
            if (isFactor(i, number))
                factors.add(i);
        return factors;
    }

    public static int aliquotSum(final Collection<Integer> factors) {
        int sum = 0;
        int targetNumber = Collections.max(factors);
        for (int n : factors) {                                               //<3>
            sum += n;
        }
        return sum - targetNumber;
    }

    public static boolean isPerfect(final int number) {
        return aliquotSum(factors(number)) == number;
    }
                                                                              //<4>
    public static boolean isAbundant(final int number) {
        return aliquotSum(factors(number)) > number;
    }

    public static boolean isDeficient(final int number) {
        return aliquotSum(factors(number)) < number;
    }
}
// END functional_number_classifier

위 코드에서는 sum에 대한 캐시를 구현하지 않았다. 캐시는 내부 상태의 유지를 의미하고, 이 버전에는 마당히 내부 상태를 유지할 자리가 없다. 합을 저장할 내부 상태가 없기 때문에 항상 계산을 해야 한다. 4장에서 볼 버전에서는 메모이제이션을 통해 이런 상태성을 보전하면서 캐시를 회복하지만, 당분간은 캐시를 없애기로 하자.

자바8을 사용한 자연수 분류기.

package com.nealford.ft.number_classifier8;

// BEGIN number_classifier_java8
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static java.lang.Math.sqrt;
import static java.util.stream.Collectors.toList;
import static java.util.stream.IntStream.range;

public class NumberClassifier {

    // BEGIN java8_filter
    public static IntStream factorsOf(int number) {
        return range(1, number + 1)
                .filter(potential -> number % potential == 0);
    }
    // END java8_filter

    public static int aliquotSum(int number) {
        return factorsOf(number).sum() - number;
    }

    public static boolean isPerfect(int number) {
        return aliquotSum(number) == number;
    }

    public static boolean isAbundant(int number) {
        return aliquotSum(number)> number;
    }

    public static boolean isDeficient(int number) {
        return aliquotSum(number) < number;
    }

}
// END number_classifier_java8

    // BEGIN java8_filter_fast
    //    public static List fastFactorsOf(int number) {
    //        List<Integer> factors = range(1, (int) (sqrt(number) + 1))
    //                .filter(potential -> number % potential == 0)
    //                .boxed()
    //                .collect(toList());
    //        List factorsAboveSqrt = factors
    //                .stream()
    //                .map(e -> number / e).collect(toList());
    //        factors.addAll(factorsAboveSqrt);
    //        return factors.stream().distinct().collect(toList());
    //    }
    // END java8_filter_fast

함수형 자바를 사용한 자연수 분류기?

package com.nealford.ft.number_classifier_functional_java;

// BEGIN number_classifier_functional_java
import fj.F;
import fj.data.List;
import static fj.data.List.range;

public class NumberClassifier {

    // BEGIN functional_java_filter
    public List<Integer> factorsOf(final int number) {
        return range(1, number + 1)                                // <1>
                .filter(new F<Integer, Boolean>() {
                    public Boolean f(final Integer i) {
                        return number % i == 0;
                    }
                });                                                // <2>
    }
    // END functional_java_filter

    // BEGIN functional_java_fold
    public int aliquotSum(List<Integer> factors) {                // <3>
        return factors.foldLeft(fj.function.Integers.add, 0) - factors.last();
    }
    // END functional_java_fold

    public boolean isPerfect(int number) {
        return aliquotSum(factorsOf(number)) == number;
    }

    public boolean isAbundant(int number) {
        return aliquotSum(factorsOf(number)) > number;
    }

    public boolean isDeficient(int number) {
        return aliquotSum(factorsOf(number)) < number;
    }
}
// END number_classifier_functional_java

factors.foldLeft()메서드의 의미는 다음과 같다.

  1. 초기 값을 목록의 첫 번째 요소와 결합
  2. 그 결과를 가지고 다음 요소와 같은 방법으로 결합
  3. 이 작업을 목록 끝까지 계속

공통된 빌딩블록

앞에서 언급했던 유용한 작업들은 자연수 분류기의 모든 함수형 버전에 등작한다. 이런 유용한 작업들은 함수형 언어 및 프레임워크 어디서나 존재.

필터

목록에 할 수 있는 흔한 작업은 필터하는 것, 사용자가 정한 조건으로 목록에 있는 요소들을 필터하여 더 작은 목록을 만드는 것,

필터 작업을 할 때에는 필터 조건에 따라서 원래 목록보다 작은 목록을 생성한다.

컬렉션의 각 요소에 같은 함수를 적용하여 새로운 컬렉션을 만드는 것.

폴드/리듀스

foldLeft/reduce캐터모피즘이라는 목록조작 개념의 특별한 변형. reduce 함수는 주로 초기값을 주어야 할 때 사용, fold는 누산기에 아무것도 없는 채로 시작.

fold의 경우 scan()과 유사하다고 판단된다.