본문 바로가기
행위 돌아보기

함수형 사고 - [1] 왜

by simplify-len 2019. 7. 18.

우리는 객체지향에서 함수형 패러다임으로 전환된다고 생각할까?

패러다임의 전환

바로 밑에 예제를 보고 느껴보자.

package com.nealford.ft.wordfreq;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

// BEGIN java_word_freq
public class Words {
    private Set NON_WORDS = new HashSet() {{
        add("the"); add("and"); add("of"); add("to"); add("a");
        add("i"); add("it"); add("in"); add("or"); add("is");
        add("d"); add("s"); add("as"); add("so"); add("but");
        add("be");
}};

    public Map wordFreq(String words) {
        TreeMap<String, Integer> wordMap = new TreeMap<String, Integer>();
        Matcher m = Pattern.compile("\\w+").matcher(words);
        while (m.find()) {
            String word = m.group().toLowerCase();
            if (! NON_WORDS.contains(word)) {
                if (wordMap.get(word) == null) {
                    wordMap.put(word, 1);
                }
                else {
                    wordMap.put(word, wordMap.get(word) + 1);
                }
            }
        }
        return wordMap;
    }
}
// END java_word_freq

문제. 텍스트 파일을 읽고, 가장 많이 사용된 단어를 찾고, 그 단어들과 빈도를 정렬된 목록으로 출력하라!

위 코드는 전통적인 자바의 내용.
\w+ 알파벳이나 숫자를 의미하는 정규표현식.
우선 비 단어 집합을 만들고, wordFreq()메서드를 만들었다. 그 안에 키/값을 저장할 Map을 생성하고, 단어를 찾는 정규표현식을 작성.

만약 자바8로 단어 빈도 수 세기를 코딩해본다면?

public class Words {
    private Set NON_WORDS = new HashSet() {{
        add("the");
        add("and");
        add("of");
        add("to");
        add("a");
        add("i");
        add("it");
        add("in");
        add("or");
        add("is");
        add("d");
        add("s");
        add("as");
        add("so");
        add("but");
        add("be");
    }};

    // BEGIN java_wordfreq8
private List regexToList(String words, String regex) {
    List wordList = new ArrayList<>();
    Matcher m = Pattern.compile(regex).matcher(words);
    while (m.find())
    wordList.add(m.group());
    return wordList;
}

public Map wordFreq(String words) {
    TreeMap<String, Integer> wordMap = new TreeMap<>();
    regexToList(words, "\\w+").stream()
            .map(w -> w.toLowerCase())
            .filter(w -> !NON_WORDS.contains(w))
            .forEach(w -> wordMap.put(w, wordMap.getOrDefault(w, 0) + 1));
    return wordMap;
}
// END java_wordfreq8
}

세 연산을 한 번의 반복자 블록에서 실행함으로써 성능을 명료함과 맞바꾸었다. 이는 보편화된 트레이드오프지만 나는 별로 권장하고 싶지 않다.

 

명령형 프로그래밍 VS 함수형 프로그래밍

언어/런타임에 제어를 양도하기

메모이제이션(Memorization) 함수의 연산 결과를 저장해두었다가, 같은 입력이 주어지면 연산을 하지 않고 리턴하는 최적화 기법.

마치 이건 JVM과 유사하다. 시간이 갈수록 개발자는 지루한 일들을 언어나 런타임에 점점 맡기게 된다. 애플리케이션을 만들면서 직접 메모리를 제어하지 않는다는 것을 조금도 후회하지 않는다. 그런 일에 무관심해졌기 때문에 좀 더 중요한 문제들에 집중할 수 있다.

간결함

객체지향 프로그래밍 구조에 대해 생각해보라. 캡슐화, 스코핑, 가시성 등의 메커니즘은 상태 변화를 누가 볼 수 있는지에 대한 세밀한 제어를 위해 존재한다. 상태에 스레드까지 곁들이면 골칫덩이는 더욱 커진다. 이러한 메커니즘이 바로 페더스가 말하는 "움직이는 부분"이다. 함수형 언어는 이런 가변(mutable)상태를 제어하는 메커니즘을 구축하기 보다, 그런 '움직이는 부분'을 아예 제거하는 데 주력한다. 언어가 오류가 발생하기 쉬운 기능을 쉽게 노출하면 개발자가 오류를 만들 가능성이 줄어든다는 이론에 따른 것이다.

객체지향 명령형 프로그래밍언어에서, 재상용의 단위는 클래스와 그클래스들이 주고받는 통신 메시지이고, 이는 클래스 다이어그램으로 포착할 수 있다.OOP의 세계에서는 고유한 자료구조를 작성하는 것을 권장한다. 그 자료구조에 특정 동작을 메서드의 형태로 부탁해서 말이다. 함수형 프로그래밍 언어는 같은 방식으로 재사용을 달성하려 하지 않고, 최적화된 동작으로 몃몃 자료구조(list, set,map)을 이용하는 방식의 재사용을 선호한다. 개발자가 이런 방법을 잘 사용하려면, 특정 용도로 정의된 방법에 자료구조와 고계함수를 함꼐 넣어야 한다.

함수형 개발자는 적은 수의 자료구조와 그것들을 잘 이해하기 위한 최적화된 방법을 만들기를 선호한다. 객체지향형 개발자는 항상 새로운 자료구조와 그것에 부착된 메서드를 만든다. 클래스와 통신 메시지를 만드는 것이 지배적인 객체지향 패러다임이다. 모든 자료구조를 캡슐화하면 메서드 수준의 재사용보다는 큰 프래임워크 스타일의 재사용을 선호하게 된다.

 // From Apache Commons Lang, http://commons.apache.org/lang/
 public static int indexOfAny(String str, char[] searchChars) {
     if (isEmpty(str) || ArrayUtils.isEmpty(searchChars)) { // 안전장치
      return INDEX_NOT_FOUND;
     }
     int csLen = str.length(); // 초기화
     int csLast = csLen - 1;
     int searchLen = searchChars.length;
     int searchLast = searchLen - 1;
     for (int i = 0; i < csLen; i++) { // 외부 반복
  char ch = str.charAt(i);
         for (int j = 0; j < searchLen; j++) { // 내부 반복
      if (searchChars[j] == ch) { // 결정, 결정 또 결정
  if (i < csLast && j < searchLast && CharUtils.isHighSurrogate(ch)) {
                     if (searchChars[j + 1] == str.charAt(i + 1)) {
  return i;
                      }
                  } else {
                      return i;
                  }
              }
          }
      }
      return INDEX_NOT_FOUND;
 }

이걸 스칼라로 바꾸면 어떻게 될까?

def firstIndexOfAny(input: String, searchChars: Seq[Char]): Option[Int] = {
      def indexedInput = (0 until input.length).zip(input)

      val result = for (pair <- indexedInput;
                        char <- searchChars
                        if char == pair._2) yield pair._1

      result.headOption
    }

만약, 해당되는 모든 값의 리스트를 반환하고 싶다면 어떻게 할까?

def firstIndexOfAny(input: String, searchChars: Seq[Char]): Seq[Int] = {
      def indexedInput = (0 until input.length).zip(input)

      val result = for (pair <- indexedInput;
                        char <- searchChars
                        if char == pair._2) yield pair._1
    }

위 내용은 함수형 사고의 1장에서 발췌했습니다.

자세한 내용은 책을 참고해주세요.

댓글