기억하세요: 함수형 프로그래밍은 함수를 사용한 프로그래밍이 아닙니다! ! !
23.4 함수형 프로그래밍
23.4.1 함수형 프로그래밍이란 무엇
입니까? 그렇게 직설적으로 물어보면 설명하기 쉽지 않은 개념이라는 걸 알게 될 것이다. 프로그래밍 분야에서 다년간의 경험을 가진 많은 베테랑들은 함수형 프로그래밍이 무엇을 공부하는지 명확하게 설명하지 못합니다. 함수형 프로그래밍은 절차적 프로그래밍에 익숙한 프로그래머에게는 생소한 분야입니다. 클로저, 연속, 커링의 개념은 우리에게 너무나 생소한 것 같습니다. 함수형 프로그래밍에는 절차적 프로그래밍과 비교할 수 없는 아름다운 수학적 프로토타입이 있지만, 박사 학위를 가진 사람만이 이를 마스터할 수 있을 정도로 신비롭습니다.
팁: 이 섹션은 약간 어렵지만 JavaScript를 마스터하는 데 필요한 기술은 아닙니다. Lisp에서 수행되는 작업을 완료하기 위해 JavaScript를 사용하고 싶지 않거나 Lisp의 난해한 기술을 배우고 싶지 않은 경우. 함수형 프로그래밍을 건너뛰고 여행의 다음 장으로 들어가세요.
그럼 질문으로 돌아가서, 함수형 프로그래밍이란 무엇입니까? 답변이 길군요...
함수형 프로그래밍의 첫 번째 법칙: 함수는 첫 번째 유형입니다.
이 문장 자체를 어떻게 이해해야 할까요? 진정한 Type One이란 무엇입니까? 다음 수학적 개념을 살펴보겠습니다.
이진 방정식 F(x, y) = 0, x, y는 변수입니다. y = f(x)로 작성하고, x는 매개변수, y는 반환 값, f는 x에서 입니다. to y 매핑 관계를 함수라고 합니다. G(x, y, z) = 0 또는 z = g(x, y)가 있는 경우 g는 x, y에서 z로의 매핑 관계이며 함수이기도 합니다. g의 매개변수 x와 y가 이전 관계 y = f(x)를 충족하면 z = g(x, y) = g(x, f(x))를 얻습니다. 이는 두 가지 의미를 갖습니다. x)는 x에 대한 함수이고 함수 g의 매개변수입니다. 둘째, g는 f보다 고차 함수입니다.
이러한 방식으로 z = g(x, f(x))를 사용하여 방정식 F(x, y) = 0 및 G(x, y, z) = 0의 관련 해를 나타냅니다. 이는 반복 함수입니다. . g를 다른 형식으로 표현할 수도 있습니다. z = g(x, y, f)를 기억하면 함수 g를 고차 함수로 일반화할 수 있습니다. 이전 표현과 비교할 때 후자 표현의 장점은 T(x,y) = 0 및 G(x,y,z) = 0의 관련 솔루션과 같은 보다 일반적인 모델이라는 것입니다. 같은 형식으로 표현될 수 있습니다(단지 f=t라고 가정). 문제에 대한 해결책을 고차 함수로 변환하는 반복을 지원하는 이 언어 시스템에서는 해당 함수를 "첫 번째 유형"이라고 합니다.
JavaScript의 함수는 분명히 "첫 번째 유형"입니다. 전형적인 예는 다음과 같습니다.
Array.prototype.each = 함수(클로저)
{
return this.length ? [closure(this[0])].concat(this.slice(1).each(closure)) : [];
}
이것은 기능적 스타일의 매력을 최대한 발휘하는 정말 마법의 매직 코드입니다. 전체 코드에는 함수와 기호만 있습니다. 형태는 단순하고 무한히 강력합니다.
[1,2,3,4].each(function(x){return x * 2})는 [2,4,6,8]을 가져오고, [1,2,3,4].each(function(x) ){return x-1})은 [0,1,2,3]을 얻습니다.
기능적 지향과 객체지향의 본질은 "도는 자연을 따른다"는 것이다. 객체지향이 현실 세계의 시뮬레이션이라면, 함수형 표현은 어떤 의미에서는 객체지향보다 추상화 수준이 더 높습니다. 왜냐하면 수학 시스템은 본질적으로 비교할 수 없는 특징을 갖고 있기 때문입니다. 추상화의.
함수형 프로그래밍의 두 번째 법칙: 클로저는 함수형 프로그래밍의 가장 좋은 친구입니다.
이전 장에서 설명했듯이 클로저는 함수형 프로그래밍에 매우 중요합니다.
가장 큰 특징은 변수(기호)
를 전달하지 않고 내부 계층에서 외부 환경에 직접 액세스할 수 있다는 것입니다. 이는 다중 중첩 하에서 기능적 프로그램에 큰 편의성을 제공합니다.
{
반환 함수 innerFun(y)
{
x * y를 반환합니다.
}
})(2)(3);
함수형 프로그래밍의 세 번째 법칙: 함수는 Currying일 수 있습니다.
커링이란 무엇입니까? 흥미로운 개념입니다. 수학부터 시작해 보겠습니다. 3차원 공간 방정식 F(x, y, z) = 0을 고려하고, z = 0으로 제한하면 F(x, y, 0) = 0이 되며 F로 표시됩니다. '(x, y). 여기서 F'는 분명히 z = 0 평면에서 3차원 공간 곡선 F(x, y, z)의 2차원 투영을 나타내는 새로운 방정식입니다. y = f(x, z)를 나타내고 z = 0이라고 하면 y = f(x, 0)을 얻고 이를 y = f'(x)로 표시합니다. 함수 f'는 f의 커링 솔루션이라고 말합니다. .
JavaScript Currying의 예는 다음과 같습니다.
함수 추가(x, y)
{
if(x!=null && y!=null) return x + y;
else if(x!=null && y==null) 함수(y)를 반환합니다.
{
x + y를 반환합니다.
}
else if(x==null && y!=null) return 함수(x)
{
x + y를 반환합니다.
}
}
var a = 추가(3, 4);
var b = 추가(2);
var c = b(10);
위의 예에서 b=add(2)는 x = 2일 때 매개변수 y의 함수인 add()의 Currying 함수를 생성합니다. 속성은 위에서도 사용됩니다. 폐쇄의.
흥미롭게도 우리는 모든 함수에 대해 Currying을 일반화할 수 있습니다. 예:
function Foo(x, y, z, w)
{
var args = 인수;
if(Foo.length < args.length)
반환 함수()
{
반품
args.callee.apply(Array.apply([], args).concat(Array.apply([], 인수)));
}
또 다른
x + y – z * w를 반환합니다.
}
함수형 프로그래밍의 네 번째 법칙: 지연된 평가와 지속.
//TODO: 여기서 다시 생각해 보세요.
단위 테스트
의 장점
엄격한 함수형 프로그래밍의 모든 기호는 직접적인 수량 또는 표현식 결과에 대한 참조이며 어떤 함수에도 부작용이 없습니다. 값은 어딘가에서 수정되지 않으며 다른 함수(예: 클래스 멤버 또는 전역 변수)에서 사용되는 범위 외부의 수량을 수정하는 함수가 없기 때문입니다. 즉, 함수 평가의 결과는 반환 값일 뿐이며, 반환 값에 영향을 미치는 유일한 것은 함수의 매개 변수입니다.
이것은 단위 테스터의 꿈입니다. 테스트 중인 프로그램의 각 함수에 대해 함수 호출 순서를 고려하거나 외부 상태를 신중하게 설정할 필요 없이 해당 매개변수만 신경 쓰면 됩니다. 당신이 해야 할 일은 극단적인 경우를 나타내는 매개변수를 전달하는 것뿐입니다. 프로그램의 모든 기능이 단위 테스트를 통과하면 소프트웨어 품질에 대해 상당한 확신을 갖게 됩니다. 그러나 명령형 프로그래밍은 그렇게 낙관적일 수 없습니다. Java나 C++에서는 함수의 반환 값을 확인하는 것만으로는 충분하지 않습니다. 함수가 수정했을 수 있는 외부 상태도 확인해야 합니다.
디버깅
기능적 프로그램이 예상한 대로 작동하지 않으면 디버깅은 식은 죽 먹기입니다. 기능적 프로그램의 버그는 실행 전에 관련 없는 코드 경로에 의존하지 않기 때문에 발생하는 문제는 항상 재현될 수 있습니다. 명령형 프로그램에서는 함수의 기능이 다른 함수의 부작용에 의존하기 때문에 버그가 나타나고 사라지며, 버그 발생과 관련 없는 방향으로 오랫동안 검색해도 결과가 나오지 않을 수 있습니다. 함수형 프로그램의 경우에는 그렇지 않습니다. 함수의 결과가 잘못된 경우 이전에 무엇을 실행했든 함수는 항상 동일한 잘못된 결과를 반환합니다.
문제를 재현하고 나면 근본 원인을 찾는 것이 더 쉬워지고 심지어 행복해질 수도 있습니다. 해당 프로그램의 실행을 중단하고 스택을 검사합니다. 명령형 프로그래밍과 마찬가지로 스택에 있는 각 함수 호출의 매개변수가 표시됩니다. 그러나 명령형 프로그램에서는 이러한 매개변수만으로는 충분하지 않습니다. 함수는 멤버 변수, 전역 변수 및 클래스 상태(이 중 다수에 따라 다름)에도 의존합니다. 함수형 프로그래밍에서 함수는 매개변수에만 의존하며, 그 정보는 바로 여러분의 눈앞에 있습니다! 또한 명령형 프로그램에서는 함수의 반환값만 확인하는 것만으로는 해당 함수가 제대로 작동하는지 확인할 수 없으며, 이를 확인하려면 해당 함수 범위 밖의 수십 개의 객체 상태를 확인해야 합니다. 함수형 프로그램을 사용하면 반환 값을 살펴보기만 하면 됩니다!
스택을 따라 함수의 매개변수와 반환값을 확인하고, 불합리한 결과를 발견하면 해당 함수를 입력하여 버그가 발생한 지점을 찾을 때까지 이 과정을 단계별로 반복합니다.
병렬 기능 프로그램은 수정 없이 병렬로 실행될 수 있습니다. 잠금을 사용하지 않으므로 교착 상태 및 중요 섹션에 대해 걱정하지 마십시오! 기능적 프로그램의 데이터는 두 개의 서로 다른 스레드는 물론이고 동일한 스레드에 의해 두 번 수정되지 않습니다. 이는 병렬 애플리케이션을 괴롭히는 전통적인 문제를 일으키지 않고 다시 생각하지 않고 스레드를 간단히 추가할 수 있음을 의미합니다.
그렇다면 고도의 병렬 작업이 필요한 응용 프로그램에서 모든 사람이 함수형 프로그래밍을 사용하지 않는 이유는 무엇입니까? 글쎄요, 그들은 그렇게 하고 있습니다. Ericsson은 Erlang이라는 기능적 언어를 설계하여 매우 높은 내결함성과 확장성을 요구하는 통신 스위치에 사용했습니다. 또한 많은 사람들이 Erlang의 장점을 발견하고 사용하기 시작했습니다. 우리는 월스트리트용으로 설계된 일반적인 시스템보다 훨씬 더 높은 신뢰성과 확장성을 요구하는 통신 제어 시스템에 대해 이야기하고 있습니다. 실제로 Erlang 시스템은 안정적이거나 확장 가능하지 않지만 JavaScript는 그렇습니다. Erlang 시스템은 매우 견고합니다.
병렬성에 대한 이야기는 여기서 끝나지 않습니다. 프로그램이 단일 스레드인 경우에도 기능적 프로그램 컴파일러는 여러 CPU에서 실행되도록 최적화할 수 있습니다. 다음 코드를 살펴보십시오.
String s1 = someLongOperation1();
String s2 = 다소LongOperation2();
String s3 = concatenate(s1, s2);
함수형 프로그래밍 언어에서 컴파일러는 코드를 분석하여 문자열 s1 및 s2를 생성하는 데 시간이 많이 걸릴 수 있는 함수를 식별한 다음 이를 병렬로 실행합니다. 이는 각 함수가 함수 범위 외부에서 상태를 수정할 수 있고 후속 함수가 이러한 수정에 따라 달라질 수 있는 명령형 언어에서는 불가능합니다. 함수형 언어에서 자동으로 함수를 분석하고 병렬 실행에 적합한 후보를 식별하는 것은 자동 함수 인라인만큼 간단합니다! 이런 의미에서 함수형 프로그래밍은 "미래 보장형"입니다(업계 용어를 사용하고 싶지 않더라도 이번에는 예외를 두겠습니다). 하드웨어 제조업체는 더 이상 CPU 실행 속도를 높일 수 없었기 때문에 프로세서 코어의 속도를 높이고 병렬성으로 인해 4배의 속도 향상을 달성했습니다. 물론 그들은 우리가 지출한 추가 비용이 병렬 문제를 해결하기 위한 소프트웨어에만 사용되었다는 사실도 잊어버렸습니다. 소수의 명령형 소프트웨어와 100% 기능적 소프트웨어가 이러한 시스템에서 직접 병렬로 실행될 수 있습니다.
Windows에 업데이트를 설치하고 컴퓨터를 다시 시작해야 하는 데 사용되는코드의 핫 배포는
불가피했으며 새 버전의 미디어 플레이어가 설치된 경우에도 두 번 이상 발생했습니다. Windows XP는 이 상황을 크게 개선했지만 여전히 이상적이지는 않습니다. (오늘 직장에서 Windows 업데이트를 실행했는데 이제 시스템을 재부팅하지 않으면 항상 짜증나는 아이콘이 트레이에 표시됩니다.) Unix 시스템은 항상 더 나은 모드에서 실행됩니다. 업데이트를 설치할 때 전체 운영 체제가 아닌 시스템 관련 구성 요소만 중지하면 됩니다. 그럼에도 불구하고 이는 대규모 서버 애플리케이션에는 여전히 만족스럽지 않습니다. 통신 시스템은 시스템이 업데이트되는 동안 긴급 전화 걸기에 실패할 경우 인명 손실이 발생할 수 있으므로 항상 100% 작동해야 합니다. 월스트리트 기업들이 업데이트를 설치하기 위해 주말에 서비스를 중단할 이유가 없습니다.
이상적인 상황은 시스템의 구성 요소를 전혀 중지하지 않고 관련 코드를 업데이트하는 것입니다. 이것은 명령형 세계에서는 불가능합니다. 런타임이 Java 클래스를 업로드하고 새 정의를 재정의하면 저장된 상태가 손실되므로 이 클래스의 모든 인스턴스를 사용할 수 없게 된다는 점을 고려하세요. 이 문제를 해결하기 위해 지루한 버전 제어 코드 작성을 시작한 다음 이 클래스의 모든 인스턴스를 직렬화하고 이러한 인스턴스를 삭제한 다음 이 클래스의 새로운 정의로 이러한 인스턴스를 다시 만든 다음 이전에 직렬화된 데이터를 로드하고 로드가 코드는 해당 데이터를 새 인스턴스로 올바르게 이식합니다. 게다가 업데이트할 때마다 포팅 코드를 수동으로 다시 작성해야 하며 개체 간의 상호 관계가 깨지지 않도록 세심한 주의를 기울여야 합니다. 이론은 간단하지만 실천은 쉽지 않습니다.
기능적 프로그램의 경우 모든 상태, 즉 함수에 전달된 매개변수가 스택에 저장되므로 핫 배포가 매우 간편해집니다. 실제로 우리가 해야 할 일은 작업 코드와 새 버전을 비교한 다음 새 코드를 배포하는 것뿐입니다. 나머지는 언어 도구에 의해 자동으로 수행됩니다! 이것이 공상 과학 이야기라고 생각한다면 다시 생각해보십시오. 수년 동안 Erlang 엔지니어들은 실행 중인 시스템을 중단하지 않고 업데이트해 왔습니다.
기계 지원 추론 및 최적화
함수형 언어의 흥미로운 속성은 수학적으로 추론할 수 있다는 것입니다. 함수형 언어는 단지 형식 시스템의 구현일 뿐이므로 종이에서 수행되는 모든 작업은 이 언어로 작성된 프로그램에 적용될 수 있습니다. 컴파일러는 수학적 이론을 사용하여 코드 조각을 동일하지만 보다 효율적인 코드로 변환할 수 있습니다[7]. 관계형 데이터베이스는 수년 동안 이러한 유형의 최적화를 진행해 왔습니다. 이 기술을 일반 소프트웨어에 적용하지 못할 이유가 없습니다.
또한 이러한 기술을 사용하여 프로그램의 일부가 정확하다는 것을 증명할 수 있으며, 코드를 분석하고 단위 테스트를 위한 엣지 케이스를 자동으로 생성하는 도구를 만들 수도 있습니다! 이 기능은 강력한 시스템에는 아무런 가치가 없지만, 속도 조정기나 항공 교통 관제 시스템을 설계하는 경우 이 도구는 반드시 필요합니다. 귀하가 작성하는 애플리케이션이 업계의 핵심 작업이 아닌 경우 이러한 유형의 도구는 경쟁사에 비해 비장의 카드를 제공할 수도 있습니다.
23.4.3 함수형 프로그래밍의 단점
클로저의 부작용
비엄격 함수형 프로그래밍에서 클로저는 외부 환경을 무시할 수 있으며(이전 장에서 이미 살펴보았습니다), 이는 부작용을 가져오고 이러한 부작용이 자주 발생할 때 그리고 프로그램이 실행되는 환경이 자주 변경되면 오류를 추적하기가 어려워집니다.
//TODO:
재귀 형식
재귀는 종종 가장 간결한 표현 형태이지만 비재귀 루프만큼 직관적이지는 않습니다.
//TODO:
지연된 값의 약점
//TODO: