Apakah pencocokan pola dengan tipe desain idiomatik atau buruk?

18

Sepertinya kode F # sering pola cocok dengan tipe. Pasti

match opt with 
| Some val -> Something(val) 
| None -> Different()

sepertinya biasa.

Tapi dari perspektif OOP, yang terlihat sangat mengerikan seperti aliran kontrol berdasarkan pemeriksaan tipe runtime, yang biasanya akan disukai. Untuk mengejanya, dalam OOP Anda mungkin lebih suka menggunakan overloading:

type T = 
    abstract member Route : unit -> unit

type Foo() = 
    interface T with
        member this.Route() = printfn "Go left"

type Bar() = 
    interface T with
        member this.Route() = printfn "Go right"

Ini tentunya lebih banyak kode. OTOH, menurut pikiran saya OOP-y memiliki kelebihan struktural:

  • ekstensi ke bentuk baru Tmudah;
  • Saya tidak perlu khawatir menemukan duplikasi aliran kontrol pemilihan rute; dan
  • Pilihan rute tidak dapat diubah dalam arti bahwa begitu saya ada Foodi tangan, saya tidak perlu khawatir tentang Bar.Route()implementasi rute

Apakah ada keuntungan dari pencocokan pola dengan jenis yang tidak saya lihat? Apakah itu dianggap idiomatis atau apakah itu kemampuan yang tidak umum digunakan?

Larry OBrien
sumber
3
Seberapa masuk akal untuk melihat bahasa fungsional dari perspektif OOP? Bagaimanapun, kekuatan sebenarnya dari pencocokan pola datang dengan pola bersarang. Hanya memeriksa konstruktor terluar adalah mungkin, tetapi tidak berarti keseluruhan cerita.
Ingo
Ini - But from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on.- terdengar terlalu dogmatis. Kadang-kadang, Anda ingin memisahkan ops Anda dari hierarki Anda: mungkin 1) Anda tidak dapat menambahkan op ke hierarki b / c Anda tidak memiliki hierarki; 2) kelas-kelas yang Anda inginkan untuk op tidak cocok dengan hierarki Anda; 3) Anda bisa menambahkan op ke hierarki Anda, tetapi tidak ingin b / c Anda tidak ingin mengacaukan API hierarki Anda dengan banyak omong kosong yang kebanyakan klien tidak gunakan.
4
Hanya untuk memperjelas, Somedan Nonebukan tipe. Keduanya adalah konstruktor yang tipenya forall a. a -> option adan forall a. option a(maaf, tidak yakin apa sintaks untuk tipe anotasi dalam F #).

Jawaban:

20

Anda benar bahwa hierarki kelas OOP sangat terkait erat dengan serikat yang didiskriminasi dalam F # dan bahwa pencocokan pola sangat erat terkait dengan tes tipe dinamis. Sebenarnya, ini sebenarnya bagaimana F # mengkompilasi serikat yang didiskriminasi ke .NET!

Mengenai ekstensibilitas, ada dua sisi masalah:

  • OO memungkinkan Anda menambahkan subkelas baru, tetapi menyulitkan untuk menambahkan fungsi baru (virtual)
  • FP memungkinkan Anda menambahkan fungsi-fungsi baru, tetapi menyulitkan untuk menambahkan kasus serikat baru

Yang mengatakan, F # akan memberi Anda peringatan ketika Anda melewatkan kasus dalam pencocokan pola, jadi menambahkan kasus serikat baru sebenarnya tidak terlalu buruk.

Mengenai menemukan duplikasi dalam pemilihan root - F # akan memberi Anda peringatan ketika Anda memiliki kecocokan yang duplikat, misalnya:

match x with
| Some foo -> printfn "first"
| Some foo -> printfn "second" // Warning on this line as it cannot be matched
| None -> printfn "third"

Fakta bahwa "pilihan rute tidak berubah" mungkin juga bermasalah. Misalnya, jika Anda ingin berbagi implementasi fungsi antara Foodan Barkasus, tetapi melakukan sesuatu yang lain untuk Zookasus ini, Anda bisa menyandikan itu dengan mudah menggunakan pencocokan pola:

match x with
| Foo y | Bar y -> y * 20
| Zoo y -> y * 30

Secara umum, FP lebih fokus pada pertama merancang jenis dan kemudian menambahkan fungsi. Jadi itu benar-benar mendapat manfaat dari fakta bahwa Anda dapat menyesuaikan jenis Anda (model domain) dalam beberapa baris dalam satu file dan kemudian dengan mudah menambahkan fungsi yang beroperasi pada model domain.

Kedua pendekatan - OO dan FP cukup saling melengkapi dan keduanya memiliki kelebihan dan kekurangan. Yang sulit (berasal dari perspektif OO) adalah F # biasanya menggunakan gaya FP sebagai default. Tetapi jika benar-benar ada lebih banyak kebutuhan untuk menambahkan sub-kelas baru, Anda selalu dapat menggunakan antarmuka. Tetapi di sebagian besar sistem, Anda sama-sama perlu menambahkan jenis dan fungsi, sehingga pilihannya benar-benar tidak terlalu penting - dan menggunakan serikat yang didiskriminasi dalam F # lebih baik.

Saya akan merekomendasikan seri blog yang hebat ini untuk informasi lebih lanjut.

Tomas Petricek
sumber
3
Meskipun Anda benar, saya ingin menambahkan bahwa ini bukan masalah OO vs FP karena ini adalah masalah objek vs tipe penjumlahan. Obsesi OOP terhadap mereka, tidak ada benda yang membuat mereka tidak berfungsi. Dan jika Anda melompati cukup simpai, Anda dapat menerapkan tipe penjumlahan dalam bahasa OOP arus utama juga (meskipun tidak akan cantik).
Doval
1
"Dan jika Anda melompati cukup banyak rintangan, Anda dapat menerapkan tipe penjumlahan dalam bahasa OOP arus utama juga (meskipun tidak akan cantik)." -> Saya kira Anda akan berakhir dengan sesuatu yang mirip dengan bagaimana tipe F # jumlah dikodekan dalam sistem tipe .NET :)
Tarmil
7

Anda telah mengamati dengan benar bahwa pencocokan pola (pada dasarnya pernyataan sakelar supercharged) dan pengiriman dinamis memiliki kesamaan. Mereka juga hidup berdampingan dalam beberapa bahasa, dengan hasil yang sangat menyenangkan. Namun, ada sedikit perbedaan.

Saya mungkin menggunakan sistem tipe untuk mendefinisikan tipe yang hanya dapat memiliki jumlah subtipe tetap:

// pseudocode
data Bool = False | True
data Option a = None | Some item:a
data Tree a = Leaf item:a | Node (left:Tree a) (right:Tree a)

Tidak akan pernah ada subtipe lain dari Boolatau Option, jadi subclass tampaknya tidak berguna (beberapa bahasa seperti Scala memiliki gagasan tentang subclass yang dapat menangani hal ini - kelas dapat ditandai sebagai "final" di luar unit kompilasi saat ini, tetapi subtipe dapat didefinisikan di dalam unit kompilasi ini).

Karena subtipe jenis seperti Optionsekarang dikenal secara statis , kompiler dapat memperingatkan jika kita lupa untuk menangani kasus dalam kecocokan pola kita. Ini berarti bahwa kecocokan pola lebih seperti downcast khusus yang memaksa kita untuk menangani semua opsi.

Selain itu, pengiriman metode dinamis (yang diperlukan untuk OOP) juga menyiratkan pemeriksaan tipe runtime, tetapi dari jenis yang berbeda. Oleh karena itu cukup tidak relevan jika kita melakukan pemeriksaan jenis ini secara eksplisit melalui kecocokan pola atau secara implisit melalui pemanggilan metode.

amon
sumber
"Ini berarti bahwa kecocokan pola lebih seperti downcast khusus yang memaksa kita untuk menangani semua opsi" - pada kenyataannya, saya percaya bahwa (selama Anda hanya cocok dengan konstruktor dan bukan nilai atau struktur bersarang) itu isomorfik untuk menempatkan metode virtual abstrak di superclass.
Jules
2

Pencocokan pola F # biasanya dilakukan dengan serikat terdiskriminasi daripada dengan kelas (dan dengan demikian tidak secara teknis jenis-cek sama sekali). Ini memungkinkan kompiler untuk memberi Anda peringatan ketika Anda tidak memperhitungkan kasus dalam pencocokan pola.

Hal lain yang perlu diperhatikan adalah bahwa dalam gaya fungsional, Anda mengatur berbagai hal berdasarkan fungsionalitas bukan oleh data, sehingga pencocokan pola memungkinkan Anda untuk mengumpulkan fungsionalitas yang berbeda di satu tempat daripada tersebar di seluruh kelas. Ini juga memiliki keuntungan bahwa Anda dapat melihat bagaimana kasus-kasus lain ditangani tepat di sebelah tempat Anda perlu melakukan perubahan.

Menambahkan opsi baru akan terlihat seperti:

  1. Tambahkan opsi baru ke serikat Anda yang didiskriminasi
  2. Perbaiki semua peringatan pada kecocokan pola yang tidak lengkap
N_A
sumber
2

Sebagian, Anda melihatnya lebih sering dalam pemrograman fungsional karena Anda menggunakan tipe untuk membuat keputusan lebih sering. Saya menyadari bahwa Anda mungkin hanya memilih contoh kurang lebih secara acak, tetapi OOP yang setara dengan contoh pencocokan pola Anda akan lebih sering terlihat seperti:

if (opt != null)
    opt.Something()
else
    Different()

Dengan kata lain, relatif jarang menggunakan polimorfisme untuk menghindari hal-hal rutin seperti cek kosong di OOP. Sama seperti seorang programmer OO tidak membuat objek nol dalam setiap situasi kecil, seorang programmer fungsional tidak selalu membebani suatu fungsi, terutama ketika Anda tahu daftar pola Anda dijamin lengkap. Jika Anda menggunakan sistem tipe dalam lebih banyak situasi, Anda akan melihatnya menggunakannya dengan cara yang tidak biasa Anda lakukan.

Sebaliknya, pemrograman fungsional idiomatis yang setara dengan contoh OOP Anda kemungkinan besar tidak akan menggunakan pencocokan pola, tetapi akan memiliki fooRoutedan barRoutefungsi yang akan diteruskan sebagai argumen ke kode panggilan. Jika seseorang menggunakan pencocokan pola dalam situasi itu, biasanya dianggap salah, seperti halnya seseorang yang mengaktifkan tipe akan dianggap salah dalam OOP.

Jadi kapan pencocokan pola dianggap sebagai kode pemrograman fungsional yang baik? Ketika Anda melakukan lebih dari sekedar melihat jenisnya, dan ketika memperpanjang persyaratan tidak akan membutuhkan penambahan lebih banyak kasus. Sebagai contoh, Some valtidak hanya memeriksa yang optmemiliki tipe Some, itu juga mengikat valke tipe yang mendasari untuk penggunaan tipe-aman di sisi lain ->. Anda tahu bahwa Anda kemungkinan besar tidak akan pernah membutuhkan kasus ketiga, jadi ini adalah penggunaan yang baik.

Pencocokan pola mungkin secara dangkal menyerupai pernyataan beralih berorientasi objek, tetapi ada banyak lagi yang terjadi, terutama dengan pola yang lebih panjang atau bersarang. Pastikan Anda memperhitungkan semua yang dilakukannya sebelum menyatakannya setara dengan beberapa kode OOP yang dirancang dengan buruk. Seringkali, ini secara ringkas menangani situasi yang tidak dapat diwakili secara bersih dalam hierarki warisan.

Karl Bielefeldt
sumber
Saya tahu Anda tahu ini, dan itu mungkin menyelinap pikiran Anda saat menulis jawaban Anda, tetapi perhatikan itu Somedan Nonebukan tipe, jadi Anda tidak mencocokkan pola pada tipe. Anda pola-pertandingan pada konstruktor dari jenis yang sama . Ini tidak seperti menanyakan "instanceof".
Andres F.