명령형 vs 선언형 (함수형)

June·2022년 3월 1일
4

우테코

목록 보기
17/84

학습 배경

List<LottoRank> lottoRanks = new ArrayList<>();
for (LottoTicket ticket : tickets) {
    lottoRanks.add(winningTicket.compare(ticket));
}

lottoRanks를 생성하고, lottoRanks값을 하나씩 추가하고 있는 구조인데요.
명령형으로 코드를 작성하면 lottoRanks가 가변변수가 되고, 사이드이펙트가 생길 수 있는 여지가 생기는데요.
이 부분을 선언적으로 작성해보는걸 추천드려요.

리뷰어의 리뷰였는데 선언적으로 작성하라는 것이 어떤 것인지 감이 오지 않았다.

그래서 이 글위키를 참고하여 개념 정리를 해보았다.

명령형 (imperative) vs 선언형(declarative)

명령형 프로그래밍은 어떻게를 일일이 간섭하는 것이고, 선언형은 무엇에 집중하고 나머지는 맡긴다.

사실 이렇게만 들어서는 이해가 잘가지 않는다. 간단하게 예시를 들어보자.

예시 1

심부름을 시킬 때

  • 명령형(HOW): 집을 나가서 오른쪽으로 50미터 직진후 CU 에서 오른쪽으로 꺾어. 수할인마트에 들어가서 삼겹살 집어서 계산대로 가. 그러고 만원짜리 하나 드리고 100원 거슬러 받아와.

  • 선언형(HOW): 삼겹살 사와

물론 선언적 방식의 접근을 위해서는 명령형 방식으로 '어떻게 접근하는가'가 먼저 추상화가 되어있어야 한다.
삼겹살을 사와라고 심부름을 시키는 것은 마트가 어디인지 알고, 거래를 할줄 안다 등등을 알고 있다고 전제하는 것이다.

선언적 접근 방식의 기저에는 명령형이 깔려있고 추상화 된 것이다.

예시 2

SQL 역시 선언형 언어이다.

SELECT *
FROM Users
WHERE country = 'Mexico';

우리는 DB가 데이터를 어떻게 가져오는지 관심이 없다. 그냥 무엇을 가져오는지에 대해서만 관심을 두고 있다.

스트림에서 쓰던 map, reduce 와 같은 기능도 무엇을에 초점을 두고 안의 방식은 추상화를 했다고 볼 수 있다.

코드 해결 부분

해결전

public LottoRanks compareResult(WinningTicket winningTicket) {
    List<LottoRank> lottoRanks = new ArrayList<>();
    for (LottoTicket ticket : tickets) {
        lottoRanks.add(winningTicket.compare(ticket));
    }
    return new LottoRanks(lottoRanks);
}

위의 코드에서는 lottoRanks를 선언해서 tickets의 각 요소들을 반복해서 돌면서 비교 결과를 넣으라고 지시를 하고 있다.

이 코드는 명령형인 부분도 문제고, 빈 배열을 선언해놓고 값을 추가하는 행위를 하고 있다. 이러면 중간에 값이 얼마든지 바뀔 수 있다.

해결 후

public LottoRanks compareResult(WinningTicket winningTicket) {
    return new LottoRanks(tickets.stream()
            .map(winningTicket::judgeRank)
            .collect(Collectors.toList()));
}

그럼 선언형과 함수형의 관계는?

Is functional programming a type of declarative programming?

함수형 프로그래밍은 선언형 프로그래밍의 일종이다. 하지만 일상에서 사용할 때는 둘의 의미를 섞어서 사용한다.

함수형 프로그래밍 보충

함수형 프로그래밍의 특징
1. 인풋과 아웃풋이 있다.
2. 외부환경으로부터 독립적이다. 다른 것에 뭘 적어두지도, 참조하지도 않는다.
3. 같은 인풋에 있어 동일한 아웃풋이 나온다. 외부 영향을 받지 않기 때문이다. 이런걸 순수함수라고 한다.
비함수형 프로그래밍에서도 실수만 없다면 괜찮겠지만, 값들을 외부에서 건드리면 차질이 생긴다.

함수형 프로그래밍은 함수의 동작에 의한 변수의 부수적인 값 변경을 원천적으로 막은 것이다.
외부 변수를 사용하더라도 그 본체에 접근해서 변경하는게 아니라 사본을 만들어서 작업하기 때문에 부작용이 없다. 이는 아무런 상태변화를 일으키지 않는게 아니다. 일정단위의 작업에서 부수효과 없이 안정적이고 예측가능한 프로그램을 짜는 것이다.

멀티코어를 이용한 멀티 프로세싱이 중요해진 오늘날 함수형 프로그래밍은 더더욱 주목받고 있다.

  1. 함수형은 선언형이다.
  2. 함수도 값이다.
  3. 고차함수. 인자로 다른 함수를 받아 결과값을 내보내는 함수를 고차함수라 한다.

추가 자료

JavaScript로 함수형 프로그래밍 배우기 - Anjana Vakil - JSUnconf

명령형 vs 함수형

사이드 이펙트 최소화

순수함수를 사용하라. 사이드 이펙트를 최소화한다. 사이드 이펙트는 함수가 인풋으로 주어지 않은 것을 사용하여 반환값을 리턴하는 것이다. 예를 들어 콘솔에 뭔가를 print하는 것은 뭔가를 리턴하지 않는다. 또한 전역 변수를 사용하는 함수는 단지 인풋 값만에 의존하지 않기 때문에 마찬가지다.

쉽게 말해 순수하다는 것은 단지 인풋 값으로 주어진 것만 이용하여 아웃풋을 내는 것이다.

비순수 함수 예시

전역 변수처럼 사용하고 있다. 또한 return도 하지 않고 있다.

순수 함수

고차함수 사용

다른 함수의 인풋 혹은 아웃풋으로 함수를 넣거나 반환하는 것이다.

위의 함수에서 makeAdjectifier는 함수를 반환하고 있다.

반복하지 마라

mutability를 사용해라

immutable 데이터를 사용하라

기존의 데이터를 변경하지 않았다는 것이 중요하다. 이렇게 하지 않으면 예상하지 못한 곳에서 버그가 발생한다.

Persistent Data Structures

위의 코드처럼 단 하나를 위해 전체를 복사하는 것은 비효율적이다.

persistent Data Structures를 사용하는 것이다

배열을 사용할 때 그냥 각 요소를 바꾸는 것이 아니라 트리처럼 만들어서 바꾸려는 부분만 새로운 걸로 교체할 수 있다.

자바스크립트에서는 Mori, Immutable.js 같은 라이브러리를 쓸 수 있다.

함수형 프로그램의 목표는 비즈니스 로직과 부작용을 분리하는 것이다. 수학적 함수는 숨은 입출력이 없는 함수다. 부작용과 예외가 숨은 출력에 해당한다. 내부 상태 또는 외부 상태에 대한 참조는 숨은 입력이다.

  • 단위테스트-

0개의 댓글