함수형 프로그래밍

초보개발·2022년 4월 28일
0

JAVA

목록 보기
7/15

배워서 바로 쓰는 스프링 프레임워크를 참고한 내용입니다.

함수형 프로그래밍

자바 8에는 함수형 프로그래밍을 지원하는 람다식, 메서드 참조 등과 Stream API가 추가되었다.

  • 프로그램은 문제의 일부분을 해결하는 여러 함수의 집합으로 작성
  • 각 함수는 가변 변수를 읽지 않음
  • 함수로 문제를 풀기 위해 WHAT을 기술

Lambda식

자바 8에는 함수형 인터페이스 개념이 도입되었다. 함수형 인터페이스는 추상 메서드가 한개만 존재하는 인터페이스이다.

  • java.lang.Runnable, java.util.concurrent.Callable

람다식은 함수형 인터페이스 타입을 인수로 받는 메서드에 전달할 수 있는 익명함수를 표현한다.

(<arg1>, <arg2>, ...) -> { <method-body> }
  • <arg>: 함수형 인터페이스의 추상 메서드가 받는 인수
  • <method-body>: 추상 메서드 구현

Comparator 대신 람다식을 사용한 sort

public class SortCarsWithLambda {
	public static void main(String args[]) {
    	List<Car> cars = new ArrayList<Car>();
        cars.add(new Car(10));
        ...
        // 람다식을 sort 메서드로 넘김
     	cars.sort((Car c1, Car c2) -> {
        	if(c1.getTopSpeed() == c2.getTopSpeed())
            	return 0;
            else if(c1.getTopSpeed() > c2.getTopSpeed())
            	return 1;
            else
            	return -1;
        })	    
    }
}

람다식의 c1, c2는 compare 메서드의 인수에 해당한다. 그리고 메서드 인수의 타입은 컴파일러가 추론할 수 있어서 생략할 수 있다.

cars.sort((c1, c2) -> {
     if(c1.getTopSpeed() == c2.getTopSpeed())
           return 0;
     else if(c1.getTopSpeed() > c2.getTopSpeed())
           return 1;
     else
           return -1;
})	  

람다식이 여러줄로 되어있어 가독성이 안좋다. 따라서 별도의 메서드로 넘기고 람다식에서 메서드를 호출하는 방식도 가능하다.

cars.sort((c1, c2) -> compareCars(c1, c2)); // 메서드 호출
...
private static int compareCars(Car c1, Car c2) {
	if(c1.getTopSpeed() == c2.getTopSpeed())
         return 0;
    ...
}

고차 함수

또한 함수형 프로그래밍에서는 고차 함수를 작성할 수 있다고 한다. 고차함수는 하나 이상의 함수를 입력 파라미터로 받거나 함수를 반환하는 함수를 말한다.

고차함수를 만들기 위해 사용할 몇 가지 함수형 인터페이스가 존재한다.

  • Function<T, R>: T 타입 인수를 받고 R 타입의 결과를 반환하는 함수
  • BiFunction<T, U, R>: 두 인수를 받고 R 타입의 결과를 반환하는 함수
  • Consumer: T 타입의 인수를 받지만 아무것도 반환하지 않는 함수
  • BiConsumer<T, U>: 두 인수를 받지만 아무것도 반환하지 않는 함수

간단한 함수들

public class MyFunctions {
  	// concatFn이 두 String을 인수로 받아 String 타입을 반환
    private static BiFunction<String, String, String> concatFn = (prefix, suffix) -> prefix + " " + suffix;
	
  	// String을 인수로 받아 Integer 타입의 값 반환
    private static Function<String, Integer> hashFn = input -> input.hashCode();
	
  	// printFn은 Object 인수를 받아서 아무것도 반환하지 않음
    private static Consumer<Object> printFn = input -> System.out.println(input);
	
    public static void main(String[] args) {
        printFn.accept(concatAndHash("Hello ", "world"));
    }

    private static int concatAndHash(String prefix, String suffix) {
        return hashFn.apply(concatFn.apply(prefix, suffix));
    }
}

고차함수

public class MyHigherOrderFunctions {
    private static Function<String, Function<String, String>> concatFn =
            prefix -> {
                Function<String, String> addSuffixFn = suffix -> {
                    return prefix + " " + suffix;
            };
        return addSuffixFn;
    };
}

concatFn은 String 인수를 받아 Function<String, String> 타입의 함수를 반환한다는 뜻이다. 내부의 prefix로 suffix를 인수로 받는 addSuffixFn 함수를 만들어 반환하고 addSuffixFn은 prefix와 suffix 문자열을 연결한 문자열을 반환한다.
concatFn 함수가 다른 함수를 반환하므로 고차 함수에 속한다!

public class MyHigherOrderFunctions {
    private static Function<String, Function<String, String>> concatFn =
            prefix -> {
                return suffix -> {
                    return prefix + " " + suffix;
            };
    };
}  

불필요한 부분을 줄여 이렇게 간단하게 작성도 가능하다.

Stream API

스트림 API를 이용해 stream source로부터 요소의 스트림을 받아 작업할 수 있다. 데이터 구조(컬렉션, 배열..), 입출력 소스와 스트림이 소비하도록 원소를 생성하는 generator function은 스트림 소스 역할이 가능하다.
스트림은 데이터를 저장하지 않고 스트림 소스에서 받은 각각의 원소를 처리하는 연산 pipeline이다.

Stream을 이용한 홀수의 합

public class SumOfOddNumbers {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        IntStream intStream = Arrays.stream(numbers);
        int sum = intStream.filter(n -> n % 2 != 0).sum();
        System.out.println("sum = " + sum);
    }
}

IntStream을 반환하는 Arrays의 stream 메서드에 numbers 배열을 전달하여 스트림 소스로 만들었다.

  • filter: 주어진 술어(predicate, bool 값을 반환하는 함수)를 만족하는 원소만 포함하는 다른 스트림을 반환, 여기서는 홀수로 구성된 새로운 스트림을 반환함

스트림 파이프라인은 하나 이상의 중간 연산과 하나의 최종 연산으로 구성된다.

  • 중간 연산: 스트림 생성 -> filter
  • 최종 연산: 값이나 부수효과(side-effect)를 생성 -> sum

mapToInt

public class SumOfStudentsAges {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("name1", 15));
        students.add(new Student("name2", 14));
        students.add(new Student("name3", 17));
        students.add(new Student("name4", 16));

        int sumOfAges = students.stream()
                .mapToInt(s -> s.getAge())
                .sum();
    }
}

리스트 students에 대해 stream 메서드를 호출하면 Stream<Student>를 반환한다. mapToInt 메서드는 스트림 원소에 적용할 수 있는 람다식을 인수로 받는다. mapToInt에 전달되는 람다식은 반드시 정수로 평가되어야 한다.

map, collect, forEach

public class NamesOfStudentsList {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("name1", 15));
        students.add(new Student("name2", 14));
        
        List<String> names = students.stream() // Student 스트림
                .map(s -> s.getName()) // 학생 이름의 스트림을 얻음
                .collect(Collectors.toList()); // 스트림에 있는 학생 이름을 리스트에 모으고 names에 저장
        
        names.stream()
  			.forEach(name -> System.out.println("name = " + name));
    }
}

reduce

public class ConCatStudentNames {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("name1", 15));
        students.add(new Student("name2", 14));
        
        String combinedNames = students.stream()
                .map(s -> s.getName())
                .reduce("", (combinedNameStr, name) ->
                        combinedNameStr.concat(name + ","));
        System.out.println("combinedNames = " + combinedNames);
    }
}

reduce 메서드는 각 이름을 콤마로 구분한 리스트를 반환한다.
reduce 메서드가 받는 인수

  • BinaryOperator 함수: 인수를 2개 받는 함수, 첫번째 인수는 이전 스트림 원소까지 이 함수를 적용해 반환 받은 결과값, 두번째 인수는 현재 스트림 원소
  • 항등원 원소: 함수의 초기값 또는 스트림에 원소가 없을때 반환할 디폴트 값

지연 계산

스트림 파이프라인에서 중간 연산은 지연 계산되는데 최종 연산이 호출될 때까지 중간 연산은 계산하지 않는다.

순차 스트림과 병렬 스트림

순차 스트림은 원소를 단일 스레드에서 순차적으로 처리하는 것을 말하고 병렬 스트림은 원소를 여러 스레드에서 병렬로 처리하는 것을 말한다.
java.util.CollectionparallelStream을 오출해 병렬 스트림을 얻을 수 있다.

public class MyparallelStream {
  public static void main(String[] args) {
      List<String> names = Arrays.asList("James", "John", "Robert");
      System.out.println("Serial stream result: ");
      names.stream()
              .forEach(e -> System.out.println("e = " + e));

      System.out.println("\nParallel stream result:");
      names.parallelStream()
              .forEach(e -> System.out.println("e = " + e));
  }
}


순차 스트림과 병렬 스트림의 결과 값이다. 순차 스트림의 경우 정의된 순서대로 출력이 되지만, 병렬 스트림은 순서가 다르다. 병렬 스트림에서 forEach 메서드의 동작이 비결정적이기 때문이다. 병렬 스트림의 경우 각 forEach 메서드 실행이 같은 입력에 대해 다른 출력을 만들어낼 수 있다는 뜻이다.

reduce 메서드의 경우 결정적 메서드이므로 순차 스트림이나 병렬 스트림이나 관계없이 결과가 항상 같다.

메서드 참조

메서드 참조는 메서드나 생성자를 가리키는 참조를 말한다. 람다식이 메서드나 생성자 호출을 단순화할 수 있다면 메서드 참조는 람다식을 대신할 수 있다.

아래 문법을 이용해 메서드 참조를 나타낼 수 있다.

<object-or-class>::<method-name>
  • object-or-class: 참조하려는 메서드가 들어 있는 클래스나 객체
    -method-name: 메서드 이름

    생성자 참조는 <class>::new를 이용한다. 여기서 class는 참조한려는 생성자가 있는 클래스이다.

0개의 댓글