함수형 프로그래밍

cutiepazzipozzi·2023년 3월 30일
1

지식스택

목록 보기
14/35
post-thumbnail

앞서 읽던 이펙티브 자바에 함수형 프로그래밍이 등장했다. 객체지향 프로그래밍과 같은 프로그래밍 방식의 한 종류인데, 자바에서는 이렇게 코드를 작성해본 적이 없기도 하고 개념도 잘 모르기 때문에 기록해보고자 한다.

함수형 프로그래밍의 개념은 사실 프로그래밍의 개념이 등장했을 때보다 더 빨리 등장했고, 람다 계산법은 1930년에 발명되었다.

패러다임

(ex. 함수형 프로그래밍, 객체지향 프로그래밍, 구조지향 프로그래밍 밖에..)
= 프로그래밍을 하는 방법 => 따라서 언어에 독립적임

  • 명령형 프로그래밍: 객체지향 프로그래밍, 구조지향 프로그래밍
    = (결과를 얻기 위해) 어떻게 할 건지 설명하는 방식
  • 선언형 프로그래밍: 함수형 프로그래밍
    = 무엇을 나타낼지 설명하는 방식
    = (작업 자체에 초점을 두어 원하는 결과를 내기 위해 노력하는 방식)

함수형 프로그래밍

= 선언적인 방식으로 구현되어 흐름 제어를 명시적으로 기술하지 않고 프로그램 로직을 표현하는 프로그래밍
= 함수단위로 개발을 하는 기법

  • 동작하는 부분을 최소화하여 코드의 이해를 돕는다.
  • 흐름 제어를 추상화하고 데이터 흐름을 설명하는 코드 라인을 사용

함수형 프로그래밍의 조건

  • 1급 객체(함수)
    자바에서는 함수형 인터페이스(추상메서드 only 하나)를 통해 구현함.

    일급 객체의 조건!

    1.전달인자로 전달 가능
    2.동적 프로퍼티 할당이 가능
    3.변수 or 데이터 구조 안에 담기 가능
    4.return값으로 활용 가능
    5.할당 시 (이름에 관계없이) 고유 객체로 구별 가능

  • 순수 함수(pure function)
    = 외부 요인에 영향을 받지 않는 함수
    = 같은 입력 -> 같은 출력 반환
    -> 부작용이 없는 함수 (다른 요인에 의한 결과 변화 X)

//ex.
public class MutateState {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Ajay", "Jaya", "Bruce");
        List<String> result = new ArrayList<>();
        names.stream()
                .filter(name -> name.length() == 4)
                .map(String::toUpperCase)
                .forEach(nameInUpperCase -> result.add(nameInUpperCase));
        System.out.println(result); //[AJAY, JAYA]
    }
}
//람다 표현식에서 forEach를 활용해 마지막에 변하기 쉬운 리스트에 값을 대문자로 바꿔 넣어주고 있다
//이 점이 이 람다 표현식을 순수하지 못한 함수로 만든다 (결과 변화)
  • 고차 함수
    = 함수를 일급 객체로 취급한다고 하고, 함수를 인자로 받거나 결과로 반환하는 함수
    -> 인자로 함수를 전달 O
    -> return값으로 함수 사용 O

  • 익명 함수 (람다식과 연관!)
    = 이름이 없는 함수(람다식으로 표현!)
    = 함수 호출 or 반환 시 상수 표현식으로 작성된 함수

  • 메서드 체이닝 방식으로 함수 연결
    = 말 그대로 메서드를 엮어서 계속해서 사용하는 방법

//ex.
public class ChainingStudent {
	private int age;
    private String name;
    
    //기존의 setter와는 다르게 반환을 객체로 해준다
    public int ChainingStudent setAge(int age) {
    	this.age = age;
        return this;
    }
    public int ChainingStudent setName(String name) {
    	this.name = name;
        return this;
    }
    public int getAge() {
    	return this.age;
    }
}

@Test
public void testChaining() {
	ChainingStudent st = new ChainingStudent();
    st.setAge(24).setName("SK");
    
    System.out.print(st.getAge()); //24
}

그래서 함수형 프로그래밍의 장점이 뭘까?

  • 대규모 병렬 처리가 쉽다.
  • 변하지 않는다
    (프로그램의 흐름에서 상태가 변하지 않으면 함수 호출이 각각 따로 실행되므로 병렬 처리 시 부수 효과가 거의 없음)
    (** 교착상태를 막아줌 -> 정리!!!)
  • 데이터의 흐름으로 코드가 작성되기 때문에 가독성이 좋고 명확하다.
    (이 장점에 람다, 스트림이 사용되는 것도 한몫할듯)

람다 (함수형 프로그래밍 지원을 위해)

= 익명 함수 형태로 구현

  • ->를 통해 매개변수를 함수 body로 전달
    (불필요한 루프문의 삭제로 재활용이 용이하며, 성능이 좋아짐)
  • but 디버깅 시 함수 콜스택 추적이 어렵고 남발하면 코드가 이해하기 어려움
  • 또한 모든 원소를 전부 순회해야 하는 경우에는 람다식이 조금 느릴 수 있다!

그 쓰임에 대해 알아보자.

interface Calculator {
	int sum(int a, int b);
}

public Class Cal {
	public static void main(String[] args) {
    	Calculator c = (int a, int b) -> a+b; //이게 람다함수!!
        //위를 Calculator c = (a, b) -> a+b; 로 바꿔도 됨
        //Integer.sum()과 동일하므로 c = Integer::sum; 으로 바꿔도 됨
        int res = c.sum(1,2);
    }
}
//여기서 인터페이스의 메서드가 하나여야만 람다함수를 사용할 수 있다!
//그래서 @FunctionalInterface 어노테이션을 활용하면 좋음!
//(구현상의 오류를 방지하기 위함)
  • 함수형 프로그래밍을 위해 제공되는 인터페이스를 활용해보자.
    (ex. Bifunction, BinaryOperator)
    Bifunction<Integer, Integer, Integer>은 순서대로 입력 2개, 출력 1개임을 의미해준다. 여기서 Bifunctionapply메서드를 활용하면 (a, b)->a+b가 실행된다.
    BinaryOperator는 입출력의타입이 동일할 때 간단하게 표현하기 위해 사용된다.
    BinaryOperator<Integer> bo = (a,b)->a+b

+링크참조

+링크참조2

++스트림

= '흐름'이란 의미
= 함수형 프로그래밍에서 단계적으로 정의된 계산을 처리하기 위한 인터페이스

  • 데이터가 물결처럼 흘러가며 필터링 과정을 통해 변경되어 return되기 때문에 이렇게 불리게 됨
  • 스트림생성().중간연산().최종연산() => 메서드 체이닝 형태로 구현!

간단히 예를 하나 들어보자.
주어진 정수 배열 int[] data = {1,3,5,2,4,6,7,9}가 있다.

이 배열에서 짝수만을 뽑아 역순으로 정렬하는 코드를 작성해보자.
원래와 같다면,

List<Integer> even = new ArrayList<>();
for(int i = 0; i < data.length; i++) {
	if(data[i]%2==0) even.add(data[i]);
}
Collections.sort(even, Comparator.reverseOrder());

int[] res = new int[even.length];
for(int i = 0; i < res.length; i++) {
	res[i] = even.get(i);
}

이만큼이나 긴 코드를 작성해야 한다.

그러나 stream을 사용하면,

int[] res = Arrays.stream(data).boxed().filter((a)->a%2==0).sorted(Comparator.reverseOrder()).mapToInt(Integer::intValue).toArray();

이렇게 메서드 체이닝 형태로 한방에 끝난다!

  • boxed() = Intstream을 Integer의 Stream으로 변경한다.
    mapToInt(Integer::intValue)로 Integer의 Stream을 IntStream으로 변경한다.
  • Iterating (for, while등의 반복문을 대신해줌)
    ex. anyMatch(e->e.contains("a"))
  • Filtering (주어진 조건을 만족하는 요소를 고름)
    ex. filter(e->e.contains("a"))
  • Mapping (주어진 조건을 만족하는 요소들만 Stream으로 묶어줌)
    ex. ~.stream().map(e->Paths.get(e))
    (여기서 Paths는 Stream)
  • Matching (어떤 조건에 맞는 요소가 있는지 boolean 형태로 반환)
    ex. anyMatch, allMatch, noneMatch
  • Reduction (특정 연산에 의해 요소들의 수를 줄이기 위해??)
    두 개의 인자(시작값, 연산자)를 받음
    ex.
    List<Integer> num = Arrays.asList(1,2,3);
    Integer reduceNum = num.stream().reduce(10, (a, b)->a+b);
  • Collecting (Stream을 Collections이나 Map으로 변환)
    ex.
    List<String> res = list.stream().map(e->e.toUpperCase()).collect(Collectors.toList());
  • 스트림은 이론적으로 요청한 요소만 계산한다는 점에서 모든 값을 메모리에 저장하는 컬렉션과 차이를 보임
  • 또 반복을 알아서 처리하고 결과값을 어딘가에 저장하는 내부 반복이 일어나 메모리 소비가 적음
    (ex. 컬렉션이라면: 숫자 리스트 안에 숫자들 다 불러줘 -> 그 중 짝수만 불러줘 -> 그걸 정렬해줘
    스트림: 숫자 리스트 안의 수 중 짝수만 데려와서 정렬해줘)

참고

https://wikidocs.net/157858
https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/util/function/package-summary.html
https://dinfree.com/lecture/language/112_java_9.html
https://blog.knoldus.com/functional-java-understanding-pure-functions-with-java/
https://namu.wiki/w/%EA%B3%A0%EC%B0%A8%20%ED%95%A8%EC%88%98
https://dreamcoding.tistory.com/60
https://www.baeldung.com/java-8-streams-introduction
https://modimodi.tistory.com/24
https://wikidocs.net/167364

profile
노션에서 자라는 중 (●'◡'●)

0개의 댓글