Apakah memiliki fasilitas bahasa generator seperti `yield` adalah ide yang bagus?

9

PHP, C #, Python dan kemungkinan beberapa bahasa lain memiliki yieldkata kunci yang digunakan untuk membuat fungsi generator.

Dalam PHP: http://php.net/manual/en/language.generators.syntax.php

Dalam Python: https://www.pythoncentral.io/python-generators-and-yield-keyword/

Dalam C #: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield

Saya khawatir bahwa sebagai fitur / fasilitas bahasa, ada yieldbeberapa konvensi. Salah satunya adalah apa yang saya sebut "kepastian". Ini adalah metode yang mengembalikan hasil yang berbeda setiap kali Anda menyebutnya. Dengan fungsi non-generator biasa Anda dapat memanggilnya dan jika diberi input yang sama, ia akan mengembalikan output yang sama. Dengan hasil, ia mengembalikan output yang berbeda, berdasarkan kondisi internal. Jadi jika Anda secara acak memanggil fungsi menghasilkan, tidak mengetahui keadaan sebelumnya, Anda tidak dapat mengharapkannya untuk mengembalikan hasil tertentu.

Bagaimana fungsi seperti ini cocok dengan paradigma bahasa? Apakah itu benar-benar melanggar konvensi? Apakah ide bagus untuk memiliki dan menggunakan fitur ini? (untuk memberikan contoh tentang apa yang baik dan apa yang buruk, gotodulunya fitur banyak bahasa dan masih, tetapi dianggap berbahaya dan karena itu diberantas dari beberapa bahasa, seperti Jawa). Apakah kompiler / juru bahasa bahasa pemrograman harus keluar dari konvensi apa pun untuk mengimplementasikan fitur seperti itu, misalnya, apakah bahasa harus mengimplementasikan multi-threading agar fitur ini berfungsi, atau dapatkah itu dilakukan tanpa teknologi threading?

Dennis
sumber
4
yieldpada dasarnya adalah mesin negara. Itu tidak dimaksudkan untuk mengembalikan hasil yang sama setiap kali. Apa yang akan dilakukannya dengan kepastian absolut adalah mengembalikan item berikutnya dalam jumlah setiap kali dipanggil. Thread tidak diperlukan; Anda perlu penutupan (lebih atau kurang), untuk mempertahankan kondisi saat ini.
Robert Harvey
1
Mengenai kualitas "kepastian," pertimbangkan bahwa, dengan urutan input yang sama, serangkaian panggilan ke iterator akan menghasilkan item yang persis sama dalam urutan yang persis sama.
Robert Harvey
4
Saya tidak yakin dari mana sebagian besar pertanyaan Anda berasal karena C ++ tidak memiliki yield kata kunci seperti halnya Python. Ini memiliki metode statis std::this_thread::yield(), tetapi itu bukan kata kunci. Jadi itu this_threadakan menambahkan hampir semua panggilan ke sana, membuatnya cukup jelas itu adalah fitur perpustakaan hanya untuk menghasilkan utas, bukan fitur bahasa tentang menghasilkan aliran kontrol secara umum.
Ixrec
tautan diperbarui ke C #, satu untuk C ++ dihapus
Dennis

Jawaban:

16

Peringatan pertama - C # adalah bahasa yang saya tahu paling baik, dan meskipun memiliki yieldyang tampaknya sangat mirip dengan bahasa lain yield, mungkin ada perbedaan halus yang saya tidak sadari.

Saya khawatir bahwa sebagai fitur / fasilitas bahasa, hasil melanggar beberapa konvensi. Salah satunya adalah apa yang saya sebut "kepastian". Ini adalah metode yang mengembalikan hasil yang berbeda setiap kali Anda menyebutnya.

Omong kosong. Apakah Anda benar-benar mengharapkan Random.Nextatau Console.ReadLine mengembalikan hasil yang sama setiap kali Anda memanggil mereka? Bagaimana dengan Panggilan Istirahat? Autentikasi? Dapatkan Barang dari koleksi? Ada segala macam fungsi (baik, berguna) yang tidak murni.

Bagaimana fungsi seperti ini cocok dengan paradigma bahasa? Apakah itu benar-benar melanggar konvensi?

Ya, yieldbermain sangat buruk dengan try/catch/finally, dan tidak diizinkan ( https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ for Info lebih lanjut).

Apakah ide bagus untuk memiliki dan menggunakan fitur ini?

Ini tentu ide yang baik untuk memiliki fitur ini. Hal - hal seperti LINQ C # benar - benar baik - mengevaluasi koleksi dengan malas memberikan manfaat kinerja yang besar, dan yieldmemungkinkan hal semacam itu dilakukan dalam sebagian kecil dari kode dengan sebagian kecil bug yang akan dilakukan oleh iterator linting tangan.

Yang mengatakan, tidak ada satu ton kegunaan untuk di yieldluar pemrosesan koleksi gaya LINQ. Saya telah menggunakannya untuk pemrosesan validasi, pembuatan jadwal, pengacakan, dan beberapa hal lainnya, tetapi saya berharap sebagian besar pengembang tidak pernah menggunakannya (atau menyalahgunakannya).

Apakah kompiler / juru bahasa bahasa pemrograman harus keluar dari konvensi apa pun untuk mengimplementasikan fitur seperti itu, misalnya, apakah bahasa harus mengimplementasikan multi-threading agar fitur ini berfungsi, atau dapatkah itu dilakukan tanpa teknologi threading?

Tidak persis. Compiler menghasilkan iterator mesin negara yang melacak di mana ia berhenti sehingga dapat mulai lagi di sana saat berikutnya disebut. Proses untuk pembuatan kode melakukan sesuatu yang mirip dengan Continuation Passing Style, di mana kode setelah yieldditarik ke dalam bloknya sendiri (dan jika ada yields, sub-blok lain, dan sebagainya). Itu adalah pendekatan yang dikenal lebih sering digunakan dalam Pemrograman Fungsional dan juga muncul dalam kompilasi async / menunggu C #.

Tidak diperlukan threading, tetapi memang membutuhkan pendekatan yang berbeda untuk pembuatan kode di sebagian besar kompiler, dan memang memiliki beberapa konflik dengan fitur bahasa lainnya.

Semua dalam semua, yieldadalah fitur dampak yang relatif rendah yang benar-benar membantu dengan subset masalah tertentu.

Telastyn
sumber
Saya tidak pernah menggunakan C # dengan serius tetapi yieldkata kunci ini mirip dengan coroutine, ya, atau sesuatu yang berbeda? Jika demikian saya berharap saya punya satu di C! Saya dapat memikirkan setidaknya beberapa bagian kode yang layak yang akan jauh lebih mudah untuk ditulis dengan fitur bahasa seperti itu.
2
@DrunkCoder - mirip, tetapi dengan beberapa keterbatasan, seperti yang saya mengerti.
Telastyn
1
Anda juga tidak ingin melihat hasil yang disalahgunakan. Semakin banyak fitur yang dimiliki bahasa, semakin besar kemungkinan Anda akan menemukan program yang ditulis dengan buruk dalam bahasa tersebut. Saya tidak yakin apakah pendekatan yang tepat untuk menulis bahasa yang dapat didekati adalah dengan melemparkan semuanya pada Anda dan melihat tongkat apa.
Neil
1
@DrunkCoder: ini adalah versi terbatas dari semi-coroutine. Sebenarnya, ini diperlakukan sebagai pola sintaksis oleh kompiler yang akan diperluas menjadi serangkaian pemanggilan metode, kelas, dan objek. (Pada dasarnya, kompiler menghasilkan objek kelanjutan yang menangkap konteks saat ini di bidang.) Implementasi default untuk koleksi adalah semi-coroutine, tetapi dengan membebani metode "ajaib" yang digunakan kompiler, Anda sebenarnya dapat menyesuaikan perilaku. Misalnya, sebelum async/ awaitditambahkan ke bahasa, seseorang menerapkannya menggunakan yield.
Jörg W Mittag
1
@Neil Secara umum dimungkinkan untuk menyalahgunakan hampir semua fitur bahasa pemrograman. Jika apa yang Anda katakan itu benar, maka akan jauh lebih sulit untuk memprogram dengan buruk menggunakan C daripada Python atau C #, tetapi ini tidak terjadi karena bahasa-bahasa tersebut memiliki banyak alat yang melindungi programmer dari banyak kesalahan yang sangat mudah untuk membuat dengan C. Pada kenyataannya, penyebab program yang buruk adalah programmer yang buruk - itu cukup masalah bahasa-agnostik.
Ben Cottrell
12

Apakah memiliki fasilitas bahasa generator seperti yieldide yang bagus?

Saya ingin menjawab ini dari perspektif Python dengan ya tegas , itu ide bagus .

Saya akan mulai dengan membahas beberapa pertanyaan dan asumsi dalam pertanyaan Anda terlebih dahulu, kemudian menunjukkan kegunaan generator dan kegunaannya yang tidak masuk akal di Python nanti.

Dengan fungsi non-generator biasa Anda dapat memanggilnya dan jika diberi input yang sama, itu akan mengembalikan output yang sama. Dengan hasil, ia mengembalikan output yang berbeda, berdasarkan kondisi internal.

Ini salah. Metode pada objek dapat dianggap sebagai fungsi itu sendiri, dengan keadaan internal mereka sendiri. Dalam Python, karena semuanya adalah objek, Anda sebenarnya bisa mendapatkan metode dari objek, dan meneruskan metode itu (yang terikat pada objek asalnya, jadi ia mengingat kondisinya).

Contoh lain termasuk fungsi acak sengaja serta metode input seperti jaringan, sistem file, dan terminal.

Bagaimana fungsi seperti ini cocok dengan paradigma bahasa?

Jika paradigma bahasa mendukung hal-hal seperti fungsi kelas satu, dan generator mendukung fitur bahasa lain seperti protokol Iterable, maka mereka cocok dengan mulus.

Apakah itu benar-benar melanggar konvensi?

Tidak. Karena dimasukkan ke dalam bahasa, konvensi dibangun dan mencakup (atau mengharuskan!) Penggunaan generator.

Apakah kompiler / juru bahasa bahasa pemrograman harus keluar dari konvensi apa pun untuk mengimplementasikan fitur tersebut

Seperti halnya fitur lain, kompiler hanya perlu dirancang untuk mendukung fitur tersebut. Dalam kasus Python, fungsi sudah objek dengan negara (seperti argumen default dan penjelasan fungsi).

apakah suatu bahasa harus mengimplementasikan multi-threading agar fitur ini berfungsi, atau dapatkah itu dilakukan tanpa teknologi threading?

Fakta menyenangkan: Implementasi Python default tidak mendukung threading sama sekali. Ini fitur Global Interpreter Lock (GIL), jadi tidak ada yang benar-benar berjalan bersamaan kecuali Anda sudah memutar proses kedua untuk menjalankan instance Python yang berbeda.


catatan: contoh dalam Python 3

Di luar Yield

Meskipun yieldkata kunci dapat digunakan dalam fungsi apa pun untuk mengubahnya menjadi generator, itu bukan satu-satunya cara untuk membuatnya. Python menampilkan Generator Expressions, cara yang ampuh untuk mengekspresikan generator dengan jelas dalam hal iterable lain (termasuk generator lain)

>>> pairs = ((x,y) for x in range(10) for y in range(10) if y >= x)
>>> pairs
<generator object <genexpr> at 0x0311DC90>
>>> sum(x*y for x,y in pairs)
1155

Seperti yang Anda lihat, tidak hanya sintaksnya yang bersih dan mudah dibaca, tetapi fungsi-fungsi sumbawaannya seperti menerima generator.

Dengan

Lihat Proposal Peningkatan Python untuk pernyataan With . Ini sangat berbeda dari yang Anda harapkan dari pernyataan With dalam bahasa lain. Dengan sedikit bantuan dari perpustakaan standar, generator Python bekerja dengan indah sebagai manajer konteks untuk mereka.

>>> from contextlib import contextmanager
>>> @contextmanager
def debugWith(arg):
        print("preprocessing", arg)
        yield arg
        print("postprocessing", arg)


>>> with debugWith("foobar") as s:
        print(s[::-1])


preprocessing foobar
raboof
postprocessing foobar

Tentu saja, mencetak sesuatu adalah hal paling membosankan yang dapat Anda lakukan di sini, tetapi hal itu menunjukkan hasil yang terlihat. Opsi yang lebih menarik termasuk pengelolaan sumber daya secara otomatis (membuka dan menutup file / stream / koneksi jaringan), mengunci konkurensi, membungkus sementara atau mengganti suatu fungsi, dan mendekompresi kemudian mengkompres ulang data. Jika fungsi panggilan seperti menyuntikkan kode ke dalam kode Anda, maka dengan pernyataan seperti membungkus bagian dari kode Anda dengan kode lain. Bagaimanapun Anda menggunakannya, ini adalah contoh kuat dari pengait yang mudah ke dalam struktur bahasa. Generator berbasis hasil bukan satu-satunya cara untuk membuat manajer konteks, tetapi mereka pasti yang nyaman.

Untuk dan Kelelahan Sebagian

Untuk loop di Python bekerja dengan cara yang menarik. Mereka memiliki format berikut:

for <name> in <iterable>:
    ...

Pertama, ekspresi yang saya panggil <iterable>dievaluasi untuk mendapatkan objek yang dapat diubah. Kedua, iterable telah __iter__memanggilnya, dan iterator yang dihasilkan disimpan di belakang layar. Selanjutnya, __next__dipanggil pada iterator untuk mendapatkan nilai untuk mengikat nama yang Anda masukkan <name>. Langkah ini berulang sampai panggilan untuk __next__melempar a StopIteration. Pengecualian ditelan oleh for loop, dan eksekusi berlanjut dari sana.

Kembali ke generator: ketika Anda memanggil __iter__generator, itu hanya mengembalikan sendiri.

>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272

Artinya, Anda dapat memisahkan iterasi atas sesuatu dari hal yang ingin Anda lakukan dengannya, dan mengubah perilaku itu di tengah jalan. Di bawah ini, perhatikan bagaimana generator yang sama digunakan dalam dua loop, dan pada yang kedua generator mulai mengeksekusi dari yang ditinggalkannya dari yang pertama.

>>> generator = (x for x in 'more boring stuff')
>>> for letter in generator:
        print(ord(letter))
        if letter > 'p':
                break


109
111
114
>>> for letter in generator:
        print(letter)


e

b
o
r
i
n
g

s
t
u
f
f

Evaluasi Malas

Salah satu kelemahan generator dibandingkan dengan daftar adalah satu-satunya hal yang dapat Anda akses dalam generator adalah hal berikutnya yang keluar darinya. Anda tidak dapat kembali dan untuk hasil sebelumnya, atau melompat ke depan untuk yang berikutnya tanpa melalui hasil antara. Sisi atas dari ini adalah generator dapat mengambil hampir tidak ada memori dibandingkan dengan daftar yang setara.

>>> import sys
>>> sys.getsizeof([x for x in range(10000)])
43816
>>> sys.getsizeof(range(10000000000))
24
>>> sys.getsizeof([x for x in range(10000000000)])
Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    sys.getsizeof([x for x in range(10000000000)])
  File "<pyshell#10>", line 1, in <listcomp>
    sys.getsizeof([x for x in range(10000000000)])
MemoryError

Generator juga dapat dirantai dengan malas.

logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))

Baris pertama, kedua, dan ketiga hanya mendefinisikan generator masing-masing, tetapi tidak melakukan pekerjaan nyata. Ketika baris terakhir dipanggil, jumlah meminta numericcolumn untuk suatu nilai, numericcolumn membutuhkan nilai dari lastcolumn, lastcolumn meminta nilai dari logfile, yang kemudian benar-benar membaca baris dari file. Tumpukan ini mengurai hingga jumlah mendapat bilangan bulat pertama. Kemudian, proses terjadi lagi untuk baris kedua. Pada titik ini, jumlah memiliki dua bilangan bulat, dan menambahkannya bersama. Perhatikan bahwa baris ketiga belum dibaca dari file. Sum kemudian melanjutkan meminta nilai dari numericcolumn (benar-benar tidak menyadari sisa rantai) dan menambahkannya, sampai numericcolumn habis.

Bagian yang sangat menarik di sini adalah bahwa garis-garisnya dibaca, dikonsumsi, dan dibuang secara individual. Pada titik tidak ada seluruh file dalam memori sekaligus. Apa yang terjadi jika file log ini, katakanlah, satu terabyte? Ini hanya berfungsi, karena hanya membaca satu baris pada satu waktu.

Kesimpulan

Ini bukan ulasan lengkap dari semua penggunaan generator di Python. Khususnya, saya melewatkan generator yang tidak terbatas, mesin negara, melewati nilai kembali, dan hubungan mereka dengan coroutine.

Saya percaya ini cukup untuk menunjukkan bahwa Anda dapat memiliki generator sebagai fitur bahasa yang terintegrasi dan bersih.

Joel Harmon
sumber
6

Jika Anda terbiasa dengan bahasa OOP klasik, generator dan yieldmungkin tampak menggelegar karena keadaan yang dapat diubah ditangkap pada tingkat fungsi daripada tingkat objek.

Pertanyaan tentang "kepastian" adalah herring merah. Biasanya disebut transparansi referensial , dan pada dasarnya berarti fungsi selalu mengembalikan hasil yang sama untuk argumen yang sama. Segera setelah Anda memiliki status yang bisa berubah, Anda kehilangan transparansi referensial. Dalam OOP, objek sering memiliki keadaan bisa berubah, yang berarti hasil pemanggilan metode tidak hanya bergantung pada argumen, tetapi juga keadaan internal objek.

Pertanyaannya adalah di mana menangkap keadaan yang bisa berubah. Dalam OOP klasik, keadaan bisa berubah ada di tingkat objek. Tetapi jika suatu bahasa mendukung penutupan, Anda mungkin memiliki status yang bisa berubah pada tingkat fungsi. Misalnya dalam JavaScript:

function getCounter() {
   var cnt = 1;
   return function(){ return cnt++; }
}
var counter = getCounter();
counter() --> 1
counter() --> 2

Singkatnya, yieldadalah alami dalam bahasa yang mendukung penutupan, tetapi akan keluar dari tempatnya dalam bahasa seperti versi Jawa yang lebih lama di mana keadaan yang bisa berubah hanya ada di tingkat objek.

JacquesB
sumber
Saya kira jika fitur bahasa memiliki spektrum, hasil akan sejauh mungkin dari fungsional. Itu belum tentu hal yang buruk. OOP dulunya sangat modis, dan sekali lagi pemrograman fungsional. Saya kira bahayanya sama dengan mencampurkan dan mencocokkan fitur seperti hasil dengan desain fungsional yang membuat program Anda berperilaku dengan cara yang tidak terduga.
Neil
0

Menurut pendapat saya, ini bukan fitur yang baik. Ini adalah fitur yang buruk, terutama karena itu perlu diajarkan dengan sangat hati-hati, dan semua orang mengajarkannya dengan salah. Orang-orang menggunakan kata "generator," menyamakan antara fungsi generator dan objek generator. Pertanyaannya adalah: hanya siapa atau apa yang menghasilkan yang sebenarnya?

Ini bukan semata pendapat saya. Bahkan Guido, dalam buletin PEP di mana dia mengatur hal ini, mengakui bahwa fungsi generator bukanlah generator tetapi "pabrik generator."

Itu agak penting, bukan begitu? Tetapi membaca 99% dokumentasi di luar sana, Anda akan mendapatkan kesan bahwa fungsi generator adalah generator yang sebenarnya, dan mereka cenderung mengabaikan fakta bahwa Anda juga membutuhkan objek generator.

Guido mempertimbangkan untuk mengganti "def" untuk "gen" untuk fungsi-fungsi ini dan berkata Tidak. Tapi saya berpendapat itu tidak akan cukup. Itu harus benar-benar:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
pengguna320927
sumber