beralih / ide pencocokan pola

151

Saya telah melihat F # baru-baru ini, dan sementara saya tidak mungkin untuk melompat pagar dalam waktu dekat, itu pasti menyoroti beberapa area di mana C # (atau dukungan perpustakaan) dapat membuat hidup lebih mudah.

Secara khusus, saya sedang berpikir tentang kemampuan pencocokan pola F #, yang memungkinkan sintaks yang sangat kaya - jauh lebih ekspresif daripada saklar saat ini / yang setara dengan C #. Saya tidak akan mencoba memberikan contoh langsung (F # saya tidak sesuai), tetapi singkatnya itu memungkinkan:

  • cocok dengan jenis (dengan pemeriksaan cakupan penuh untuk serikat yang didiskriminasi) [perhatikan ini juga menyimpulkan jenis untuk variabel terikat, memberikan akses anggota dll]
  • cocok dengan predikat
  • kombinasi di atas (dan mungkin beberapa skenario lain yang tidak saya sadari)

Meskipun akan menyenangkan bagi C # untuk akhirnya meminjam [ahem] sebagian dari kekayaan ini, untuk sementara saya telah melihat apa yang dapat dilakukan saat runtime - misalnya, cukup mudah untuk menyatukan beberapa objek untuk memungkinkan:

var getRentPrice = new Switch<Vehicle, int>()
        .Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) // "bike" here is typed as Motorcycle
        .Case<Bicycle>(30) // returns a constant
        .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
        .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
        .ElseThrow(); // or could use a Default(...) terminator

di mana getRentPrice adalah Func <Vehicle, int>.

[note - mungkin Switch / Kasing di sini adalah istilah yang salah ... tapi itu menunjukkan ide]

Bagi saya, ini jauh lebih jelas daripada yang setara menggunakan mengulangi if / else, atau conditional terner komposit (yang menjadi sangat berantakan untuk ekspresi non-sepele - kurung berlimpah). Ini juga menghindari banyak casting, dan memungkinkan untuk ekstensi sederhana (baik secara langsung atau melalui metode ekstensi) untuk pertandingan yang lebih spesifik, misalnya pertandingan InRange (...) yang sebanding dengan VB Select ... Case "x To y "penggunaan.

Saya hanya mencoba untuk mengukur apakah orang berpikir ada banyak manfaat dari konstruksi seperti di atas (tanpa adanya dukungan bahasa)?

Perhatikan juga bahwa saya telah bermain dengan 3 varian di atas:

  • versi Fungsi <TSource, TValue> untuk evaluasi - sebanding dengan pernyataan kondisional ternary komposit
  • versi Action <TSource> - dapat dibandingkan dengan if / else if / else if / else if / else
  • versi Expression <Func <TSource, TValue >> - sebagai yang pertama, tetapi dapat digunakan oleh penyedia LINQ yang sewenang-wenang

Selain itu, menggunakan versi berbasis-Ekspresi memungkinkan penulisan ulang Expression-tree, pada dasarnya inline semua cabang menjadi Ekspresi bersyarat komposit tunggal, daripada menggunakan doa berulang-ulang. Saya belum memeriksanya baru-baru ini, tetapi dalam beberapa pengembangan Entitas Framework awal saya ingat ini diperlukan, karena tidak terlalu menyukai InvocationExpression. Ini juga memungkinkan penggunaan yang lebih efisien dengan LINQ-to-Objects, karena ia menghindari pemanggilan delegasi berulang - tes menunjukkan kecocokan seperti di atas (menggunakan formulir Ekspresi) yang bekerja pada kecepatan yang sama [sedikit lebih cepat, pada kenyataannya] dibandingkan dengan C # yang setara pernyataan bersyarat komposit. Untuk kelengkapan, versi berbasis Func <...> membutuhkan waktu 4 kali lebih lama daripada pernyataan kondisional C #, tetapi masih sangat cepat dan tidak mungkin menjadi hambatan utama dalam sebagian besar kasus penggunaan.

Saya menyambut setiap pemikiran / masukan / kritik / dll di atas (atau tentang kemungkinan dukungan bahasa C # lebih kaya ... di sini berharap ;-p).

Marc Gravell
sumber
"Saya hanya mencoba mengukur apakah orang berpikir ada banyak manfaat dari konstruksi seperti di atas (dengan tidak adanya dukungan bahasa)?" IMHO, ya. Bukankah sesuatu yang serupa sudah ada? Jika tidak, merasa terdorong untuk menulis perpustakaan yang ringan.
Konrad Rudolph
10
Anda bisa menggunakan VB .NET yang mendukung ini dalam pernyataan kasus pilihnya. Eek!
Jim Burger
Saya juga akan membunyikan klakson saya sendiri dan menambahkan tautan ke perpustakaan saya: functional-dotnet
Alexey Romanov
1
Saya suka ide ini dan itu membuat bentuk switch-case yang sangat bagus dan lebih fleksibel; Namun, bukankah ini benar-benar cara yang dibumbui untuk menggunakan sintaks seperti Linq sebagai pembungkus if-then? Saya akan mencegah seseorang untuk menggunakan ini di tempat yang sebenarnya, yaitu switch-casepernyataan. Jangan salah paham, saya pikir ini tempatnya dan saya mungkin akan mencari cara untuk mengimplementasikannya.
IAbstract
2
Meskipun pertanyaan ini sudah lebih dari dua tahun, rasanya relevan untuk menyebutkan bahwa C # 7 akan segera keluar (ish) dengan kemampuan pencocokan pola.
Abion47

Jawaban:

22

Saya tahu ini adalah topik lama, tetapi di c # 7 Anda dapat melakukannya:

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}
Marcus Pierce
sumber
Perbedaan penting di sini antara C # dan F # adalah kelengkapan dari kecocokan pola. Bahwa pencocokan pola mencakup setiap kemungkinan case yang tersedia, dijelaskan sepenuhnya, peringatan dari kompiler jika Anda tidak. Meskipun Anda berhak berdebat bahwa case default melakukan ini, ia juga sering dalam praktiknya merupakan pengecualian run-time.
VoronoiPotato
37

Setelah mencoba melakukan hal-hal "fungsional" seperti itu di C # (dan bahkan mencoba buku tentang itu), saya sampai pada kesimpulan bahwa tidak, dengan beberapa pengecualian, hal-hal seperti itu tidak banyak membantu.

Alasan utamanya adalah bahasa-bahasa seperti F # mendapatkan banyak kekuatan dari benar-benar mendukung fitur-fitur ini. Bukan "Anda bisa melakukannya", tetapi "itu sederhana, jelas, itu diharapkan".

Misalnya, dalam pencocokan pola, Anda mendapatkan kompiler memberi tahu Anda jika ada pertandingan yang tidak lengkap atau ketika pertandingan lain tidak akan pernah mengenai. Ini kurang berguna untuk tipe open ended, tetapi ketika mencocokkan serikat yang didiskriminasi atau tupel, ini sangat bagus. Dalam F #, Anda mengharapkan orang untuk mencocokkan pola, dan itu langsung masuk akal.

"Masalahnya" adalah begitu Anda mulai menggunakan beberapa konsep fungsional, wajar jika Anda ingin melanjutkan. Namun, memanfaatkan tupel, fungsi, aplikasi metode parsial dan kari, pencocokan pola, fungsi bersarang, generik, dukungan monad, dll. Di C # menjadi sangat jelek, sangat cepat. Sangat menyenangkan, dan beberapa orang yang sangat pintar telah melakukan beberapa hal yang sangat keren di C #, tetapi sebenarnya menggunakannya terasa berat.

Apa yang akhirnya saya gunakan (lintas proyek) di C #:

  • Fungsi urutan, melalui metode ekstensi untuk IEnumerable. Hal-hal seperti ForEach atau Proses ("Terapkan"? - lakukan tindakan pada item urutan seperti yang disebutkan) cocok karena sintaks C # mendukungnya dengan baik.
  • Mengabstraksi pola pernyataan umum. Blok try / catch / akhirnya rumit atau blok kode lain yang terlibat (sering sangat generik). Memperluas LINQ-to-SQL juga cocok di sini.
  • Tuples, sampai batas tertentu.

** Namun perlu diperhatikan: Kurangnya generalisasi otomatis dan inferensi tipe benar-benar menghambat penggunaan bahkan fitur-fitur ini. **

Semua ini dikatakan, seperti orang lain sebutkan, di tim kecil, untuk tujuan tertentu, ya, mungkin mereka bisa membantu jika Anda terjebak dengan C #. Tetapi dalam pengalaman saya, mereka biasanya merasa lebih merepotkan daripada nilainya - YMMV.

Beberapa tautan lain:

MichaelGG
sumber
25

Arguably alasan bahwa C # tidak membuatnya mudah untuk mengaktifkan tipe adalah karena itu terutama bahasa berorientasi objek, dan cara yang 'benar' untuk melakukan ini dalam istilah berorientasi objek akan mendefinisikan metode GetRentPrice pada Kendaraan dan menimpanya di kelas turunan.

Yang mengatakan, saya telah menghabiskan sedikit waktu bermain dengan multi-paradigma dan bahasa fungsional seperti F # dan Haskell yang memiliki jenis kemampuan ini, dan saya telah menemukan sejumlah tempat di mana itu akan berguna sebelumnya (misalnya ketika Anda tidak menulis jenis yang perlu Anda aktifkan sehingga Anda tidak dapat menerapkan metode virtual pada mereka) dan itu adalah sesuatu yang saya akan sambut dengan bahasa bersama dengan serikat yang didiskriminasi.

[Sunting: Bagian yang dihapus tentang kinerja seperti yang ditunjukkan oleh Marc bisa mengalami hubungan pendek]

Masalah potensial lainnya adalah kegunaan - jelas dari panggilan terakhir apa yang terjadi jika pertandingan gagal memenuhi persyaratan apa pun, tetapi bagaimana perilaku jika cocok dengan dua atau lebih kondisi? Haruskah itu membuat pengecualian? Haruskah mengembalikan pertandingan pertama atau terakhir?

Suatu cara yang saya cenderung gunakan untuk memecahkan masalah semacam ini adalah dengan menggunakan bidang kamus dengan jenis sebagai kunci dan lambda sebagai nilainya, yang cukup singkat untuk membangun menggunakan sintaksis objek initializer; Namun, ini hanya memperhitungkan jenis beton dan tidak memungkinkan predikat tambahan sehingga mungkin tidak cocok untuk kasus yang lebih kompleks. [Catatan tambahan - jika Anda melihat output dari kompiler C # itu sering mengonversi pernyataan beralih ke tabel lompatan berbasis kamus, jadi sepertinya tidak ada alasan yang bagus mengapa ia tidak dapat mendukung pengaktifan jenis]

Greg Beech
sumber
1
Sebenarnya - versi yang saya miliki hubung singkat di kedua delegasi dan versi ekspresi. Versi ekspresi dikompilasi ke kondisional majemuk; versi delegasi hanyalah sekumpulan predikat dan fungsi / tindakan - begitu sudah cocok, ia akan berhenti.
Marc Gravell
Menarik - dari tampilan sepintas saya berasumsi harus melakukan setidaknya pemeriksaan dasar setiap kondisi seperti itu terlihat seperti rantai metode, tapi sekarang saya menyadari metode sebenarnya rantai contoh objek untuk membangunnya sehingga Anda bisa melakukan ini. Saya akan mengedit jawaban saya untuk menghapus pernyataan itu.
Greg Beech
22

Saya tidak berpikir perpustakaan semacam ini (yang bertindak seperti ekstensi bahasa) cenderung mendapat penerimaan luas, tetapi menyenangkan untuk dimainkan, dan bisa sangat berguna bagi tim kecil yang bekerja di domain tertentu di mana ini berguna. Sebagai contoh, jika Anda menulis banyak 'aturan / logika bisnis' yang melakukan tes jenis sewenang-wenang seperti ini dan yang lainnya, saya bisa melihat bagaimana itu akan berguna.

Saya tidak tahu apakah ini akan menjadi fitur bahasa C # (sepertinya ragu, tapi siapa yang bisa melihat masa depan?).

Untuk referensi, F # yang sesuai kira-kira:

let getRentPrice (v : Vehicle) = 
    match v with
    | :? Motorcycle as bike -> 100 + bike.Cylinders * 10
    | :? Bicycle -> 30
    | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20
    | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20
    | _ -> failwith "blah"

dengan asumsi Anda mendefinisikan hierarki kelas di sepanjang baris

type Vehicle() = class end

type Motorcycle(cyl : int) = 
    inherit Vehicle()
    member this.Cylinders = cyl

type Bicycle() = inherit Vehicle()

type EngineType = Diesel | Gasoline

type Car(engType : EngineType, doors : int) = 
    inherit Vehicle()
    member this.EngineType = engType
    member this.Doors = doors
Brian
sumber
2
Terima kasih untuk versi F #. Saya kira saya suka cara F # menangani ini, tapi saya tidak yakin (secara keseluruhan) F # adalah pilihan yang tepat saat ini, jadi saya harus berjalan di jalan tengah ...
Marc Gravell
13

Untuk menjawab pertanyaan Anda, ya saya pikir konstruksi sintaksis pencocokan pola berguna. I untuk satu ingin melihat dukungan sintaksis di C # untuk itu.

Berikut ini adalah implementasi saya dari kelas yang menyediakan (hampir) sintaksis yang sama seperti yang Anda jelaskan

public class PatternMatcher<Output>
{
    List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>();

    public PatternMatcher() { }        

    public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function)
    {
        cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function));
        return this;
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function)
    {
        return Case(
            o => o is T && condition((T)o), 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Func<T, Output> function)
    {
        return Case(
            o => o is T, 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o)
    {
        return Case(condition, x => o);
    }

    public PatternMatcher<Output> Case<T>(Output o)
    {
        return Case<T>(x => o);
    }

    public PatternMatcher<Output> Default(Func<Object, Output> function)
    {
        return Case(o => true, function);
    }

    public PatternMatcher<Output> Default(Output o)
    {
        return Default(x => o);
    }

    public Output Match(Object o)
    {
        foreach (var tuple in cases)
            if (tuple.Item1(o))
                return tuple.Item2(o);
        throw new Exception("Failed to match");
    }
}

Berikut ini beberapa kode tes:

    public enum EngineType
    {
        Diesel,
        Gasoline
    }

    public class Bicycle
    {
        public int Cylinders;
    }

    public class Car
    {
        public EngineType EngineType;
        public int Doors;
    }

    public class MotorCycle
    {
        public int Cylinders;
    }

    public void Run()
    {
        var getRentPrice = new PatternMatcher<int>()
            .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10) 
            .Case<Bicycle>(30) 
            .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
            .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
            .Default(0);

        var vehicles = new object[] {
            new Car { EngineType = EngineType.Diesel, Doors = 2 },
            new Car { EngineType = EngineType.Diesel, Doors = 4 },
            new Car { EngineType = EngineType.Gasoline, Doors = 3 },
            new Car { EngineType = EngineType.Gasoline, Doors = 5 },
            new Bicycle(),
            new MotorCycle { Cylinders = 2 },
            new MotorCycle { Cylinders = 3 },
        };

        foreach (var v in vehicles)
        {
            Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v));
        }
    }
cdiggins
sumber
9

Pencocokan pola (seperti dijelaskan di sini ), tujuannya adalah mendekonstruksi nilai sesuai dengan spesifikasi tipenya. Namun, konsep kelas (atau tipe) di C # tidak setuju dengan Anda.

Tidak ada yang salah dengan desain bahasa multi-paradigma, sebaliknya, itu sangat bagus untuk memiliki lambda di C #, dan Haskell dapat melakukan hal-hal penting seperti misalnya IO. Tapi itu bukan solusi yang sangat elegan, tidak dalam mode Haskell.

Tetapi karena bahasa pemrograman prosedural berurutan dapat dipahami dalam hal kalkulus lambda, dan C # cocok dengan parameter dari bahasa prosedural berurutan, itu cocok. Tetapi, mengambil sesuatu dari konteks fungsional murni katakanlah Haskell, dan kemudian menempatkan fitur itu ke dalam bahasa yang tidak murni, well, melakukan hal itu, tidak akan menjamin hasil yang lebih baik.

Maksud saya adalah ini, apa yang membuat tick pola yang cocok dikaitkan dengan desain bahasa dan model data. Karena itu, saya tidak percaya pencocokan pola menjadi fitur yang berguna dari C # karena tidak menyelesaikan masalah C # khas juga tidak cocok dengan paradigma pemrograman imperatif.

John Leidegren
sumber
1
Mungkin. Memang, saya akan berjuang untuk memikirkan argumen "pembunuh" yang meyakinkan untuk mengapa itu diperlukan (sebagai lawan dari "mungkin bagus dalam beberapa kasus tepi dengan biaya membuat bahasa lebih kompleks").
Marc Gravell
5

IMHO cara OO melakukan hal-hal seperti itu adalah pola Pengunjung. Metode anggota pengunjung Anda hanya bertindak sebagai konstruksi kasus dan Anda membiarkan bahasa itu sendiri menangani pengiriman yang sesuai tanpa harus "mengintip" jenis.

bacila
sumber
4

Meskipun tidak terlalu 'C-sharpey' untuk mengaktifkan tipe, saya tahu bahwa konstruk akan sangat membantu dalam penggunaan umum - saya memiliki setidaknya satu proyek pribadi yang dapat menggunakannya (meskipun ATM yang dikelola). Apakah ada banyak masalah kinerja kompilasi, dengan pohon ekspresi menulis ulang?

Simon Buchan
sumber
Tidak jika Anda men-cache objek untuk digunakan kembali (yang sebagian besar cara kerja ekspresi C # lambda, kecuali kompiler menyembunyikan kode). Penulisan ulang pasti meningkatkan kinerja yang dikompilasi - namun, untuk penggunaan reguler (daripada LINQ-to-Something) Saya berharap versi delegasi mungkin lebih berguna.
Marc Gravell
Perhatikan juga - ini tidak harus berupa saklar pada tipe - ini juga dapat digunakan sebagai bersyarat komposit (bahkan melalui LINQ) - tetapi tanpa uji x => yang berantakan? Hasil1: (Test2? Hasil2: (Test3? Hasil 3: Result4))
Marc Gravell
Senang mengetahui, meskipun saya bermaksud melakukan kompilasi yang sebenarnya : berapa lama csc.exe memakan waktu - saya tidak cukup akrab dengan C # untuk mengetahui apakah itu benar-benar masalah, tapi ini masalah besar bagi C ++.
Simon Buchan
csc tidak akan berkedip karena hal ini - sangat mirip dengan cara kerja LINQ, dan kompiler C # 3.0 cukup bagus dalam metode LINQ / ekstensi dll.
Marc Gravell
3

Saya pikir ini terlihat sangat menarik (+1), tetapi satu hal yang harus diperhatikan: kompiler C # cukup bagus dalam mengoptimalkan pernyataan switch. Tidak hanya untuk hubungan arus pendek - Anda mendapatkan IL yang sama sekali berbeda tergantung pada berapa banyak kasus yang Anda miliki dan seterusnya.

Contoh spesifik Anda memang melakukan sesuatu yang menurut saya sangat berguna - tidak ada sintaks yang setara dengan kasus per jenis, seperti (misalnya) typeof(Motorcycle) bukan konstanta.

Ini semakin menarik dalam aplikasi dinamis - logika Anda di sini bisa dengan mudah digerakkan oleh data, memberikan eksekusi gaya 'mesin-aturan'.

Keith
sumber
0

Anda dapat mencapai apa yang Anda inginkan dengan menggunakan perpustakaan yang saya tulis, bernama OneOf

Keuntungan utama atas switch(dan ifdan exceptions as control flow) adalah bahwa ia aman saat kompilasi - tidak ada penangan default atau gagal

   OneOf<Motorcycle, Bicycle, Car> vehicle = ... //assign from one of those types
   var getRentPrice = vehicle
        .Match(
            bike => 100 + bike.Cylinders * 10, // "bike" here is typed as Motorcycle
            bike => 30, // returns a constant
            car => car.EngineType.Match(
                diesel => 220 + car.Doors * 20
                petrol => 200 + car.Doors * 20
            )
        );

Ada di Nuget dan menargetkan net451 dan netstandard1.6

mcintyre321
sumber