람다로 사고하기: 선언적 프로그래밍

선언적 프로그래밍과 람다

함수형 프로그래밍 시리즈인 람다로 사고하기의 네 번째 편입니다. 이전 편에서는 부분 적용과 커링을 활용해 다수의 인자를 받는 함수들을 조합하는 방법을 살펴보았습니다.

작은 기능 단위를 만들어 내고 이를 조합하다 보면, 일반적인 산술 연산, 비교, 논리 연산 및 제어 구조에 대한 래퍼 함수를 자주 작성하게 됩니다. 이 과정은 반복적일 수 있지만, 람다는 이러한 문제를 해결할 수 있는 도구를 제공합니다.

명령형과 선언형

프로그래밍 스타일은 다양한 방식으로 나눌 수 있습니다. 정적 타입과 동적 타입, 해석형과 컴파일형, 저수준과 고수준 등이 대표적입니다.

또 다른 분류는 명령형과 선언형입니다.

명령형 프로그래밍은 컴퓨터에게 "어떻게" 작업을 수행할지 지시하는 방식입니다. 대부분의 일상적인 코드에서 사용하는 제어 구조(if, for, while), 산술 연산(+, -, *, /), 비교 연산(===, >, <), 논리 연산(&&, ||, !) 모두 명령형의 예입니다.

반면 선언형 프로그래밍은 "무엇을" 하고 싶은지를 표현하고, 컴퓨터가 그 결과를 스스로 도출하도록 합니다. 대표적인 선언형 언어로는 프롤로그(Prolog)가 있으며, 이 언어는 사실과 추론 규칙을 기반으로 질문에 답하는 방식으로 동작합니다.

함수형 프로그래밍은 선언형의 한 종류로, 함수를 정의한 후 이를 조합하여 프로그램의 흐름을 구성합니다.

선언형 코드에서도 여전히 산술, 비교, 논리, 제어 흐름 같은 기본 요소는 필요하지만, 그 표현 방식이 달라져야 합니다.

선언적 대체 구문

자바스크립트는 명령형 언어이므로 일반적인 코드에서는 표준 연산자를 사용해도 무방합니다. 하지만 함수형 변환 파이프라인을 사용할 때는 명령형 구문이 잘 맞지 않습니다.

람다에서는 이러한 문제를 해결할 수 있도록 다양한 함수를 제공합니다.

산술 연산

이전 편에서 파이프라인을 통해 산술 변환을 구현한 바 있습니다.

const multiply = (a, b) => a * b;
const addOne = x => x + 1;
const square = x => x * x;

const operate = pipe(multiply, addOne, square);
operate(3, 4); // ((3 * 4) + 1)^2 = 169

람다는 add, subtract, multiply, divide 등의 함수를 제공하여 표준 연산자를 대체할 수 있게 해줍니다. 또한 add는 커링되므로 add(1)처럼 사용 가능하며, squaremultiply(x, x)로 다시 정의할 수 있습니다.

const square = x => multiply(x, x);
const operate = pipe(multiply, add(1), square);

add(1)은 증가 연산자(++)와 유사하지만, ++는 상태를 변경하므로 불변성 원칙에 어긋납니다. 따라서 inc, dec 함수를 사용해 증감을 표현하는 것이 더 적절합니다.

const operate = pipe(multiply, inc, square);

subtract는 이항 - 연산자와 대응되며, 부호 반전을 위한 단항 - 연산자는 negate 함수로 대체할 수 있습니다.

비교 연산

이전 편에서 투표 가능 여부를 판단하는 함수를 작성했습니다.

const isOver18 = person => person.age >= 18;
const isCitizen = either(wasBornInCountry, wasNaturalized);
const isEligibleToVote = both(isOver18, isCitizen);

람다에서는 equals===을, gte>=을, gt>, lt<, lte<=를 대체할 수 있습니다.

const isOver18 = person => gte(person.age, 18);
const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY);

주의할 점은 gte, gt 등 함수들이 데이터를 마지막에 받는 람다의 관례("data-last")에 어긋난다는 것입니다. 따라서 파이프라인에서 사용할 때는 __ 플레이스홀더나 flip을 함께 사용해야 합니다.

또한 isEmpty는 빈 문자열이나 배열을 확인할 때, isNilnull 또는 undefined를 검사할 때 유용합니다.

논리 연산

both, either, complement&&, ||, !의 함수형 버전입니다. 이들은 동일한 값에 대해 작동할 때 유용합니다.

다른 경우, 즉 서로 다른 값을 조건적으로 처리할 때는 and, or, not 함수를 사용합니다. 이들은 값 자체를 다루는 반면, both 등은 함수를 다룹니다.

기본값 설정 시 ||falsy 값을 기준으로 동작하므로, 0 같은 유효한 값이 무시될 수 있습니다. 이때 defaultTo 함수를 사용하면 보다 안전하게 기본값을 설정할 수 있습니다.

const lineWidth = defaultTo(80, settings.lineWidth);

defaultTo는 두 번째 인자가 null이나 undefined인지 확인하고, 그렇지 않으면 그 값을 반환합니다.

조건부 처리

함수형 코드에서도 조건문은 필요할 수 있습니다.

ifElse

나이가 21 이상이면 21을 유지하고, 그렇지 않으면 1씩 증가시키는 함수를 생각해봅시다.

const forever21 = age => age >= 21 ? 21 : age + 1;

이 코드는 조건과 두 가지 분기를 함수로 표현할 수 있습니다. ifElse 함수는 if...then...else 문의 함수형 버전입니다.

const forever21 = age => ifElse(gte(__, 21), always(21), inc)(age);

여기서 __는 플레이스홀더로, 인자를 생략하고 이후에 전달되는 값을 의미합니다. lt를 사용해도 되지만, gte__ 조합이 더 직관적입니다.

always

상수 함수는 매우 유용하며, 람다에서는 always 함수로 간편하게 표현할 수 있습니다.

const forever21 = age => ifElse(gte(__, 21), always(21), inc)(age);

또한 TF는 각각 always(true)always(false)의 축약형입니다.

identity

나이가 16세 미만이면 16을 반환하고, 그렇지 않으면 그대로 반환하는 함수를 생각해봅시다.

const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), a => a)(age);

a => a는 입력을 그대로 반환하는 함수로, identity 함수라고 불립니다. 람다에서는 identity 함수를 제공합니다.

const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), identity)(age);

identity는 여러 인자를 받을 수 있지만 항상 첫 번째 인자를 반환합니다. 다른 값을 반환하고 싶다면 nthArg를 사용할 수 있으나 드물게 쓰입니다.

whenunless

조건 분기 중 하나가 identity인 경우, whenunless을 사용할 수 있습니다.

  • when(condition, fn)은 조건이 참일 때만 fn을 실행합니다.
  • unless(condition, fn)은 조건이 거짓일 때만 fn을 실행합니다.
const alwaysDrivingAge = age => when(lt(__, 16), always(16))(age);

또는 조건을 반대로 하면:

const alwaysDrivingAge = age => unless(gte(__, 16), always(16))(age);
cond

다수의 조건을 처리할 때는 cond 함수를 사용할 수 있습니다. 이는 switch 문이나 긴 if...else 체인을 대체할 수 있습니다.

const water = temperature => cond([
  [equals(0), always('water freezes at 0°C')],
  [equals(100), always('water boils at 100°C')],
  [T, temp => `nothing special happens at ${temp}°C`]
])(temperature);

이 함수는 조건 리스트를 순차적으로 평가하고, 처음으로 참이 되는 조건에 해당하는 함수를 실행합니다. 마지막 항목은 T로 항상 참이므로 기본 처리를 제공합니다.

마무리

람다는 명령형 코드를 선언형 함수형 코드로 전환할 수 있도록 다양한 도구를 제공합니다. 이제 우리는 함수 조합을 더욱 깔끔하게 표현할 수 있게 되었습니다.

다음 편에서는 이러한 패턴을 더 깔끔하게 만들 수 있는 점 없는 스타일(점프리 스타일)에 대해 다룰 것입니다.

태그: Ramda Functional Programming declarative Pipeline currying

6월 21일 03:53에 게시됨