IT TIP

함수형 프로그래밍의 맥락에서 구성 가능성은 무엇을 의미합니까?

itqueen 2020. 11. 30. 20:33
반응형

함수형 프로그래밍의 맥락에서 구성 가능성은 무엇을 의미합니까?


함수형 프로그래머가 어떤 것을 구성 가능하거나 구성 할 수 없다고 말할 때 의미하는 것은 무엇입니까?

내가 읽은 이런 종류의 진술 중 일부는 다음과 같습니다.

  • 제어 구조는 구성 할 수 없습니다.
  • 스레드는 작성하지 않습니다.
  • 모나 딕 연산은 구성 가능합니다.

Marcelo Cantos는 꽤 좋은 설명 을했지만 조금 더 정확할 수 있다고 생각합니다.

사물의 유형은 동일한 유형의 사물을 생성하기 위해 특정 방식으로 여러 인스턴스를 결합 할 수있을 때 구성 할 수 있습니다.

제어 구조 구성 가능성. C와 같은 언어를 구별하게 표현 새 표현을 생산하는 연산자를 사용하여 구성 할 수있다, 그리고 문장 같은 제어 구조를 사용하여 구성 할 수 있습니다 if, for그리고 "시퀀스 제어 구조"순서로 간단하게 수행 문 있음을. 이 배열의 문제는이 두 범주가 동일한 기반에 있지 않다는 것입니다. 많은 제어 구조가 표현식을 사용하지만 (예 : if실행할 분기를 선택하기 위해 평가되는 표현식 ), 표현식은 제어 구조를 사용할 수 없습니다 (예 : for루프를 반환 할 수 없습니다 ). "돌아가고 싶다"는 게 미친 짓이거나 무의미 해 보일 수 있지만for저장 주위에 전달 될 수있는 일류의 개체로 제어 구조 치료의 일반적인 생각은 할 수 있지만 유용한뿐만 아니라 실제로 루프 ". 게으른 기능 하스켈 같은 언어, 제어 구조에서 같은 iffor일반 함수로 표현 될 수있는 전달 된 매개 변수에 따라 다른 함수를 "빌드"하고 호출자에게 반환하는 함수와 같은 기능을 활성화하여 다른 용어와 마찬가지로 표현식에서 조작 할 수 있습니다. 따라서 C (예 : 프로그래머는 "하고 싶을 수도 있습니다."라는 범주의 객체를 결합하는 방법을 제한하고 이러한 범주의 객체를 결합 할 수있는 방법을 제한합니다. Haskell (예를 들어)에는 하나의 범주 만 있고 그러한 제한을 부과하지 않으므로 이러한 의미에서 더 많은 구성 성을 제공합니다.

스레드 구성 가능성. Marcelo Cantos가 한 것처럼 잠금 / 뮤텍스를 사용하는 스레드의 구성 가능성에 대해 실제로 이야기하고 있다고 가정합니다. 이것은 약간 더 까다로운 경우입니다. 겉으로는 여러 잠금을 사용하는 스레드를 가질 있기 때문입니다 . 그러나 중요한 점은 .NET Core를 갖도록 보장하는 여러 잠금을 사용하는 스레드를 가질 수 없다는 입니다.

우리는 잠금을 수행 할 수있는 특정 작업이있는 사물의 유형으로 정의 할 수 있습니다. 한 가지 보장은 다음과 같습니다. 잠금 객체가 있다고 가정하고 x, 호출하는 모든 프로세스가 lock(x)결국을 호출 unlock(x)하면에 대한 모든 호출 lock(x)은 결국 x현재 스레드 / 프로세스에 의해 잠긴 상태 로 성공적으로 반환 됩니다. 이 보장은 프로그램 동작에 대한 추론을 크게 단순화합니다.

불행히도 세계에 둘 이상의 자물쇠가 있으면 더 이상 사실이 아닙니다. 스레드 A가 호출 lock(x); lock(y);하고 스레드 B가 호출 lock(y); lock(x);하면 A가 잠금 x을 잡고 B가 잠금을 잡고 y둘 다 다른 스레드가 다른 잠금 (교착 상태)을 해제 할 때까지 무기한 대기 할 수 있습니다. 따라서 잠금을 구성 할 수 없습니다. 두 개 이상을 사용할 때 잠금을 관리하는 방법을보기 위해 코드를 자세히 분석하지 않고서 는 중요한 보증이 여전히 유지된다고 주장 할 수 없기 때문 입니다. 즉, 더 이상 기능을 "블랙 박스"로 취급 할 여유가 없습니다.

구성 가능한 것은 추상화 를 가능하게하므로 모든 세부 사항에 신경을 쓰지 않고도 코드에 대해 추론 할 수 있고 프로그래머의인지 부담을 줄여주기 때문에 좋습니다.


구성 가능성의 간단한 예는 Linux 명령 줄입니다. 여기서 파이프 문자를 사용하면 간단한 명령 (ls, grep, cat 등)을 거의 무제한의 방법으로 결합 할 수 있으므로 여러 가지 복잡한 동작을 "구성"할 수 있습니다. 적은 수의 단순한 기본 요소.

구성 가능성에는 몇 가지 이점이 있습니다.

  • 보다 균일 한 동작 : 예를 들어 "결과를 한 번에 한 페이지 씩 표시"( more) 를 구현하는 단일 명령을 사용 하면 모든 명령이 자체 메커니즘 (및 명령)을 구현하는 경우 불가능한 수준의 페이징 균일 성을 얻을 수 있습니다. 라인 플래그) 페이징을 수행합니다.
  • 덜 반복되는 구현 작업 (DRY) : 수많은 다른 페이징 구현을 사용하는 대신 모든 곳에서 사용되는 하나만 있습니다.
  • 주어진 양의 구현 노력에 대한 더 많은 기능 : 기존의 프리미티브를 결합하여 구성 불가능한 모 놀리 식 명령을 구현하는 데 동일한 노력이 들어간 경우보다 훨씬 더 광범위한 작업을 해결할 수 있습니다.

Linux 명령 줄 예제에서 알 수 있듯이 구성 가능성은 반드시 함수형 프로그래밍에만 국한되지는 않지만 개념은 동일합니다. 제한된 작업을 수행하는 작은 코드 조각을 갖고 출력과 입력을 적절하게 라우팅하여 더 복잡한 기능을 구축합니다.

요점은 함수형 프로그래밍이 이에 적합하다는 것입니다. 불변 변수와 부작용에 대한 제한을 사용하면 호출되는 함수의 내부에서 일어나는 일에 대해 걱정할 필요가 없으므로 더 쉽게 작성할 수 있습니다. 공유 변수를 업데이트하여 결과를 얻습니다. 특정 작업 시퀀스에 대해 유효하지 않거나 공유 잠금에 액세스하여 특정 호출 시퀀스가 ​​교착 상태를 제공합니다.

이것이 함수형 프로그래밍의 구성 가능성입니다. 모든 함수는 입력 매개 변수에만 의존하며 출력은 반환 값의 유형을 처리 할 수있는 모든 함수에 전달 될 수 있습니다.

확장하면 데이터 유형이 적을수록 구성 가능성이 높아집니다. Clojure의 Rich Hickey는

모든 새로운 객체 유형은 본질적으로 지금까지 작성된 모든 코드와 호환되지 않습니다.

그것은 확실히 잘된 요점입니다.

실용적인 구성 가능성은 또한 Unix 명령이 "탭으로 구분 된 줄 기반 텍스트"표준을 사용하는 것처럼 작은 데이터 유형 집합에 대한 표준화에 달려 있습니다.

추신

Eric Raymond는 Unix 철학에 관한 책을 썼습니다. 그가 나열한 두 가지 디자인 원칙은 다음과 같습니다.

  • 모듈화 규칙 : 깨끗한 인터페이스로 연결된 간단한 부품을 작성합니다.
  • 구성 규칙 : 다른 프로그램과 연결되도록 프로그램을 디자인합니다.

에서 http://catb.org/~esr/writings/taoup/html/ch01s06.html#id2877537

함수형 프로그래밍의 구성 가능성은 이러한 원칙을 개별 함수 수준으로 낮추는 것이라고 말할 수 있습니다.


컴퓨터 과학의 구성은 단순한 행동을 모아 복잡한 행동을 조합하는 능력입니다. 기능적 분해 (Functional decomposition)가 이에 대한 예입니다. 복잡한 함수는 이해하기 쉬운 작은 함수로 분할되고 최상위 함수에 의해 최종 시스템으로 조립됩니다. 최상위 기능은 조각을 전체로 "구성"했다고 말할 수 있습니다.

특정 개념은 쉽게 구성되지 않습니다. 예를 들어, 스레드로부터 안전한 데이터 구조는 요소의 안전한 삽입 및 제거를 허용 할 수 있으며 데이터 구조 또는 일부 하위 집합을 잠 가서 한 스레드가 변경 사항을 방해하지 않고 필요한 조작을 수행 할 수 있도록합니다. 작동하는 동안 데이터 구조가 손상되었습니다. 그러나 비즈니스 기능은 한 컬렉션에서 요소를 제거한 다음 다른 컬렉션에 삽입하고 전체 작업을 원자 적으로 수행해야 할 수 있습니다. 문제는 잠금이 데이터 구조별로 발생한다는 것입니다. 하나에서 요소를 안전하게 제거 할 수 있지만 일부 키 위반으로 인해 다른 요소에 삽입 할 수 없습니다. 또는 1 초에 삽입 한 다음 1 초에서 제거 할 수도 있습니다. 다른 실이 당신의 코 아래에서 그것을 훔친 것을 발견했습니다. 작업을 완료 할 수 없다는 사실을 깨달은 후에는 상황을 원래대로 되돌리려 고 시도 할 수 있으며, 비슷한 이유로 반전이 실패하고 이제는 림보 상태에 있습니다! 물론 여러 데이터 구조를 포괄하는 더 풍부한 잠금 체계를 구현할 수 있지만 모든 사람이 새로운 잠금 체계에 동의하고 모든 작업이 단일 작업에서 수행되는 경우에도 항상 사용하는 부담을 짊어진 경우에만 작동합니다. 데이터 구조.

Mutex-style locking is thus a concept that doesn't compose. You can't implement higher-level thread-safe behaviour simply by aggregating lower-level thread-safe operations. The solution in this case is to use a concept that does compose, such as STM.


I agree with Marcelo Cantos's answer, but I think it may assume more background than some readers have, which is also related to why composition in functional programming is special. Functional programming function composition is essentially identical to function composition in mathematics. In math, you may have a function f(x) = x^2, and a function g(x) = x + 1. Composing the functions means creating a new function, in which the function arguments are given to the "inner" function, and the "inner" function's output serves as input to the "outer" function. Composing f outer with g inner could be written f(g(x)). If you provide a value of 1 for x, then g(1) == 1 + 1 == 2, so f(g(1)) == f(2) == 2^2 == 4. More generally, f(g(x)) == f(x + 1) == (x+1)^2. I described composition using the f(g(x)) syntax, but mathematicians often prefer a different syntax, (f . g)(x). I think this is because it makes clearer that f composed with g is a function in its own right, which takes a single argument.

Functional programs are built entirely using compositional primitives. A program in Haskell is, to possibly oversimplify, a function that takes as an argument a run-time environment, and returns the result of some manipulation of that environment. (Grokking this statement will require some understanding of monads.) Everything else is done with composition in the mathematical sense.


Another example: consider async programming in .NET. If you use a language like C# and need to make a series of asynchronous (non-blocking) I/O calls via Begin/End APIs, then in order to call two operations, Foo and Bar, in sequence, you have to expose two methods (BeginFooAndBar, EndFooAndBar), where BeginFooAndBar calls BeginFoo and passes a callback to Intermediate, and Intermediate then calls BeginBar, and you have to thread the intermediate values and IAsyncResult state information through, and if you want a try-catch block around the whole thing, then good luck, you need to duplicate the exception handling code in three places, yikes, and it's an awful mess.

But then with F#, there's the async type, built atop functional continuations that are composable, and so you can write e.g.

let AsyncFooAndBar() = async {
    let! x = Async.FromBeginEnd(BeginFoo, EndFoo)
    let! y = Async.FromBeginEnd(BeginBar, EndBar, x)
    return y * 2 }

or what have you, and it's simple, and if you want to put a try-catch around it, great, the code is all in one method, rather than spread across three, you just put a try-catch around it and it works.


Here's a real-world example. The names of all of the people that live in your house is the list of names of all the males in your house combined with the list of all females in your house.

This is composable, as each of those two subproblems can be solved independently and without interfering with solving the other one.

On the other hand, many recipes are not composable, as the steps must be done in a particular order and rely upon the results of other steps. You have to break the eggs before you whisk them!


Composability allows for the developer community to continually raise the level of abstraction, multiple levels, without being chained to the base layer.

참고URL : https://stackoverflow.com/questions/2887013/what-does-composability-mean-in-context-of-functional-programming

반응형