나는 잠시 동안 덤불 주위를 때릴 것입니다. 그러나 요점이 있습니다.
반 그룹
답은 이진 축소 연산의 연관 속성입니다 .
그것은 매우 추상적이지만 곱셈이 좋은 예입니다. 경우 X , Y 및 Z는 어떤 자연수 (또는 정수 또는 유리수, 또는 실수 또는 복소수, 또는이다 N × N의 행렬, 또는 전체 무리의 더 이상 일을), 다음 X × Y는 같은 종류입니다 x 와 y 모두 숫자 입니다. 우리는 두 개의 숫자로 시작하여 이항 연산이고 하나를 얻었으므로 우리는 숫자의 수를 하나씩 줄 였으므로 이것을 감소 연산으로 만듭니다. 그리고 ( x × y ) × z 는 항상 x × ( y ×z )는 연관 특성입니다.
(이미 이미 알고 있다면 다음 섹션으로 건너 뛸 수 있습니다.)
동일한 방식으로 작동하는 컴퓨터 과학에서 자주 볼 수있는 몇 가지 사항 :
- 곱하기 대신 이런 종류의 숫자를 더하는 것
- 문자열을 연결하는 (
"a"+"b"+"c"
인 "abc"
당신이 시작 여부 "ab"+"c"
또는 "a"+"bc"
)
- 두 목록을 함께 연결합니다.
[a]++[b]++[c]
마찬가지로 [a,b,c]
앞뒤로 또는 앞뒤로입니다.
cons
머리를 싱글 톤 목록으로 생각하면 머리와 꼬리에. 그것은 단지 두 개의 목록을 연결하는 것입니다.
- 집합의 조합 또는 교차점을 복용
- 부울 및 부울 또는
- 비트
&
, |
그리고^
- 함수 구성 : ( f ∘ g ) ∘ h x = f ∘ ( g ∘ h ) x = f ( g ( h ( x )))
- 최대 및 최소
- 추가 모듈로 p
그렇지 않은 것들 :
- 1- (1-2) ≠ (1-1) -2이므로 뺄셈
- tan (π / 4 + π / 4)가 정의되지 않았으므로 x ⊕ y = tan ( x + y )
- -1 × -1은 음수가 아니므로 음수에 대한 곱셈
- 세 가지 문제가 모두있는 정수 나누기!
- 논리적이 아닌 두 개의 피연산자가 아닌 하나의 피연산자가 있으므로
int print2(int x, int y) { return printf( "%d %d\n", x, y ); }
등 print2( print2(x,y), z );
및 print2( x, print2(y,z) );
다른 출력을 갖는다.
우리가 명명 한 유용한 개념입니다. 이러한 속성을 가진 연산이 포함 된 집합은 semigroup 입니다. 따라서 곱셈의 실수는 반 그룹입니다. 그리고 당신의 질문은 이런 종류의 추상화가 현실 세계에서 유용하게되는 방법 중 하나 인 것으로 밝혀졌습니다. 원하는대로 세미 그룹 작업을 최적화 할 수 있습니다.
집에서 이것을 시도
내가 아는 한,이 기술은 1974 년 Daniel Friedman과 David Wise의 논문 인 “스타일 화 된 재귀를 반복으로 접는 방법” 에서 처음 설명 되었지만, 필요한 것보다 몇 가지 더 많은 속성을 가정했습니다.
Haskell은 Semigroup
표준 라이브러리에 typeclass 가 있기 때문에 이것을 설명하기에 좋은 언어 입니다. 일반 Semigroup
연산자의 연산자를 호출합니다 <>
. 목록과 문자열은의 인스턴스 Semigroup
이므로 해당 인스턴스 는 예를 들어 <>
연결 연산자로 정의 됩니다 ++
. 그리고 올바른 가져 오기 [a] <> [b]
를 사용하면의 별칭 [a] ++ [b]
이입니다 [a,b]
.
그러나 숫자는 어떻습니까? 우리는 단지 숫자 유형에 따라 반군 있습니다 본 중 하나를 추가 또는 곱셈! 어느 그래서 하나가 될 얻는다 <>
A에 대한 Double
? 둘 중 하나! 하스켈 유형 정의 Product Double
, where (<>) = (*)
또한 (즉 하스켈 실제의 정의이다)과 Sum Double
, where (<>) = (+)
.
하나의 주름은 1이 곱셈이라는 사실을 사용했다는 것입니다. 아이덴티티가있는 세미 그룹을 모노 아이디라고하며 Haskell 패키지에 정의되어 있으며 Data.Monoid
,이 클래스는 typeclass의 일반 아이덴티티 요소를 호출합니다 mempty
. Sum
, Product
및 각리스트는 식별 소자 (0, 1 및 보유 []
가 인스턴스 그래서 각각 등) Monoid
뿐만 아니라이 Semigroup
. ( 모나드 와 혼동하지 마십시오. 모니터를 키우는 것을 잊지 마십시오 .)
모노 이드를 사용하여 알고리즘을 Haskell 함수로 변환하기에 충분한 정보입니다.
module StylizedRec (pow) where
import Data.Monoid as DM
pow :: Monoid a => a -> Word -> a
{- Applies the monoidal operation of the type of x, whatever that is, by
- itself n times. This is already in Haskell as Data.Monoid.mtimes, but
- let’s write it out as an example.
-}
pow _ 0 = mempty -- Special case: Return the nullary product.
pow x 1 = x -- The base case.
pow x n = x <> (pow x (n-1)) -- The recursive case.
중요하게도 이것은 꼬리 재귀 모듈로 반 그룹입니다. 모든 경우는 값, 꼬리 재귀 호출 또는 두 그룹의 반 그룹 곱입니다. 또한이 예제 mempty
는 사례 중 하나에 사용 되었지만, 필요하지 않은 경우보다 일반적인 typeclass로 수행 할 수 있습니다 Semigroup
.
이 프로그램을 GHCI에로드하고 어떻게 작동하는지 봅시다 :
*StylizedRec> getProduct $ pow 2 4
16
*StylizedRec> getProduct $ pow 7 2
49
우리 pow
가 제네릭 Monoid
형식으로 선언 한 방식을 기억 a
하십니까? 우리는 유형 것을 추론 할 GHCI 충분한 정보를 준 a
여기 Product Integer
인, instance
의 Monoid
, 그 <>
작업이 정수의 곱셈이다. 따라서 로 또는로 pow 2 4
확장됩니다 . 여태까지는 그런대로 잘됐다.2<>2<>2<>2
2*2*2*2
16
그러나 우리의 함수는 일반적인 monoid 연산만을 사용합니다. 이전에, 나는 또 다른 인스턴스가 존재했다 Monoid
라는 Sum
누구의 <>
작업입니다 +
. 시도해 볼 수 있습니까?
*StylizedRec> getSum $ pow 2 4
8
*StylizedRec> getSum $ pow 7 2
14
이제 동일한 확장이 2+2+2+2
대신에 우리 를 제공합니다 2*2*2*2
. 지수는 곱셈이므로 곱셈은 더하기입니다!
그러나 나는 Haskell monoid : list의 다른 예를 제시했습니다.
*StylizedRec> pow [2] 4
[2,2,2,2]
*StylizedRec> pow [7] 2
[7,7]
쓰기는 [2]
,이 목록입니다 컴파일러를 알려줍니다 <>
목록에 ++
, 그래서 [2]++[2]++[2]++[2]
이다 [2,2,2,2]
.
마지막으로, 알고리즘 (실제로 2 개)
단순히 교체함으로써 x
와 함께 [x]
, 당신은 사용이 목록을 만듭니다 하나에 모듈 반군 재귀하는 일반적인 알고리즘을 변환합니다. 어느 목록입니까? 알고리즘이 적용되는 요소 목록 <>
. 리스트도 가지고있는 세미 그룹 연산 만 사용했기 때문에 결과리스트는 원래 계산과 동형이됩니다. 그리고 원래의 작업은 연관성이 있었으므로 요소를 앞뒤로 또는 앞뒤로 동등하게 평가할 수 있습니다.
알고리즘이 기본 사례에 도달하고 종료되면 목록이 비어 있지 않습니다. 터미널 케이스가 무언가를 반환했기 때문에 목록의 마지막 요소가되므로 적어도 하나의 요소를 갖게됩니다.
목록의 모든 요소에 이진 축소 작업을 어떻게 순서대로 적용합니까? 그렇습니다. 따라서을 대신 [x]
하고 x
로 줄일 요소 목록을 <>
가져온 다음 목록을 오른쪽 또는 왼쪽으로 접을 수 있습니다.
*StylizedRec> getProduct $ foldr1 (<>) $ pow [Product 2] 4
16
*StylizedRec> import Data.List
*StylizedRec Data.List> getProduct $ foldl1' (<>) $ pow [Product 2] 4
16
와 버전 foldr1
실제로는 같은 표준 라이브러리에 존재 sconcat
에 대한 Semigroup
및 mconcat
대한 Monoid
. 목록에서 게으른 오른쪽 접기를 수행합니다. 즉,로 확장 [Product 2,Product 2,Product 2,Product 2]
됩니다 2<>(2<>(2<>(2)))
.
이 경우에는 모든 용어를 생성 할 때까지 개별 용어로 아무 것도 수행 할 수 없으므로이 방법은 효율적이지 않습니다. (한 시점에서 오른쪽 접기를 사용할 때와 엄격한 왼쪽 접기를 사용할 때에 대해 논의했지만 너무 멀리 나갔습니다.)
버전 foldl1'
은 엄격하게 평가 된 왼쪽 접기입니다. 즉, 엄격한 누산기를 갖춘 꼬리 재귀 함수입니다. 이 평가는으로 평가되며 (((2)<>2)<>2)<>2
나중에 필요할 때 즉시 계산되지 않습니다. (적어도 내에 지연이없는 자체를 겹 :. 게으른 평가를 포함 할 수 있습니다 다른 기능에 의해 여기서 생성되는 접힌되는 목록) 그래서, 배 계산 (4<>2)<>2
, 즉시 계산 8<>2
한 후, 16
. 이것이 우리가 연산을 연관시키는 것이 필요한 이유입니다 : 우리는 단지 괄호의 그룹을 변경했습니다!
엄격한 왼쪽 접기는 GCC가 수행하는 작업과 동일합니다. 이전 예에서 가장 왼쪽에있는 숫자는 누산기 (이 경우 실행중인 제품)입니다. 각 단계마다 목록의 다음 숫자가 곱해집니다. 그것을 표현하는 또 다른 방법은 곱할 값을 반복하여 실행중인 제품을 누산기에 유지하고 각 반복마다 누산기에 다음 값을 곱하는 것입니다. 즉, while
변장 루프입니다.
때로는 효율적으로 만들 수도 있습니다. 컴파일러는 메모리에서 목록 데이터 구조를 최적화 할 수 있습니다. 이론적으로는 컴파일 타임에 여기에서 그렇게해야한다는 것을 알기에 충분한 정보 [x]
가 있습니다. 싱글 톤이므로 [x]<>xs
와 동일합니다 cons x xs
. 함수를 반복 할 때마다 동일한 스택 프레임을 재사용하고 적절한 매개 변수를 업데이트 할 수 있습니다.
특정한 경우에는 오른쪽 접기 또는 엄격한 왼쪽 접기가 더 적절할 수 있으므로 원하는 것을 알고 있어야합니다. 올바른 접힘 만 수행 할 수있는 작업도 있습니다 (예 : 모든 입력을 기다리지 않고 대화식 출력 생성 및 무한 목록에서 작동). 그러나 여기서는 일련의 연산을 간단한 값으로 줄이고 있으므로 엄격한 왼쪽 접기가 우리가 원하는 것입니다.
보시다시피, 하나의 행에서 게으른 오른쪽 접힘 또는 엄격한 왼쪽 접힘에 대해 세미 반복 모듈로를 세미 그룹 (예 : 곱셈에서 일반적인 숫자 유형 중 하나)으로 자동 최적화 할 수 있습니다. 하스켈.
더 일반화
이진 연산의 두 인수는 초기 값이 결과와 동일한 유형 인 한 같은 유형일 필요는 없습니다. (물론 항상 접는 종류, 왼쪽 또는 오른쪽 순서와 일치하도록 인수를 뒤집을 수 있습니다.) 따라서 파일에 패치를 반복적으로 추가하여 업데이트 된 파일을 얻거나 초기 값으로 시작할 수 있습니다. 1.0은 정수로 나눠 부동 소수점 결과를 누적합니다. 또는 목록을 얻으려면 빈 목록 앞에 요소를 추가하십시오.
또 다른 유형의 일반화는 접기를 목록이 아닌 다른 Foldable
데이터 구조 에 적용하는 것 입니다. 종종 불변 선형 링크리스트는 주어진 알고리즘에 대해 원하는 데이터 구조가 아닙니다. 위에서 언급하지 않은 한 가지 문제는 목록보다 요소를 목록보다 앞에 추가하는 것이 훨씬 효율적이며 작업이 정식 적이 지 않을 때 작업 x
의 왼쪽과 오른쪽에 적용 하는 것이 효과적 이지 않다는 것입니다 똑같다. 따라서 x
오른쪽 <>
뿐만 아니라 왼쪽에도 적용 할 수있는 알고리즘을 나타내려면 목록 쌍 또는 이진 트리와 같은 다른 구조를 사용해야 합니다.
또한 연관 속성을 사용하면 나누기 및 정복과 같은 다른 유용한 방법으로 작업을 다시 그룹화 할 수 있습니다.
times :: Monoid a => a -> Word -> a
times _ 0 = mempty
times x 1 = x
times x n | even n = y <> y
| otherwise = x <> y <> y
where y = times x (n `quot` 2)
또는 자동 병렬 처리. 각 스레드는 하위 범위를 값으로 축소 한 다음 다른 범위와 결합합니다.
if(n==0) return 0;
(질문과 같이 1을 반환하지 않습니다) 있습니다.x^0 = 1
버그입니다. 그러나 나머지 질문에는 중요하지 않습니다. 반복적 인 asm은 그 특별한 경우를 먼저 확인합니다. 그러나 이상하게도 반복 구현1 * x
은float
버전 을 만들더라도 소스에 존재하지 않는 배수를 소개합니다 . gcc.godbolt.org/z/eqwine (및 gcc는으로 만 성공-ffast-math
)