기능적 프로그래밍 마스터링: 제로에서 히어로까지
Ethan Miller
Product Engineer · Leapcell

기능적 프로그래밍 상세 설명
기능적 프로그래밍에 대해 들어본 적이 있거나 심지어 얼마 동안 사용해 왔을 수도 있습니다. 하지만 그것이 무엇인지 명확하게 설명할 수 있습니까?
온라인에서 검색하면 많은 답변을 쉽게 찾을 수 있습니다.
- 객체 지향 프로그래밍 및 절차적 프로그래밍과 병행되는 프로그래밍 패러다임입니다.
- 가장 중요한 특징은 함수가 일급 시민이라는 것입니다.
- 계산 프로세스를 재사용 가능한 함수로 분해하는 것을 강조합니다. 일반적인 예는
map
메서드와reduce
메서드로 구성된 MapReduce 알고리즘입니다. - 부작용이 없는 순수 함수만이 자격을 갖춘 함수입니다.
위의 모든 설명이 옳지만 충분하지 않습니다. 그들은 더 깊은 질문에 답하지 않습니다. 왜 이런 식으로 해야 할까요? 이것이 이 기사가 답하고자 하는 것입니다. 기능적 프로그래밍을 이해하고 가장 간단한 언어로 기본 구문을 배우도록 도와드리겠습니다.
I. 범주론
기능적 프로그래밍의 기원은 범주론이라는 수학의 한 분야입니다. 기능적 프로그래밍을 이해하는 핵심은 범주론을 이해하는 것입니다. 그것은 세상의 모든 개념 체계가 '범주'로 추상화될 수 있다고 믿는 복잡한 수학입니다.
1.1 범주의 개념
범주란 무엇일까요? 위키피디아의 한 문장 정의는 다음과 같습니다. "수학에서 범주는 '화살표'로 연결된 '객체'로 구성된 대수적 구조입니다."
즉, 서로 특정 관계를 맺고 있는 개념, 사물, 객체 등은 모두 '범주'를 형성합니다. 그들 사이의 관계를 찾을 수 있는 한 무엇이든 '범주'를 정의할 수 있습니다.
예를 들어 다양한 점과 그 사이의 화살표는 범주를 형성합니다. 화살표는 범주 구성원 간의 관계를 나타내며 공식 명칭은 "형태"입니다. 범주론은 동일한 범주의 모든 구성원이 서로 다른 상태의 "변환"이라고 믿습니다. "형태"를 통해 한 구성원을 다른 구성원으로 변환할 수 있습니다.
1.2 수학적 모델
'범주'는 특정 변환 관계를 만족하는 모든 객체이므로 수학적 모델은 다음과 같이 요약할 수 있습니다.
- 모든 구성원은 집합을 형성합니다.
- 변환 관계는 함수입니다.
즉, 범주론은 집합론의 더 높은 수준의 추상화입니다. 간단한 이해는 "집합 + 함수"입니다. 이론적으로 함수를 통해 한 구성원에서 범주의 다른 모든 구성원을 계산할 수 있습니다.
1.3 범주 및 컨테이너
'범주'를 두 가지를 포함하는 컨테이너로 상상할 수 있습니다.
- 값.
- 값의 변환 관계, 즉 함수.
다음은 간단한 범주를 정의하는 코드입니다.
class Category { constructor(val) { this.val = val; } addOne(x) { return x + 1; } }
위 코드에서 Category
는 클래스이자 컨테이너이며 값(this.val
)과 변환 관계(addOne
)를 포함합니다. 여기서 범주는 서로 1씩 다른 모든 숫자라는 것을 알 수 있습니다.
이 문서의 다음 부분에서는 "컨테이너"가 언급될 때마다 "범주"를 나타냅니다.
1.4 범주론과 기능적 프로그래밍의 관계
범주론은 함수를 사용하여 범주 간의 관계를 표현합니다. 범주론의 발전과 함께 일련의 함수 연산 방법이 개발되었습니다. 이 방법 집합은 처음에는 수학 연산에만 사용되었습니다. 나중에 누군가가 컴퓨터에서 구현하여 오늘날의 "기능적 프로그래밍"이 되었습니다.
본질적으로 기능적 프로그래밍은 범주론의 연산 방법일 뿐입니다. 수학적 논리, 미적분, 행렬식과 같은 종류의 것입니다. 그것들은 모두 수학적 방법입니다. 단지 프로그램을 작성하는 데 사용할 수 있다는 것뿐입니다.
그렇다면 기능적 프로그래밍이 함수가 순수하고 부작용이 없어야 하는 이유를 이해하십니까? 수학적 연산이며 원래 목적은 다른 작업을 수행하지 않고 값을 평가하는 것이기 때문입니다. 그렇지 않으면 함수 연산 규칙을 만족할 수 없습니다.
간단히 말해서 기능적 프로그래밍에서 함수는 파이프와 같습니다. 한쪽 끝에 값이 들어가고 다른 쪽 끝에 새 값이 나오며 다른 효과는 없습니다.
II. 함수 합성 및 커링
기능적 프로그래밍에는 합성 및 커링이라는 두 가지 가장 기본적인 연산이 있습니다.
2.1 함수 합성
값이 다른 값이 되기 위해 여러 함수를 거쳐야 하는 경우 모든 중간 단계를 하나의 함수로 결합할 수 있으며 이를 "함수 합성"이라고 합니다.
예를 들어 X
와 Y
사이의 변환 관계가 함수 f
이고 Y
와 Z
사이의 변환 관계가 함수 g
인 경우 X
와 Z
사이의 관계는 g
와 f
의 합성 함수 g ∘ f
입니다.
다음은 코드 구현입니다(JavaScript 언어 사용). 이 문서의 모든 예제 코드는 단순화되었습니다. 두 함수를 합성하는 간단한 코드는 다음과 같습니다.
const compose = function (f, g) { return function (x) { return f(g(x)); }; }
함수 합성은 또한 결합 법칙을 만족해야 합니다.
compose(f, compose(g, h)) // is equivalent to compose(compose(f, g), h) // is equivalent to compose(f, g, h)
합성은 또한 함수가 순수해야 하는 이유이기도 합니다. 순수하지 않은 함수를 다른 함수와 어떻게 합성할 수 있습니까? 다양한 합성이 예상되는 동작을 달성할 것이라고 어떻게 보장할 수 있습니까?
앞서 언급했듯이 함수는 데이터의 파이프와 같습니다. 그러면 함수 합성은 이러한 파이프를 연결하여 데이터가 한 번에 여러 파이프를 통과할 수 있도록 하는 것입니다.
2.2 커링
f(x)
와 g(x)
를 f(g(x))
로 합성하는 데는 f
와 g
가 모두 하나의 매개변수만 허용할 수 있다는 숨겨진 전제가 있습니다. f(x, y)
및 g(a, b, c)
와 같이 여러 매개변수를 허용할 수 있는 경우 함수 합성은 매우 번거로울 것입니다.
이것이 커링이 등장하는 곳입니다. 소위 "커링"은 다중 매개변수 함수를 단일 매개변수 함수로 변환하는 것입니다.
// 커링 전 function add(x, y) { return x + y; } add(1, 2) // 3 // 커링 후 function addX(y) { return function (x) { return x + y; }; } addX(2)(1) // 3
커링을 사용하면 모든 함수가 하나의 매개변수만 허용하도록 할 수 있습니다. 다음 내용에서 달리 명시되지 않는 한 함수에는 처리할 값인 하나의 매개변수만 있다고 가정합니다.
III. 펑터
함수는 동일한 범주 내에서 값의 변환뿐만 아니라 한 범주를 다른 범주로 변환하는 데에도 사용할 수 있습니다. 여기에는 펑터가 관련됩니다.
3.1 펑터의 개념
펑터는 기능적 프로그래밍에서 가장 중요한 데이터 유형이며 연산 및 기능의 기본 단위이기도 합니다.
무엇보다도 범주, 즉 값과 변환 관계를 포함하는 컨테이너입니다. 특별한 점은 변환 관계를 각 값에 차례로 적용하여 현재 컨테이너를 다른 컨테이너로 변환할 수 있다는 것입니다.
예를 들어 왼쪽 원은 사람 이름 범주를 나타내는 펑터입니다. 외부 함수 f
가 전달되면 오른쪽의 아침 식사 범주로 변환됩니다.
더 일반적으로 함수 f
는 값의 변환(a
에서 b
로)을 완료합니다. 펑터에 전달하면 범주의 변환(Fa
에서 Fb
로)을 달성할 수 있습니다.
3.2 펑터의 코드 구현
map
메서드가 있는 모든 데이터 구조는 펑터의 구현으로 간주할 수 있습니다.
class Functor { constructor(val) { this.val = val; } map(f) { return new Functor(f(this.val)); } }
위 코드에서 Functor
는 펑터입니다. 해당 map
메서드는 함수 f
를 매개변수로 받아 새 펑터를 반환합니다. 그 안에 포함된 값은 f
( f(this.val)
)로 처리된 값입니다.
일반적으로 펑터의 표시는 컨테이너에 map
메서드가 있다는 데 동의합니다. 이 메서드는 컨테이너 내부의 각 값을 다른 컨테이너에 매핑합니다.
다음은 몇 가지 사용 예입니다.
(new Functor(2)).map(function (two) { return two + 2; }); // Functor(4) (new Functor('flamethrowers')).map(function(s) { return s.toUpperCase(); }); // Functor('FLAMETHROWERS') (new Functor('bombs')).map(_.concat(' away')).map(_.prop('length')); // Functor(10)
위의 예는 기능적 프로그래밍의 연산이 모두 펑터를 통해 완료된다는 것을 보여줍니다. 즉, 연산은 값에 직접적으로 수행되는 것이 아니라 이러한 값의 컨테이너인 펑터에 수행됩니다. 펑터 자체에는 외부 인터페이스(map
메서드)가 있고 다양한 함수가 연산자입니다. 그들은 인터페이스를 통해 컨테이너에 연결되어 컨테이너 내부의 값이 변환되도록 합니다.
따라서 기능적 프로그래밍을 배우는 것은 실제로 펑터의 다양한 연산을 배우는 것입니다. 연산 방법을 펑터에 캡슐화할 수 있기 때문에 다양한 유형의 펑터가 파생되었습니다. 연산의 종류가 많을수록 펑터의 종류도 많습니다. 기능적 프로그래밍은 다양한 펑터를 적용하여 실제 문제를 해결하는 것이 됩니다.
IV. of
메서드
위에서 새 펑터를 생성할 때 new
명령이 사용되었음을 눈치챘을 것입니다. new
명령은 객체 지향 프로그래밍의 상징이기 때문에 이것은 정말 기능적 프로그래밍과 같지 않습니다.
기능적 프로그래밍에서는 일반적으로 펑터에 새 컨테이너를 생성하는 of
메서드가 있다는 데 동의합니다.
다음은 new
를 of
메서드로 대체합니다.
Functor.of = function(val) { return new Functor(val); };
그런 다음 이전 예제를 다음과 같이 변경할 수 있습니다.
Functor.of(2).map(function (two) { return two + 2; }); // Functor(4)
이것은 기능적 프로그래밍과 더 유사합니다.
V. Maybe 펑터
펑터는 컨테이너 내부의 값을 처리하기 위해 다양한 함수를 허용합니다. 여기에 문제가 있습니다. 컨테이너 내부의 값은 null 값(null
과 같은)일 수 있으며 외부 함수에는 null 값을 처리하는 메커니즘이 없을 수 있습니다. null 값이 전달되면 오류가 발생할 가능성이 높습니다.
Functor.of(null).map(function (s) { return s.toUpperCase(); }); // TypeError
위 코드에서 펑터 내부의 값은 null
이고 소문자를 대문자로 변환할 때 오류가 발생합니다.
Maybe 펑터는 이러한 종류의 문제를 해결하기 위해 설계되었습니다. 간단히 말해서 해당 map
메서드에는 null 값 검사가 있습니다.
class Maybe extends Functor { map(f) { return this.val? Maybe.of(f(this.val)) : Maybe.of(null); } }
Maybe 펑터를 사용하면 null 값을 처리해도 오류가 발생하지 않습니다.
Maybe.of(null).map(function (s) { return s.toUpperCase(); }); // Maybe(null)
VI. Either 펑터
조건 연산 if...else
는 가장 일반적인 연산 중 하나입니다. 기능적 프로그래밍에서는 Either 펑터를 사용하여 표현합니다.
Either 펑터 내부에는 왼쪽 값(Left
)과 오른쪽 값(Right
)이라는 두 개의 값이 있습니다. 오른쪽 값은 정상적인 상황에서 사용되는 값이고 왼쪽 값은 오른쪽 값이 없을 때 사용되는 기본값입니다.
class Either extends Functor { constructor(left, right) { this.left = left; this.right = right; } map(f) { return this.right? Either.of(this.left, f(this.right)) : Either.of(f(this.left), this.right); } } Either.of = function (left, right) { return new Either(left, right); };
다음은 사용법입니다.
var addOne = function (x) { return x + 1; }; Either.of(5, 6).map(addOne); // Either(5, 7); Either.of(1, null).map(addOne); // Either(2, null);
위 코드에서 오른쪽 값에 값이 있으면 오른쪽 값이 사용되고, 그렇지 않으면 왼쪽 값이 사용됩니다. 이러한 방식으로 Either 펑터는 조건 연산을 표현합니다.
Either 펑터의 일반적인 사용법은 기본값을 제공하는 것입니다. 다음은 예입니다.
Either .of({address: 'xxx'}, currentUser.address) .map(updateField);
위 코드에서 사용자가 주소를 제공하지 않으면 Either 펑터는 왼쪽 값의 기본 주소를 사용합니다.
Either 펑터의 또 다른 사용법은 try...catch
를 대체하여 왼쪽 값을 오류를 나타내는 데 사용하는 것입니다.
function parseJSON(json) { try { return Either.of(null, JSON.parse(json)); } catch (e: Error) { return Either.of(e, null); } }
위 코드에서 왼쪽 값이 비어 있으면 오류가 없음을 의미합니다. 그렇지 않으면 왼쪽 값에 오류 객체 e
가 포함됩니다. 일반적으로 오류를 일으킬 수 있는 모든 연산은 Either 펑터를 반환할 수 있습니다.
VII. Ap 펑터
펑터에 포함된 값은 함수일 수 있습니다. 한 펑터의 값이 숫자이고 다른 펑터의 값이 함수인 상황을 상상할 수 있습니다.
function addTwo(x) { return x + 2; } const A = Functor.of(2); const B = Functor.of(addTwo)
위 코드에서 펑터 A
내부의 값은 2
이고 펑터 B
내부의 값은 함수 addTwo
입니다.
때로는 펑터 B
내부의 함수가 펑터 A
내부의 값을 사용하여 계산할 수 있기를 원합니다. 이것이 Ap 펑터가 등장하는 곳입니다.
ap
는 "applicative"의 약자입니다. ap
메서드를 배포하는 모든 펑터는 Ap 펑터입니다.
class Ap extends Functor { ap(F) { return Ap.of(this.val(F.val)); } }
ap
메서드의 매개변수는 함수가 아니라 다른 펑터입니다.
따라서 이전 예제를 다음과 같이 작성할 수 있습니다.
Ap.of(addTwo).ap(Functor.of(2)) // Ap(4)
Ap 펑터의 의미는 여러 매개변수가 있는 함수의 경우 여러 컨테이너에서 값을 가져와 펑터의 연결된 연산을 달성할 수 있다는 것입니다.
function add(x) { return function (y) { return x + y; }; } Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3)); // Ap(5)
위 코드에서 함수 add
는 커링된 형태이며 총 2개의 매개변수가 필요합니다. Ap 펑터를 통해 두 컨테이너에서 값을 가져올 수 있습니다. 다른 작성 방법도 있습니다.
Ap.of(add(2)).ap(Maybe.of(3));
VIII. Monad 펑터
펑터는 모든 값을 포함할 수 있는 컨테이너입니다. 펑터가 다른 펑터를 포함하는 것은 완전히 합법적입니다. 그러나 이것은 중첩된 펑터를 초래합니다.
Maybe.of( Maybe.of( Maybe.of({name: 'Mulburry', number: 8402}) ) )
이 펑터에는 세 개의 Maybe
가 중첩되어 있습니다. 내부 값을 가져오려면 this.val
을 세 번 연속으로 가져와야 합니다. 이것은 물론 매우 불편하므로 Monad 펑터가 등장했습니다.
Monad 펑터의 역할은 항상 단일 레이어 펑터를 반환하는 것입니다. map
메서드와 동일한 기능을 하는 flatMap
메서드가 있습니다. 유일한 차이점은 중첩된 펑터가 생성되면 후자의 값을 추출하여 반환되는 것이 항상 단일 레이어 컨테이너이고 중첩된 상황이 없도록 합니다.
class Monad extends Functor { join() { return this.val; } flatMap(f) { return this.map(f).join(); } }
위 코드에서 함수 f
가 펑터를 반환하면 this.map(f)
는 중첩된 펑터를 생성합니다. 따라서 join
메서드는 flatMap
메서드가 항상 단일 레이어 펑터를 반환하도록 보장합니다. 이는 중첩된 펑터가 평면화됨을 의미합니다.
IX. IO 연산
Monad 펑터의 중요한 응용 프로그램은 I/O (입력/출력) 연산을 구현하는 것입니다.
I/O는 순수하지 않은 연산이며 일반적인 기능적 프로그래밍으로는 처리할 수 없습니다. 이때 I/O 연산을 Monad 펑터로 작성하여 연산을 완료해야 합니다.
var fs = require('fs'); var readFile = function(filename) { return new IO(function() { return fs.readFileSync(filename, 'utf - 8'); }); }; var print = function(x) { return new IO(function() { console.log(x); return x; }); }
위 코드에서 파일 읽기 및 인쇄는 그 자체로 순수하지 않은 연산이지만 readFile
및 print
는 항상 IO
펑터를 반환하기 때문에 순수 함수입니다.
IO
펑터가 flatMap
메서드가 있는 Monad
인 경우 다음과 같이 이러한 두 함수를 호출할 수 있습니다.
readFile('./user.txt') .flatMap(print)
놀라운 점은 위 코드가 순수하지 않은 연산을 완료하지만 flatMap
이 IO
펑터를 반환하기 때문에 이 표현식은 순수하다는 것입니다. 순수한 표현식을 통해 부작용이 있는 연산을 완료합니다. 이것이 Monad
의 역할입니다.
반환되는 것이 여전히 IO
펑터이기 때문에 연결된 연산을 달성할 수 있습니다. 따라서 대부분의 라이브러리에서 flatMap
메서드의 이름이 chain
으로 변경됩니다.
var tail = function(x) { return new IO(function() { return x[x.length - 1]; }); } readFile('./user.txt') .flatMap(tail) .flatMap(print) // Equivalent to readFile('./user.txt') .chain(tail) .chain(print)
위 코드는 user.txt
파일을 읽은 다음 마지막 줄을 선택하여 출력합니다.
Leapcell: 웹 호스팅을 위한 차세대 서버리스 플랫폼
마지막으로 서비스를 배포하기 위한 최고의 플랫폼인 Leapcell을 추천합니다.
1. 다국어 지원
- JavaScript, Python, Go 또는 Rust로 개발하세요.
2. 무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하세요. 요청도, 요금도 없습니다.
3. 최고의 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하세요.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
4. 간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 지표 및 로깅.
5. 간편한 확장성 및 고성능
- 고도의 동시성을 쉽게 처리하기 위한 자동 확장.
- 운영 오버헤드 제로 — 빌드에만 집중하세요.

Leapcell 트위터: https://x.com/LeapcellHQ