선언형, 명령형 그리고 추상화
선언형이란?
명령형은 어떻게(How)에, 선언형은 무엇(What)에 집중한다.
선언형은 명령형 코드에서 ‘어떻게’를 감추고 ‘무엇을’만 노출하는 방식의 추상화이다. 일종의 리팩토링이다.
예시를 들면 다음과 같다.
1
2
3
4
5
6
7
8
// 명령형 코드 : 배열에 있는 모든 숫자를 하나씩 제곱해서 result 배열에 넣는다.
function double(arr) {
let results = [];
for (let i = 0; i < arr.length; i++) {
results.push(arr[i] * 2);
}
return results;
}
1
2
3
4
// 선언형 코드 : 모든 배열에서 숫자가 제곱된다.
function double(arr) {
return arr.map((item) => item * 2);
}
배열의 모든 요소를 돌며 하나씩 제곱하는 것이 아니라, 배열의 각 요소를 대상으로 제곱이 되라고 선언한 형태이다. How는 map 함수 안에 숨겨져있다.
함수로 묶으면 다 선언적인가?
선언형 프로그래밍의 대중적인 정의 몇가지
- A high-level program that describes what a computation should perform.
- Any programming language that lacks side effects (or more specifically, is referentially transparent)
- A language with a clear correspondence to mathematical logic.
여기서 두 번째 불렛의 사이드 이펙트가 적고 순수하다라는 점이 중요하다.
1
moveToEmptyTable(myPosition, tables);
위 함수는 충분히 선언적이지 못하다.
- 먼저 순수하지 못하다. 빈자리를 얻어내는 계산함수와 , 빈자리에 배치하는 액션함수가 하나의 메소드로 묶여있다.
- 여러 번 호출하거나, 특수한 상황에서 호출했을 때 다른 결과물을 줄 수 있다. 따라서 재사용이 어렵다. 이는 계산과 액션이 묶여있기에 발생한다.
- 빈자리에 이미 앉아있는 경우에
moveToEmptyTable()
을 호출한다면 빈자리에 찾아갈지 아니면 오류가 날지 모른다.
- 빈자리에 이미 앉아있는 경우에
따라서 언제 불러도 같은 결과가 나오도록, 또 순수해지도록 리팩트링하면 다음과 같이 두 함수로 나뉠 수 있다.
1
2
const emptyTablePosition = getEmptyTablePosition(tables);
move(me, emptyTablePosition);
- 코드의 절차적인 순서와 상관없이 언제 어디서 불러도 동일한 결과값을 준다.(재사용성 up)
- what이 함수명에 적절히 표현되어있다.
- 세부 구현은 함수 내부에 추상화되었다.
선언적 프로그래밍의 또다른 특징 : 코드순서 노상관
라인 바이 라인의 코드순서가 중요하지 않아질수록 더 선언적이게 된다.
순서의존도가 없기 때문에 사이드이펙트도 줄어들고 이해하기도 더 쉬워진다.
예를들어 리액트 컴포넌트의 props은 순서에 상관없이 동일한 동작을 한다.
1
2
3
4
5
<Modal
title="뭐먹지"
onClick={() => alert("짬뽕")}
description="중국음식?"
/>
여기에 title, onClick, description의 코드 순서는 중요하지 않다.
명령형 추상화, 선언형 추상화
다음은 동,읍,면 Input이 있고, 동을 입력하면 자동으로 읍으로, 읍을 입력하면 자동으로 면으로 넘어가는 코드이다.
1
2
3
4
5
6
7
8
9
10
11
// 명령형 코드 원본
if (value.length < maxLength) {
return;
}
if (cursorPosition === "동") {
읍input.focus();
읍input.selectionStart = 읍input.value.length;
} else if (cursorPosition === "읍") {
면input.focus();
면input.selectionStart = 면input.value.length;
}
1
2
3
4
5
6
7
8
9
10
// 명령형 추상화
const isInputFull = value.length === maxLength;
if (!isInputFull) {
return;
}
if (cursorPosition === "동") {
moveToInput("읍");
} else if (cursorPosition === "읍") {
moveToInput("면");
}
1
2
3
4
// 선언형 추상화
<Input name="동" onFull={() => moveFocusTo('읍')}/>
<Input name="읍" onFull={() => moveFocusTo('면')}/>
<Input name="면"/>
정리
명령형 코드를
- What을 적절히 인터페이스에 노출하면서
- How를 내부에 숨기고
- 언제 어디서 불러도 동일한 결과가 나와서 재사용하기 편하게 추상화한다면
선언적 코드라고 볼 수 있다.
명령형 코드를 해석할 때는, 흐름을 따라가면서 읽어야 하는 시간축
이 있다. 하지만 선언적코드는 흐름에 따라 읽지 않아도 되기에 읽기도, 디버깅하기도, 재사용하기도 좋다. 또한 내부를 몰라도 사용할 수 있다는 점에서 사용성이 좋다.
선언적 프로그래밍은 코드 자체에 어떤일이 벌어진지 설명하기 때문에, 좀 더 추론하기 좋다.
아래 예시를 통해 알아보자
1
2
3
4
5
6
7
8
9
10
11
12
const loadAndMapMembers = compose(
combineWith(sessionStorage, "members"),
save(sessionStorage, "members"),
scopeMembers(window),
logMemberInfoToConsole("name.first"),
countMemberBy("location.state"),
prepStatesForMapping,
save(sessionStorage, "map"),
renderUSMap
);
getFakeMembers(100).then(loadAndMapMembers);
많은 함수가 결합되어있지만, 선언형으로 되어있기에 각 함수가 어떻게 구현되어있을 거라고 추론하기 쉬워진다.
출처
https://milooy.github.io/dev/220810-abstraction-and-declarative-programming/