ฉันจะไปรอบ ๆ พุ่มไม้สักพัก แต่ก็มีประเด็น
semigroups
คำตอบคือเชื่อมโยงทรัพย์สินของการดำเนินงานที่ลดลงไบนารี
มันค่อนข้างเป็นนามธรรม แต่การคูณเป็นตัวอย่างที่ดี ถ้า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 )))
- สูงสุดและต่ำสุด
- นอกจากนี้โมดูโลพี
บางสิ่งที่ไม่:
- การลบเพราะ 1- (1-2) ≠ (1-1) -2
- x ⊕ y = tan ( x + y ) เนื่องจาก tan (π / 4 + π / 4) ไม่ได้ถูกกำหนด
- การคูณเหนือจำนวนลบเพราะ -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
พิมพ์ในห้องสมุดมาตรฐาน มันเรียกร้องการดำเนินงานของทั่วไปผู้ประกอบการSemigroup
<>
เนื่องจากรายการและสตริงเป็นอินสแตนซ์ของอินสแตนซ์ของSemigroup
พวกเขาจึงกำหนด<>
เป็นตัวดำเนินการเรียงต่อกัน++
เป็นต้น และมีการนำเข้าที่เหมาะสม[a] <> [b]
เป็นนามแฝงสำหรับซึ่งเป็น[a] ++ [b]
[a,b]
แต่แล้วตัวเลขล่ะ เราเพียงแค่เห็นว่าประเภทที่เป็นตัวเลข semigroups ภายใต้การอย่างใดอย่างหนึ่งนอกจากหรือคูณ! เพื่อที่หนึ่งที่ได้รับจะ<>
เป็นDouble
? อย่างใดอย่างหนึ่ง! Haskell กำหนดประเภทProduct Double
, where (<>) = (*)
(นั่นคือความหมายที่เกิดขึ้นจริงใน Haskell) และยัง,Sum Double
where (<>) = (+)
หนึ่งรอยย่นคือคุณใช้ความจริงที่ว่า 1 คือเอกลักษณ์ของการคูณ กึ่งกลุ่มที่มีตัวตนที่เรียกว่าหนังสือและถูกกำหนดไว้ในแพคเกจ Haskell Data.Monoid
ซึ่งเรียกเอกลักษณ์องค์ประกอบทั่วไปของ mempty
typeclass Sum
, Product
และรายชื่อแต่ละคนมีเอกลักษณ์องค์ประกอบ (0, 1 และ[]
ตามลำดับ) เพื่อให้พวกเขามีกรณีเช่นเดียวกับMonoid
Semigroup
(เพื่อไม่ให้สับสนกับmonadดังนั้นเพียงลืมฉันยังนำขึ้นเหล่านั้น)
ข้อมูลเพียงพอที่จะแปลอัลกอริทึมของคุณเป็นฟังก์ชัน Haskell โดยใช้ monoids:
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.
ที่สำคัญโปรดทราบว่านี่คือ semigroup หางแบบเรียกซ้ำหาง: ทุกกรณีเป็นค่าการเรียกแบบเรียกซ้ำหางหรือผลิตภัณฑ์กึ่งกลุ่มของทั้งสองอย่าง นอกจากนี้ตัวอย่างนี้เกิดขึ้นเพื่อใช้mempty
สำหรับกรณีใดกรณีหนึ่ง แต่ถ้าเราไม่ต้องการสิ่งนั้นเราสามารถทำมันด้วยประเภททั่วไปที่กว้าง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
การคูณคือการบวกตามการยกกำลังคือการคูณ!
แต่ฉันให้อีกตัวอย่างหนึ่งของ Hoidell monoid: รายการที่มีการดำเนินการเรียงต่อกัน
*StylizedRec> pow [2] 4
[2,2,2,2]
*StylizedRec> pow [7] 2
[7,7]
เขียน[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
การทำซ้ำแต่ละครั้งของฟังก์ชั่นอาจจะสามารถใช้กรอบสแต็คเดียวกันอีกครั้งและอัปเดตพารามิเตอร์ให้เข้าที่
การพับแบบด้านขวาหรือแบบพับด้านซ้ายอย่างเข้มงวดอาจเหมาะสมกว่าในบางกรณีดังนั้นควรรู้ว่าคุณต้องการแบบใด นอกจากนี้ยังมีบางสิ่งที่สามารถพับครึ่งทางขวาเท่านั้น (เช่นสร้างเอาต์พุตแบบโต้ตอบโดยไม่ต้องรออินพุตทั้งหมดและทำงานในรายการที่ไม่มีที่สิ้นสุด) อย่างไรก็ตามที่นี่เรากำลังลดลำดับของการปฏิบัติงานให้เป็นค่าแบบง่ายดังนั้นรอยพับด้านซ้ายที่เข้มงวดคือสิ่งที่เราต้องการ
ดังนั้นอย่างที่คุณเห็นมันเป็นไปได้ที่จะปรับออเดอร์โมดูโลแบบเรียกซ้ำแบบหางโดยอัตโนมัติ semigroup ใด ๆ (ตัวอย่างหนึ่งซึ่งเป็นประเภทตัวเลขใด ๆ ที่ปกติภายใต้การคูณ) เป็นการพับแบบขี้เกียจขวา Haskell
สรุปเพิ่มเติม
อาร์กิวเมนต์สองตัวของการดำเนินการแบบไบนารีไม่จำเป็นต้องเป็นชนิดเดียวกันตราบใดที่ค่าเริ่มต้นเป็นประเภทเดียวกันกับผลลัพธ์ของคุณ (แน่นอนว่าคุณสามารถพลิกอาร์กิวเมนต์ได้เสมอเพื่อให้ตรงกับคำสั่งของรอยพับที่คุณทำอยู่ทางซ้ายหรือขวา) ดังนั้นคุณอาจเพิ่มแพตช์ลงในไฟล์ซ้ำ ๆ เพื่อรับไฟล์ที่อัพเดตหรือเริ่มต้นด้วยค่าเริ่มต้นที่ 1.0 หารด้วยจำนวนเต็มเพื่อสะสมผลลัพธ์ทศนิยม หรือเติมองค์ประกอบลงในรายการว่างเพื่อรับรายการ
การวางนัยทั่วไปอีกประเภทหนึ่งคือการนำ fold ไปใช้กับรายการ แต่ใช้กับ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
)