Mengapa evaluator optimal λ-kalkulus dapat menghitung eksponen modular yang besar tanpa rumus?


137

Nomor gereja adalah pengkodean bilangan asli sebagai fungsi.

(\ f x → (f x))             -- church number 1
(\ f x → (f (f (f x))))     -- church number 3
(\ f x → (f (f (f (f x))))) -- church number 4

Dengan rapi, Anda dapat mengeksponensial 2 nomor gereja hanya dengan menerapkannya. Artinya, jika Anda menerapkan 4 hingga 2, Anda mendapatkan nomor gereja 16, atau 2^4. Jelas sekali, itu sama sekali tidak praktis. Nomor gereja membutuhkan jumlah memori linier dan sangat, sangat lambat. Menghitung sesuatu seperti 10^10- yang GHCI dengan cepat menjawab dengan benar - akan memakan waktu lama dan toh tidak bisa memuat memori di komputer Anda.

Saya telah bereksperimen dengan evaluator λ yang optimal belakangan ini. Pada pengujian saya, saya tidak sengaja mengetik yang berikut ini pada kalkulator λ optimal saya:

10 ^ 10 % 13

Itu seharusnya perkalian, bukan eksponensiasi. Sebelum saya bisa menggerakkan jari saya untuk membatalkan program yang berjalan selamanya dengan putus asa, itu menjawab permintaan saya:

3
{ iterations: 11523, applications: 5748, used_memory: 27729 }

real    0m0.104s
user    0m0.086s
sys     0m0.019s

Dengan "peringatan bug" saya berkedip, saya membuka Google dan memverifikasi 10^10%13 == 3. Tapi kalkulator λ tidak seharusnya menemukan hasil itu, ia hampir tidak bisa menyimpan 10 ^ 10. Saya mulai menekankannya, untuk sains. Ini langsung menjawab aku 20^20%13 == 3, 50^50%13 == 4, 60^60%3 == 0. Saya harus menggunakan alat eksternal untuk memverifikasi hasil tersebut, karena Haskell sendiri tidak dapat menghitungnya (karena overflow integer) (jika Anda menggunakan Integer bukan Ints, tentu saja!). Mendorongnya hingga batasnya, ini adalah jawaban untuk 200^200%31:

5
{ iterations: 10351327, applications: 5175644, used_memory: 23754870 }

real    0m4.025s
user    0m3.686s
sys 0m0.341s

Jika kita memiliki satu salinan alam semesta untuk setiap atom di alam semesta, dan kita memiliki komputer untuk setiap atom yang kita miliki secara total, kita tidak dapat menyimpan nomor gereja 200^200. Ini mendorong saya untuk mempertanyakan apakah Mac saya benar-benar sekuat itu. Mungkin evaluator optimal mampu melewati cabang yang tidak perlu dan sampai pada jawaban dengan cara yang sama seperti yang dilakukan Haskell dengan evaluasi malas. Untuk mengujinya, saya menyusun program λ ke Haskell:

data Term = F !(Term -> Term) | N !Double
instance Show Term where {
    show (N x) = "(N "++(if fromIntegral (floor x) == x then show (floor x) else show x)++")";
    show (F _) = "(λ...)"}
infixl 0 #
(F f) # x = f x
churchNum = F(\(N n)->F(\f->F(\x->if n<=0 then x else (f#(churchNum#(N(n-1))#f#x)))))
expMod    = (F(\v0->(F(\v1->(F(\v2->((((((churchNum # v2) # (F(\v3->(F(\v4->(v3 # (F(\v5->((v4 # (F(\v6->(F(\v7->(v6 # ((v5 # v6) # v7))))))) # v5))))))))) # (F(\v3->(v3 # (F(\v4->(F(\v5->v5)))))))) # (F(\v3->((((churchNum # v1) # (churchNum # v0)) # ((((churchNum # v2) # (F(\v4->(F(\v5->(F(\v6->(v4 # (F(\v7->((v5 # v7) # v6))))))))))) # (F(\v4->v4))) # (F(\v4->(F(\v5->(v5 # v4))))))) # ((((churchNum # v2) # (F(\v4->(F(\v5->v4))))) # (F(\v4->v4))) # (F(\v4->v4))))))) # (F(\v3->(((F(\(N x)->F(\(N y)->N(x+y)))) # v3) # (N 1))))) # (N 0))))))))
main = print $ (expMod # N 5 # N 5 # N 4)

Ini dengan benar mengeluarkan 1( 5 ^ 5 % 4) - tetapi membuang apa pun di atas 10^10dan itu akan macet, menghilangkan hipotesis.

The evaluator optimal saya menggunakan adalah 160-garis panjang, program JavaScript unoptimized yang tidak termasuk apapun eksponensial modulus matematika - dan fungsi lambda-kalkulus modulus saya yang digunakan adalah sama sederhana:

(λab.(b(λcd.(c(λe.(d(λfg.(f(efg)))e))))(λc.(c(λde.e)))(λc.(a(b(λdef.(d(λg.(egf))))(λd.d)(λde.(ed)))(b(λde.d)(λd.d)(λd.d))))))

Saya tidak menggunakan algoritma atau rumus aritmatika modular tertentu. Jadi, bagaimana evaluator yang optimal bisa sampai pada jawaban yang benar?


2
Dapatkah Anda memberi tahu kami lebih banyak tentang jenis evaluasi optimal yang Anda gunakan? Mungkin kutipan kertas? Terima kasih!
Jason Dagit

11
Saya menggunakan algoritme abstrak Lamping, seperti yang dijelaskan di buku The Optimal Implementation of Functional Programming Languages . Perhatikan bahwa saya tidak menggunakan "oracle" (tanpa croissant / brackets) karena istilah tersebut dapat diketik EAL. Juga, alih-alih mengurangi penggemar secara paralel secara acak, saya secara berurutan melintasi grafik untuk tidak mengurangi simpul yang tidak dapat dijangkau, tapi saya khawatir ini bukan pada literatur AFAIK ...
MaiaVictor

7
Oke, jika ada yang penasaran, saya telah menyiapkan repositori GitHub dengan kode sumber untuk evaluator optimal saya. Ini memiliki banyak komentar dan Anda dapat mengujinya berjalan node test.js. Beri tahu saya jika Anda memiliki pertanyaan.
MaiaVictor

1
Temukan rapi! Saya tidak cukup tahu tentang evaluasi yang optimal, tetapi saya dapat mengatakan bahwa ini mengingatkan saya pada Teorema Kecil Fermat / Teorema Euler. Jika Anda tidak menyadarinya, ini mungkin titik awal yang baik.
luqui

5
Ini adalah pertama kalinya di mana saya tidak memiliki petunjuk sedikit pun tentang apa pertanyaan itu, tetapi tetap menyukai pertanyaan itu, dan terutama, jawaban pertama yang luar biasa.
Marco13

Jawaban:


125

Fenomena ini berasal dari jumlah langkah pengurangan beta bersama, yang bisa sangat berbeda dalam evaluasi malas gaya Haskell (atau nilai panggilan-demi-biasa, yang tidak terlalu jauh dalam hal ini) dan di Vuillemin-Lévy-Lamping- Kathail-Asperti-Guerrini- (dkk…) evaluasi "optimal". Ini adalah fitur umum, yang sepenuhnya tidak bergantung pada rumus aritmatika yang dapat Anda gunakan dalam contoh khusus ini.

Berbagi berarti memiliki representasi istilah lambda Anda di mana satu "node" dapat menggambarkan beberapa bagian serupa dari istilah lambda sebenarnya yang Anda wakili. Misalnya, Anda dapat mewakili istilah tersebut

\x. x ((\y.y)a) ((\y.y)a)

menggunakan grafik (asiklik terarah) di mana hanya ada satu kemunculan mewakili subgraf (\y.y)a, dan dua sisi menargetkan subgraf itu. Dalam istilah Haskell, Anda memiliki satu pemikiran, yang Anda evaluasi hanya sekali, dan dua petunjuk untuk pemikiran ini.

Memo bergaya Haskell mengimplementasikan berbagi subterms lengkap. Tingkat berbagi ini dapat diwakili oleh grafik asiklik terarah. Pembagian optimal tidak memiliki batasan ini: ia juga dapat membagikan subterms "parsial", yang mungkin menyiratkan siklus dalam representasi grafik.

Untuk melihat perbedaan antara dua tingkat berbagi ini, pertimbangkan istilahnya

\x. (\z.z) ((\z.z) x)

Jika pembagian Anda dibatasi untuk menyelesaikan subterms seperti yang terjadi di Haskell, Anda mungkin hanya memiliki satu kemunculan \z.z, tetapi dua beta-redex di sini akan berbeda: satu adalah (\z.z) xdan yang lainnya adalah (\z.z) ((\z.z) x), dan karena keduanya bukan istilah yang sama mereka tidak dapat dibagikan. Jika berbagi subterms parsial diperbolehkan, maka menjadi mungkin untuk berbagi istilah parsial (\z.z) [](bukan hanya fungsi \z.z, tapi "fungsi yang \z.zditerapkan pada sesuatu ), yang mengevaluasi dalam satu langkah ke hanya sesuatu , apapun argumen ini. Oleh karena itu. Anda dapat memiliki grafik di mana hanya satu node yang mewakili dua aplikasi\z.zmenjadi dua argumen berbeda, dan di mana kedua aplikasi ini dapat dikurangi hanya dalam satu langkah. Perhatikan bahwa ada siklus pada node ini, karena argumen "kemunculan pertama" tepatnya adalah "kemunculan kedua". Akhirnya, dengan pembagian optimal Anda dapat beralih dari (grafik mewakili) \x. (\z.z) ((\z.z) x))ke (grafik mewakili) hasil \x.xhanya dalam satu langkah pengurangan beta (ditambah beberapa pembukuan). Ini pada dasarnya apa yang terjadi pada evaluator optimal Anda (dan representasi grafik juga mencegah ledakan ruang angkasa).

Untuk penjelasan yang sedikit lebih panjang, Anda dapat melihat makalah Optimalitas Lemah, dan Arti Berbagi (yang menarik bagi Anda adalah pendahuluan dan bagian 4.1, dan mungkin beberapa petunjuk bibliografi di bagian akhir).

Kembali ke contoh Anda, pengkodean fungsi aritmatika yang bekerja pada bilangan bulat Gereja adalah salah satu tambang "terkenal" contoh di mana penilai optimal dapat bekerja lebih baik daripada bahasa arus utama (dalam kalimat ini, terkenal sebenarnya berarti segelintir spesialis mengetahui contoh ini). Untuk lebih banyak contoh seperti itu, lihat kertas Safe Operator: Brackets Closed Forever oleh Asperti dan Chroboczek (dan omong-omong, Anda akan menemukan istilah lambda yang menarik yang tidak dapat diketikkan EAL; jadi saya mendorong Anda untuk mengambil melihat oracle, dimulai dengan makalah Asperti / Chroboczek ini).

Seperti yang Anda katakan sendiri, pengkodean semacam ini sama sekali tidak praktis, tetapi mereka masih menunjukkan cara yang baik untuk memahami apa yang sedang terjadi. Dan izinkan saya menyimpulkan dengan tantangan untuk penyelidikan lebih lanjut: akankah Anda dapat menemukan contoh di mana evaluasi optimal tentang pengkodean yang seharusnya buruk ini sebenarnya setara dengan evaluasi tradisional pada representasi data yang wajar? (sejauh yang saya tahu ini adalah pertanyaan terbuka yang nyata).


35
Itu posting pertama yang sangat teliti. Selamat datang di StackOverflow!
dfeuer

2
Tidak kurang dari berwawasan. Terima kasih, dan selamat datang di komunitas!
MaiaVictor

7

Ini bukan jawaban tetapi ini adalah saran dari mana Anda dapat mulai mencari.

Ada cara yang mudah untuk menghitung eksponen modular dalam ruang kecil, khususnya dengan menulis ulang

(a * x ^ y) % z

sebagai

(((a * x) % z) * x ^ (y - 1)) % z

Jika seorang evaluator mengevaluasi seperti ini dan menyimpan parameter yang terakumulasi adalam bentuk normal maka Anda tidak akan menggunakan terlalu banyak ruang. Jika memang penilai Anda sudah optimal maka mungkin ia tidak boleh melakukan pekerjaan lebih dari yang ini, jadi secara khusus tidak dapat menggunakan lebih banyak ruang daripada waktu yang dibutuhkan untuk mengevaluasi.

Saya tidak begitu yakin apa sebenarnya evaluator yang optimal, jadi saya khawatir saya tidak bisa membuat ini lebih ketat.


4
@Viclib Fibonacci seperti yang dikatakan @Tom adalah contoh yang bagus. fibmembutuhkan waktu eksponensial dengan cara yang naif, yang dapat direduksi menjadi linier dengan memoization sederhana / pemrograman dinamis. Bahkan waktu logaritmik (!) Dimungkinkan melalui penghitungan pangkat matriks ke-n [[0,1],[1,1]](selama Anda menghitung setiap perkalian memiliki biaya konstan).
chi

1
Bahkan waktu yang konstan jika Anda cukup berani untuk memperkirakan :)
J. Abrahamson

5
@ TomEllis Mengapa sesuatu yang hanya tahu bagaimana mengurangi ekspresi kalkulus lambda sewenang-wenang tahu itu (a * b) % n = ((a % n) * b) % n? Itu pasti bagian yang misterius.
Reid Barton

2
@ReidBarton pasti saya mencobanya! Hasil yang sama.
MaiaVictor

2
@TomEllis dan Chi, Hanya ada komentar kecil. Itu semua mengasumsikan bahwa fungsi rekursif tradisional adalah implementasi fib yang "naif", tetapi IMO ada cara alternatif untuk mengungkapkannya yang jauh lebih alami. Bentuk normal dari representasi baru itu memiliki setengah dari ukuran yang tradisional), dan Optlam berhasil menghitungnya secara linier! Jadi saya akan berpendapat bahwa itu adalah definisi "naif" dari fib sejauh λ-kalkulus diperhatikan. Saya akan membuat posting blog tapi saya tidak yakin itu benar-benar layak ...
MaiaVictor
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.