Bir süre çalıların etrafında çırpacağım, ama bir nokta var.
yarıgruplar
Cevap, ikili indirgeme işleminin ilişkisel özelliğidir .
Bu oldukça soyut, ama çarpma iyi bir örnek. Eğer x , y ve z , bazı doğal veya numaralar (veya tam sayılar ya da rasyonel sayı veya reel sayılar ya da kompleks numaralar veya olan K x K matrisler veya bütün bir demet daha fazla şeyler), o zaman X x Y aynı tür her ikisi de sayısının x ve y . İki sayı ile başladık, bu bir ikili işlem ve bir tane aldık, bu yüzden sahip olduğumuz sayı sayısını bir azaltarak bu bir azaltma işlemi haline getirdik. Ve ( x × y ) × z her zaman x × ( y × ile aynıdır)z ), ilişkilendirilebilir özelliktir.
(Tüm bunları zaten biliyorsanız, bir sonraki bölüme atlayabilirsiniz.)
Bilgisayar biliminde aynı şekilde çalışan sıkça gördüğünüz birkaç şey:
- çarpmak yerine bu tür numaralardan herhangi birini eklemek
- dizeleri bitiştirmek (
"a"+"b"+"c"
olan "abc"
sen ile başlayan olsun "ab"+"c"
ya "a"+"bc"
)
- İki listeyi birleştirme.
[a]++[b]++[c]
benzer şekilde [a,b,c]
ya arkadan öne ya da önden arkaya doğrudur.
cons
Eğer kafa tek bir liste olarak düşünüyorsanız, kafa ve kuyruk üzerinde. Bu sadece iki listeyi birleştiriyor.
- birlik veya setlerin kesişme noktasını almak
- Boole ve Boole veya
- bitsel
&
, |
ve^
- fonksiyonların bileşimi: ( f ∘ g ) ∘ h x = f ∘ ( g ∘ h ) x = f ( g ( h ( x )))
- maksimum ve minimum
- toplama modulo p
Yapmayan bazı şeyler:
- çıkarma, çünkü 1- (1-2) ≠ (1-1) -2
- x ⊕ y = tan ( x + y ), çünkü tan (π / 4 + π / 4) tanımsız
- negatif sayılar üzerinde çarpma, çünkü -1 × -1 negatif bir sayı değildir
- üç problemin de olduğu tamsayıların bölünmesi!
- mantıklı değil, çünkü sadece bir işlenen var, iki değil
int print2(int x, int y) { return printf( "%d %d\n", x, y ); }
, gibi print2( print2(x,y), z );
ve print2( x, print2(y,z) );
farklı çıktı var.
Adını verdiğimiz yeterince kullanışlı bir kavram. Bu özelliklere sahip bir işleme sahip bir küme bir yarıgruptur . Yani, çarpma altındaki gerçek sayılar bir yarıgruptur. Ve sorunuz bu tür soyutlamanın gerçek dünyada faydalı hale gelmesinin yollarından biri olduğu ortaya çıkıyor. Yarıgrup operasyonlarının tümü sizin istediğiniz şekilde optimize edilebilir.
Bunu Evde Deneyin
Bildiğim kadarıyla, bu teknik ilk olarak 1974'te Daniel Friedman ve David Wise'ın “Stilize Özyinelemelerin İterasyonlara Katlanması” başlıklı makalesinde açıklanmıştı , ancak ihtiyaç duyduklarından birkaç özellik daha varsaydılar.
Haskell bunu göstermek için harika bir dildir, çünkü Semigroup
standart kütüphanesinde typeclass vardır. Genel bir Semigroup
operatörün çalışmasını çağırır <>
. Listeler ve dizeler birer örnek olduğundan Semigroup
, örnekleri <>
birleştirme operatörü olarak tanımlanır ++
. Ve sağ ithalat ile, [a] <> [b]
bir başka adıdır [a] ++ [b]
vardır [a,b]
.
Peki ya sayılar? Biz sadece bu sayısal türleri altında yarıgruplar olan testere ya ek ya da çarpma! Peki hangisi <>
için Double
? Her ikisinden biri! Haskell tiplerini tanımlar Product Double
, where (<>) = (*)
aynı zamanda (yani, Haskell gerçek tanımı) ve Sum Double
, where (<>) = (+)
.
Bir kırışıklık, 1'in çarpımsal kimlik olduğu gerçeğini kullandığınızdır. Kimliğe sahip bir yarıgrup monoid olarak adlandırılır ve Data.Monoid
bir tip sınıfının genel kimlik öğesini çağıran Haskell paketinde tanımlanır mempty
. Sum
, Product
Ve bu liste, her bir kimlik elemanı (0, 1 ve var []
bunlar örnekleri böylece, sırasıyla) Monoid
ve ayrıca Semigroup
. (Bir monad ile karıştırılmamalı , bu yüzden onları bile yetiştirdiğimi unut.)
Bu, algoritmanızı monoidler kullanarak bir Haskell işlevine çevirmek için yeterli bilgi:
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.
Önemli olarak, bunun kuyruk özyineleme modulo yarıgrupu olduğuna dikkat edin: her durum ya bir değer, kuyruk özyinelemeli çağrı veya her ikisinin yarıgrup ürünüdür. Ayrıca, bu örnek mempty
vakalardan biri için kullanıldı , ancak buna ihtiyacımız olmasaydı, daha genel tip sınıfla yapabilirdik Semigroup
.
Bu programı GHCI'ye yükleyelim ve nasıl çalıştığını görelim:
*StylizedRec> getProduct $ pow 2 4
16
*StylizedRec> getProduct $ pow 7 2
49
Biz ilan nasıl hatırla pow
jenerik için Monoid
kimin tip olarak adlandırdığımız, a
? Biz tipin anlamak için GHCI yeterince bilgi verdi a
burada Product Integer
bir olduğunu instance
ve Monoid
kimin <>
operasyon tamsayı çarpımıdır. Yani pow 2 4
özyinelemeli olarak genişler 2<>2<>2<>2
, 2*2*2*2
ya da 16
. Çok uzak çok iyi.
Ancak bizim fonksiyonumuz sadece genel monoid işlemleri kullanır. Daha önce, operasyonu olan başka bir Monoid
çağrı örneği olduğunu söylemiştim . Bunu deneyebilir miyiz?Sum
<>
+
*StylizedRec> getSum $ pow 2 4
8
*StylizedRec> getSum $ pow 7 2
14
Şimdi aynı genişleme bize 2+2+2+2
değil 2*2*2*2
. Çarpma, çarpma için çarpmadır!
Ama bir Haskell monoid örneğine bir örnek daha verdim: işlemleri birleştirme olan listeler.
*StylizedRec> pow [2] 4
[2,2,2,2]
*StylizedRec> pow [7] 2
[7,7]
Yazma [2]
derleyiciye bunun bir liste olduğunu, <>
listelerde de ++
öyle [2]++[2]++[2]++[2]
olduğunu söyler [2,2,2,2]
.
Son olarak, bir Algoritma (İki, Aslında)
Basitçe değiştirerek x
ile [x]
, sen kullandığı listesini oluşturur birine modülo yarıgrubuna bir özyineleme olduğunu jenerik algoritma dönüştürmek. Hangi liste? Algoritmanın uygulandığı öğelerin listesi <>
. Yalnızca listelerin de bulunduğu yarıgrup işlemleri kullandığımız için, sonuçta ortaya çıkan liste orijinal hesaplama için izomorfik olacaktır. Ve orijinal operasyon ilişkisel olduğu için, öğeleri arkadan öne veya önden arkaya eşit derecede iyi değerlendirebiliriz.
Algoritmanız temel bir duruma ulaşır ve sonlanırsa, liste boş olmaz. Terminal durumu bir şey döndürdüğünden, listenin son elemanı olacak, bu yüzden en az bir elemanı olacaktır.
Bir listenin her öğesine sırayla bir ikili azaltma işlemi nasıl uygularsınız? Bu doğru, bir kat. Eğer yerini alabilir Yani [x]
için x
, azaltılmasınıöngörmektedir elemanların listesini almak <>
ve sonra ya sağ kat veya listeyi sol kat:
*StylizedRec> getProduct $ foldr1 (<>) $ pow [Product 2] 4
16
*StylizedRec> import Data.List
*StylizedRec Data.List> getProduct $ foldl1' (<>) $ pow [Product 2] 4
16
İle versiyon foldr1
aslında olduğu gibi, standart kütüphanesinde bulunan sconcat
için Semigroup
ve mconcat
için Monoid
. Listede tembel bir sağ kat yapar. Olduğunu, bu genişler [Product 2,Product 2,Product 2,Product 2]
için 2<>(2<>(2<>(2)))
.
Bu durumda bu etkili değildir, çünkü hepsini oluşturana kadar tek tek terimlerle hiçbir şey yapamazsınız. (Bir noktada burada sağ kıvrımların ne zaman ve katı sol kıvrımların ne zaman kullanılacağı hakkında bir tartışma yaptım, ancak çok uzağa gitti.)
İle sürüm foldl1'
kesinlikle değerlendirilmiş bir sol kat. Yani, sıkı bir akümülatörlü bir kuyruk özyinelemeli işlev. Bu değerlendirilir (((2)<>2)<>2)<>2
, hemen hesaplanır ve gerektiğinde hesaplanmaz. (En azından, katlamanın içinde herhangi bir gecikme yoktur: katlanan liste burada tembel değerlendirme içerebilecek başka bir işlev tarafından oluşturulur.) Böylece, kat hesaplar (4<>2)<>2
, sonra hemen hesaplar 8<>2
, sonra 16
. Bu yüzden operasyonun çağrışımsal olması gerekti: parantezlerin gruplandırılmasını değiştirdik!
Katı sol kat, GCC'nin yaptıklarına eşdeğerdir. Önceki örnekteki en soldaki sayı akümülatördür, bu durumda çalışan bir üründür. Her adımda, listedeki bir sonraki sayıyla çarpılır. Bunu ifade etmenin başka bir yolu da: çalışan ürünü bir akümülatörde tutarak, çarpılacak değerler üzerinde tekrarlarsınız ve her bir yinelemede, akümülatörü bir sonraki değerle çarparsınız. Yani, while
kılık değiştirmiş bir döngü.
Bazen bu kadar verimli hale getirilebilir. Derleyici, bellekteki liste veri yapısını optimize edebilir. Teorik olarak, burada yapması gerektiğini anlamak için derleme zamanında yeterli bilgiye sahiptir: [x]
bir singleton, [x]<>xs
aynı şekilde cons x xs
. İşlevin her yinelemesi aynı yığın çerçevesini yeniden kullanabilir ve parametreleri yerinde güncelleyebilir.
Belirli bir durumda ya sağ kat ya da katı sol kat daha uygun olabilir, bu yüzden hangisini istediğinizi bilin. Sadece sağ katlamanın yapabileceği bazı şeyler de vardır (tüm girişi beklemeden etkileşimli çıktı üretmek ve sonsuz bir listede çalışmak gibi). Bununla birlikte, burada, bir dizi işlemi basit bir değere indiriyoruz, bu yüzden istediğimiz katı bir katlama.
Gördüğünüz gibi, herhangi bir yarıgrupta (bir örneği çarpma altındaki olağan sayısal türlerden herhangi biri olan) kuyruk özyineleme modülünü tembel bir sağ kat veya katı bir sol kat için otomatik olarak optimize etmek mümkündür. Haskell.
Genelleme
İkili işlemin iki bağımsız değişkeninin, başlangıç değeri sonucunuzla aynı tür olduğu sürece aynı tür olması gerekmez. (Elbette argümanları her zaman yaptığınız katlama sırasına göre sola veya sağa çevirebilirsiniz.) Böylece güncellenmiş bir dosya almak veya başlangıç değeri ile başlayarak bir dosyaya tekrar tekrar yama ekleyebilirsiniz. 1.0, kayan nokta sonucu biriktirmek için tamsayılara bölün. Veya bir liste almak için öğeleri boş listeye ekleyin.
Başka bir genelleme türü, kıvrımları listelere değil, diğer Foldable
veri yapılarına uygulamaktır. Genellikle, değişmeyen doğrusal bağlantılı liste, belirli bir algoritma için istediğiniz veri yapısı değildir. Yukarıda bahsetmediğim bir konu, listenin önüne arkaya eleman eklemenin çok daha verimli olması ve işlem değişmeli olmadığında, işlemin x
soluna ve sağına uygulanmamasıdır . aynısı. Bu nedenle x
, sağda <>
ve solda uygulanabilecek bir algoritmayı temsil etmek için bir çift liste veya ikili ağaç gibi başka bir yapı kullanmanız gerekir .
İlişkilendirilebilir özelliğin, işlemleri bölme ve fethetme gibi diğer yararlı şekillerde yeniden gruplandırmanıza izin verdiğini unutmayın:
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)
Veya her bir iş parçacığının bir alt aralığı daha sonra diğerleriyle birleştirilen bir değere düşürdüğü otomatik paralellik.
if(n==0) return 0;
(sorunuzdaki gibi 1 döndürmez).x^0 = 1
, bu bir hata. Yine de sorunun geri kalanı için önemli olduğu için değil; yinelemeli asm ilk önce bu özel durumu kontrol eder. Ancak garip bir şekilde, yinelemeli uygulama1 * x
, birfloat
sürüm yapsak bile, kaynakta mevcut olmayanların çoğunu ortaya koyuyor . gcc.godbolt.org/z/eqwine (ve gcc yalnızca başarılı-ffast-math
.)