Mengapa ada begitu banyak fungsi `map` untuk berbagai tipe di F #

9

Saya sedang belajar F #. Saya mulai FP dengan Haskell, dan saya ingin tahu untuk ini.

Karena F # adalah bahasa .NET, Tampaknya lebih masuk akal bagi saya untuk mendeklarasikan antarmuka seperti Mappable, seperti halnya Functorkelas jenis haskell .

masukkan deskripsi gambar di sini

Tapi seperti gambar di atas, fungsi F # dipisahkan dan diimplementasikan sendiri. Apa tujuan desain dari desain semacam itu? Bagi saya, memperkenalkan Mappable.mapdan mengimplementasikan ini untuk setiap tipe data akan lebih mudah.

MyBug18
sumber
Pertanyaan ini bukan milik SO. Ini bukan masalah pemrograman. Saya sarankan Anda bertanya di F # Slack atau forum diskusi lainnya.
Bent Tranberg
5
@BentTranberg Dengan murah hati membaca, The community is here to help you with specific coding, algorithm, or language problems.juga akan mencakup pertanyaan desain bahasa, selama kriteria lainnya terpenuhi.
kaefer
3
Singkatnya, F # tidak memiliki kelas tipe, dan karena itu perlu mengimplementasikan kembali mapdan fungsi tingkat tinggi umum lainnya untuk setiap jenis koleksi. Antarmuka akan sedikit membantu karena masih membutuhkan setiap jenis koleksi untuk menyediakan implementasi yang terpisah.
dumetrulo

Jawaban:

19

Ya, pertanyaan yang sangat mudah di permukaan. Tetapi jika Anda meluangkan waktu untuk memikirkannya sampai akhir, Anda masuk ke kedalaman teori jenis yang tak terukur. Dan teori tipe juga menatap Anda.

Pertama, tentu saja, Anda sudah mengetahui dengan benar bahwa F # tidak memiliki kelas tipe, dan itulah sebabnya. Tapi Anda mengusulkan antarmuka Mappable. Ok, mari kita lihat itu.

Katakanlah kita dapat mendeklarasikan antarmuka semacam itu. Bisakah Anda bayangkan seperti apa tanda tangannya?

type Mappable =
    abstract member map : ('a -> 'b) -> 'f<'a> -> 'f<'b>

Di mana fjenis mengimplementasikan antarmuka. Oh tunggu! F # tidak memilikinya juga! Berikut fadalah variabel tipe yang lebih tinggi, dan F # tidak memiliki jenis yang lebih tinggi sama sekali. Tidak ada cara untuk mendeklarasikan fungsi f : 'm<'a> -> 'm<'b>atau sesuatu seperti itu.

Tapi ok, katakanlah kita bisa mengatasi rintangan itu juga. Dan sekarang kita memiliki antarmuka Mappableyang dapat diimplementasikan oleh List, Array, Seq, dan wastafel dapur. Tapi tunggu! Sekarang kita memiliki metode alih-alih fungsi, dan metode tidak menyusun dengan baik! Mari kita lihat menambahkan 42 ke setiap elemen daftar bersarang:

// Good ol' functions:
add42 nestedList = nestedList |> List.map (List.map ((+) 42))

// Using an interface:
add42 nestedList = nestedList.map (fun l -> l.map ((+) 42))

Lihat: sekarang kita harus menggunakan ekspresi lambda! Tidak ada cara untuk meneruskan .mapimplementasi ini ke fungsi lain sebagai nilai. Secara efektif akhir dari "berfungsi sebagai nilai" (dan ya, saya tahu, menggunakan lambda tidak terlihat sangat buruk dalam contoh ini, tapi percayalah, itu menjadi sangat jelek)

Tapi tunggu, kita masih belum selesai. Sekarang ini adalah pemanggilan metode, ketik inferensi tidak berfungsi! Karena tipe tanda tangan metode .NET tergantung pada jenis objek, tidak ada cara bagi kompilator untuk menyimpulkan keduanya. Ini sebenarnya adalah masalah yang sangat umum yang dialami pemula ketika beroperasi dengan. Dan satu-satunya obat adalah dengan memberikan tanda tangan jenis:

add42 (nestedList : #Mappable) = nestedList.map (fun l -> l.map ((+) 42))

Oh, tapi ini masih belum cukup! Meskipun saya telah memberikan tanda tangan untuk nestedListdirinya sendiri, saya belum memberikan tanda tangan untuk parameter lambda l. Seperti apa tanda tangan itu? Apakah Anda akan mengatakannya fun (l: #Mappable) -> ...? Oh, dan sekarang kita akhirnya mendapatkan peringkat-N jenis, karena Anda lihat, #Mappableadalah jalan pintas untuk "semua jenis 'aseperti itu 'a :> Mappable" - yaitu ekspresi lambda yang sendiri generik.

Atau, sebagai alternatif, kita bisa kembali ke kebaikan yang lebih tinggi dan mendeklarasikan tipe yang nestedListlebih tepat:

add42 (nestedList : 'f<'a<'b>> where 'f :> Mappable, 'a :> Mappable) = ...

Tapi ok, mari kita kesampingkan inferensi tipe untuk saat ini dan kembali ke ekspresi lambda dan bagaimana kita sekarang tidak bisa meneruskan mapsebagai nilai ke fungsi lain. Katakanlah kita memperluas sedikit sintaks untuk memungkinkan seperti apa yang Elm lakukan dengan bidang rekaman:

add42 nestedList = nestedList.map (.map ((+) 42))

Seperti apa jadinya .map? Itu harus menjadi tipe kendala , seperti di Haskell!

.map : Mappable 'f => ('a -> 'b) -> 'f<'a> -> 'f<'b>

Wow, baiklah. Mengesampingkan fakta bahwa .NET bahkan tidak mengizinkan jenis seperti itu ada, secara efektif kita baru saja kembali kelas jenis!

Tetapi ada alasan bahwa F # tidak memiliki kelas tipe di tempat pertama. Banyak aspek dari alasan itu dijelaskan di atas, tetapi cara yang lebih ringkas untuk mengatakannya adalah: kesederhanaan .

Untuk Anda lihat, ini adalah bola benang. Setelah Anda memiliki kelas tipe, Anda harus memiliki kendala, jenis yang lebih tinggi, peringkat-N (atau setidaknya peringkat-2), dan sebelum Anda menyadarinya, Anda meminta jenis yang tidak tepat, fungsi tipe, GADT, dan semua sisanya.

Tapi Haskell membayar harga untuk semua barang. Ternyata tidak ada cara yang baik untuk menyimpulkan semua hal itu. Jenis yang lebih baik agak bekerja, tetapi kendala sudah agak tidak. Peringkat-N - bahkan tidak memimpikannya. Dan bahkan ketika itu berhasil, Anda mendapatkan kesalahan ketik yang Anda harus memiliki PhD untuk mengerti Dan itu sebabnya di Haskell Anda dengan lembut didorong untuk menaruh tanda tangan ketik pada segalanya. Yah, tidak semuanya - semuanya, tapi benar-benar hampir semuanya. Dan di mana Anda tidak menaruh tanda tangan tipe (misalnya di dalam letdan where) - kejutan-kejutan, tempat-tempat itu sebenarnya monomorf, jadi Anda pada dasarnya kembali ke tanah F # yang sederhana.

Di F #, di sisi lain, tipe tanda tangan jarang, kebanyakan hanya untuk dokumentasi atau untuk .NET interop. Di luar kedua kasus itu, Anda dapat menulis program kompleks besar di F # dan tidak menggunakan tipe tanda tangan sekali pun. Jenis inferensi berfungsi dengan baik, karena tidak ada yang terlalu rumit atau ambigu untuk ditangani.

Dan ini adalah keuntungan besar F # atas Haskell. Ya, Haskell memungkinkan Anda mengekspresikan hal-hal yang sangat kompleks dengan cara yang sangat tepat, itu bagus. Tapi F # membuat Anda menjadi sangat plin-plan, hampir seperti Python atau Ruby, dan masih ada kompiler yang menangkap Anda jika tersandung.

Fyodor Soikin
sumber