Apakah metode ini murni?

9

Saya memiliki metode ekstensi berikut:

    public static IEnumerable<T> Apply<T>(
        [NotNull] this IEnumerable<T> source,
        [NotNull] Action<T> action)
        where T : class
    {
        source.CheckArgumentNull("source");
        action.CheckArgumentNull("action");
        return source.ApplyIterator(action);
    }

    private static IEnumerable<T> ApplyIterator<T>(this IEnumerable<T> source, Action<T> action)
        where T : class
    {
        foreach (var item in source)
        {
            action(item);
            yield return item;
        }
    }

Itu hanya berlaku tindakan untuk setiap item dari urutan sebelum mengembalikannya.

Saya bertanya-tanya apakah saya harus menerapkan Pureatribut (dari anotasi Resharper) ke metode ini, dan saya bisa melihat argumen yang mendukung dan menentangnya.

Pro:

  • tegasnya, itu adalah murni; hanya menyebutnya pada urutan tidak mengubah urutan (mengembalikan urutan baru) atau membuat perubahan keadaan yang dapat diamati
  • memanggilnya tanpa menggunakan hasilnya jelas merupakan kesalahan, karena itu tidak berpengaruh kecuali urutannya disebutkan, jadi saya ingin Resharper memperingatkan saya jika saya melakukannya.

Cons:

  • meskipun Applymetode itu sendiri murni, menyebutkan urutan yang dihasilkan akan membuat perubahan keadaan yang dapat diamati (yang merupakan titik metode). Misalnya, items.Apply(i => i.Count++)akan mengubah nilai item setiap kali itu disebutkan. Jadi menerapkan atribut Pure mungkin menyesatkan ...

Bagaimana menurut anda? Haruskah saya menerapkan atribut atau tidak?

Thomas Levesque
sumber

Jawaban:

15

Tidak itu tidak murni, karena memiliki efek samping. Konkret itu memanggil actionsetiap item. Juga, ini bukan threadsafe.

Sifat utama dari fungsi murni adalah dapat dipanggil beberapa kali dan tidak pernah melakukan hal lain selain mengembalikan nilai yang sama. Itu bukan kasusmu. Juga, menjadi murni berarti Anda tidak menggunakan apa pun selain parameter input. Ini berarti dapat dipanggil dari utas kapan saja dan tidak menyebabkan perilaku yang tidak terduga. Sekali lagi, itu bukan kasus fungsi Anda.

Juga, Anda mungkin salah pada satu hal: kemurnian fungsi bukan masalah pro atau kontra. Bahkan satu keraguan, bahwa itu dapat memiliki efek samping, sudah cukup untuk membuatnya tidak murni.

Eric Lippert memunculkan poin yang bagus. Saya akan menggunakan http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx sebagai bagian dari argumen balasan saya. Terutama garis

Metode murni diizinkan untuk memodifikasi objek yang telah dibuat setelah masuk ke metode murni.

Katakanlah kita membuat metode seperti ini:

int Count<T>(IEnumerable<T> e)
{
    var enumerator = e.GetEnumerator();
    int count = 0;
    while (enumerator.MoveNext()) count ++;
    return count;
}

Pertama, ini menganggap itu GetEnumeratorjuga murni (saya tidak dapat menemukan sumbernya). Jika ya, maka menurut aturan di atas, kita dapat menjelaskan metode ini dengan [Murni], karena hanya memodifikasi contoh yang dibuat di dalam tubuh itu sendiri. Setelah itu kita dapat menyusun ini dan ApplyIterator, yang seharusnya menghasilkan fungsi murni, kan?

Count(ApplyIterator(source, action));

Tidak Komposisi ini tidak murni, bahkan ketika kedua Countdan ApplyIteratormurni. Tapi saya mungkin akan membangun argumen ini pada premis yang salah. Saya berpikir bahwa gagasan yang dibuat dalam metode ini dikecualikan dari aturan kemurnian salah atau setidaknya tidak cukup spesifik.

Euforia
sumber
1
Kemurnian fungsi +1 bukan masalah pro atau kontra. Kemurnian fungsi adalah petunjuk tentang penggunaan dan keamanan. Anehnya OP yang dimasukkan where T : class, tetapi jika OP sederhananya where T : strutakan menjadi murni.
ArTs
4
Saya tidak setuju dengan jawaban ini. Panggilan sequence.Apply(action)tidak memiliki efek samping; jika ya, nyatakan efek samping yang dimilikinya. Sekarang, panggilan sequence.Apply(action).GetEnumerator().MoveNext()memiliki efek samping, tetapi kita sudah tahu itu; itu bermutasi pencacah! Mengapa harus sequence.Apply(action)dianggap tidak murni karena panggilan MoveNextitu tidak murni, tetapi sequence.Where(predicate)dianggap murni? sequence.Where(predicate).GetEnumerator().MoveNext()setiap bit tidak murni.
Eric Lippert
@EricLippert Anda meningkatkan poin yang bagus. Tapi, bukankah cukup menelepon GetEnumerator saja? Bisakah kita menganggap itu Murni?
Euforia
@ Euphoric: Apa efek samping yang dapat diamati yang memanggil GetEnumeratorproduk, selain mengalokasikan enumerator dalam kondisi awal?
Eric Lippert
1
@ EricLippert Lalu mengapa Enumerable.Count dianggap Murni oleh Kontrak Kode .NET? Saya tidak memiliki tautan, tetapi ketika saya bermain dengannya di studio visual, saya mendapat peringatan ketika saya menggunakan hitungan non-murni khusus, tetapi kontrak berfungsi dengan baik dengan Enumerable.Count.
Euforia
18

Saya tidak setuju dengan jawaban Euforia dan Robert Harvey . Tentu saja itu adalah fungsi murni; masalahnya adalah

Itu hanya berlaku tindakan untuk setiap item dari urutan sebelum mengembalikannya.

sangat tidak jelas apa arti "itu" yang pertama. Jika "itu" berarti salah satu dari fungsi itu, maka itu tidak benar; tak satu pun dari fungsi-fungsi itu melakukan itu; yang MoveNextdari pencacah urutan melakukan itu, dan "kembali" item melalui Currentproperti, tidak dengan kembali.

Urutan-urutan itu disebutkan dengan malas , tidak bersemangat sehingga tentu saja bukan kasus bahwa tindakan diterapkan sebelum urutan dikembalikan oleh Apply. Tindakan diterapkan setelah urutan dikembalikan, jika MoveNextdipanggil pada enumerator.

Seperti yang Anda perhatikan, fungsi-fungsi ini mengambil tindakan dan urutan dan mengembalikan urutan; output tergantung pada input, dan tidak ada efek samping yang dihasilkan, jadi ini adalah fungsi murni ..

Sekarang, jika Anda membuat enumerator dari urutan yang dihasilkan dan kemudian memanggil MoveNext pada iterator itu maka metode MoveNext tidak murni, karena memanggil tindakan dan menghasilkan efek samping. Tapi kita sudah tahu bahwa MoveNext tidak murni karena itu memecah enumerator!

Sekarang, untuk pertanyaan Anda, sebaiknya Anda menerapkan atribut: Saya tidak akan menerapkan atribut karena saya tidak akan menulis metode ini di tempat pertama . Jika saya ingin menerapkan tindakan ke urutan maka saya menulis

foreach(var item in sequence) action(item);

yang jelas baik.

Eric Lippert
sumber
2
Saya kira metode ini jatuh dalam kantong yang sama dengan ForEachmetode ekstensi, yang sengaja bukan bagian dari Linq karena tujuannya adalah untuk menghasilkan efek samping ...
Thomas Levesque
1
@ThomasLevesque: Saran saya adalah jangan pernah melakukan itu . Kueri harus menjawab pertanyaan , bukan mengubah urutan ; itu sebabnya mereka disebut kueri . Memutasi urutan seperti yang ditanyakan sangat berbahaya . Pertimbangkan misalnya apa yang terjadi jika permintaan seperti itu kemudian mengalami beberapa panggilan Any()dari waktu ke waktu; tindakan akan dilakukan berulang kali, tetapi hanya pada item pertama! Urutan harus merupakan urutan nilai ; jika Anda menginginkan urutan tindakan maka buatlah IEnumerable<Action>.
Eric Lippert
2
Jawaban ini mengeruhkan air lebih dari yang diterangi. Sementara semua yang Anda katakan tidak diragukan lagi benar, prinsip-prinsip kekekalan dan kemurnian adalah prinsip-prinsip bahasa pemrograman tingkat tinggi, bukan detail implementasi tingkat rendah. Programmer yang bekerja pada level fungsional tertarik pada bagaimana kode mereka berperilaku pada level fungsional, bukan apakah pekerjaan dalamnya murni atau tidak . Mereka hampir pasti tidak murni di bawah tenda jika Anda cukup rendah. Kita semua umumnya menjalankan hal-hal ini pada arsitektur Von Neumann, yang tentunya tidak murni.
Robert Harvey
2
@ThomasEding: Metode ini tidak memanggil action, jadi kemurnian actiontidak relevan. Aku tahu itu terlihat seperti itu panggilan action, tetapi metode ini adalah sintaksis gula untuk dua metode, salah satu yang mengembalikan enumerator, dan salah satu yang merupakan MoveNextpencacah. Yang pertama jelas murni, dan yang kedua jelas tidak. Lihatlah seperti ini: apakah Anda akan mengatakan itu IEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }murni? Karena itulah fungsi sebenarnya ini.
Eric Lippert
1
@ThomasEding: Anda melewatkan sesuatu; bukan itu cara iterators bekerja. The ApplyIteratormethod mengembalikan segera . Tidak ada kode di tubuh ApplyIteratordijalankan sampai panggilan pertama ke MoveNextpada enumerator objek yang dikembalikan. Sekarang setelah Anda tahu itu, Anda dapat menyimpulkan jawaban untuk teka-teki ini: blogs.msdn.com/b/ericlippert/archive/2007/09/05/... Jawabannya ada di sini: blogs.msdn.com/b/ericlippert/archive / 2007/09/06 / ...
Eric Lippert
3

Itu bukan fungsi murni, jadi menerapkan atribut Murni menyesatkan.

Fungsi murni tidak mengubah koleksi asli, dan tidak masalah apakah Anda melewati tindakan yang tidak berpengaruh atau tidak; itu masih merupakan fungsi yang tidak murni karena tujuannya adalah untuk menyebabkan efek samping.

Jika Anda ingin menjadikan fungsi ini murni, salin koleksi ke koleksi baru, terapkan perubahan yang diambil Action ke koleksi baru, dan kembalikan koleksi baru, biarkan koleksi asli tidak berubah.

Robert Harvey
sumber
Yah, itu tidak mengubah koleksi asli, karena hanya mengembalikan urutan baru dengan item yang sama; inilah mengapa saya mempertimbangkan untuk menjadikannya murni. Tapi itu mungkin mengubah keadaan item ketika Anda menyebutkan hasilnya.
Thomas Levesque
Jika itemadalah tipe referensi, itu memodifikasi koleksi asli, meskipun Anda kembali itemdalam iterator. Lihat stackoverflow.com/questions/1538301
Robert Harvey
1
Bahkan jika dia menyalin dalam-dalam koleksi itu masih tidak akan murni, karena actionmungkin memiliki efek samping selain memodifikasi item yang diteruskan ke sana.
Idan Arye
@IdanArye: Benar, Aksi juga harus murni.
Robert Harvey
1
@IdanArye: ()=>{}dapat dikonversi ke Action, dan ini adalah fungsi murni. Keluarannya semata-mata bergantung pada inputnya dan tidak memiliki efek samping yang dapat diamati.
Eric Lippert
0

Menurut pendapat saya, fakta bahwa ia menerima Tindakan (dan bukan sesuatu seperti PureAction) membuatnya tidak murni.

Dan saya bahkan tidak setuju dengan Eric Lippert. Dia menulis ini "() => {} dapat dikonversi ke Aksi, dan ini adalah fungsi murni. Keluarannya hanya bergantung pada inputnya dan tidak memiliki efek samping yang dapat diamati".

Nah, bayangkan bahwa alih-alih menggunakan delegasi, ApplyIterator menggunakan metode bernama Action.

Jika Action murni, maka ApplyIterator juga murni. Jika Action tidak murni, maka ApplyIterator tidak boleh murni.

Mengingat jenis delegasi (bukan nilai yang diberikan sebenarnya), kami tidak memiliki jaminan bahwa itu akan murni, sehingga metode tersebut akan berperilaku sebagai metode murni hanya ketika delegasi itu murni. Jadi, untuk membuatnya benar-benar murni, ia harus menerima delegasi murni (dan yang ada, kita dapat mendeklarasikan delegasi sebagai [Murni], sehingga kita dapat memiliki PureAction).

Menjelaskannya secara berbeda, metode Murni harus selalu memberikan hasil yang sama dengan input yang sama dan tidak menghasilkan perubahan yang dapat diamati. ApplyIterator dapat diberikan sumber dan delegasi yang sama dua kali tetapi, jika delegasi mengubah tipe referensi, eksekusi selanjutnya akan memberikan hasil yang berbeda. Contoh: Delegasi melakukan sesuatu seperti item.Content + = "Berubah";

Jadi, menggunakan ApplyIterator di atas daftar "wadah string" (objek dengan properti Konten dari string tipe), kita mungkin memiliki nilai-nilai asli ini:

Test

Test2

Setelah eksekusi pertama, daftar akan memiliki ini:

Test Changed

Test2 Changed

Dan ini yang ketiga kalinya:

Test Changed Changed

Test2 Changed Changed

Jadi, kami mengubah isi daftar karena delegasi tidak murni dan tidak ada optimasi dapat dilakukan untuk menghindari mengeksekusi panggilan 3 kali jika dipanggil 3 kali, karena setiap eksekusi akan menghasilkan hasil yang berbeda.

Paulo Zemek
sumber