Apakah ada cara yang baik untuk membuat fungsi tanda tangan lebih informatif dalam Haskell?

Saya menyadari bahwa ini bisa berpotensi dianggap subjektif atau mungkin off-topik pertanyaan, jadi saya berharap bahwa daripada harus ditutup itu akan mendapatkan bermigrasi, mungkin untuk Programmer.

I'm mulai belajar Haskell, sebagian besar untuk membangunnya sendiri, dan aku suka banyak dari ide-ide dan prinsip-prinsip dukungan bahasa. Saya menjadi terpesona dengan bahasa-bahasa fungsional setelah mengambil teori bahasa kelas di mana kita bermain-main dengan Cadel, dan aku telah mendengar banyak hal baik tentang bagaimana produktif Haskell bisa, jadi saya pikir saya'd menyelidiki sendiri. Sejauh ini, saya suka bahasa, kecuali untuk satu hal yang saya dapat't hanya mendapatkan jauh dari: ibu-ibu sialan fungsi tanda tangan.

Saya profesional latar belakang sebagian besar melakukan OO, terutama di pulau Jawa. Kebanyakan dari tempat-tempat yang saya've bekerja untuk memiliki dipalu di banyak standar modern dogma, Lincah, Kode yang Bersih, TDD, dll. Setelah beberapa tahun bekerja dengan cara ini, Itu pasti telah menjadi zona nyaman saya, terutama gagasan bahwa "baik" kode harus menjadi diri mendokumentasikan. I've menjadi digunakan untuk bekerja di sebuah IDE, dimana panjang dan verbose nama metode dengan sangat deskriptif tanda tangan non-isu dengan intelligent auto penyelesaian dan berbagai macam alat-alat analisis untuk menavigasi paket dan simbol-simbol; jika saya dapat menekan Ctrl+Space di Eclipse, kemudian menyimpulkan apa metode ini melakukan dari melihat namanya dan lokal cakupan variabel-variabel yang terkait dengan argumen bukan menarik sampai JavaDocs, I'm bahagia dalam kotoran.

Ini, jelas, bukan bagian dari komunitas praktek-praktek terbaik dalam Haskell. I'telah membaca banyak pendapat yang berbeda tentang masalah ini, dan saya mengerti bahwa Haskell masyarakat menganggap kekompakan untuk menjadi "pro". I've pergi melalui Cara Membaca Haskell, dan saya memahami alasan di balik banyak keputusan, tapi itu doesn't berarti bahwa saya seperti mereka; satu huruf nama-nama variabel, dll. aren't menyenangkan bagi saya. Saya mengakui bahwa saya'll harus mendapatkan digunakan untuk itu jika aku ingin tetap hacking dengan bahasa.

Tapi aku bisa't mendapatkan alih fungsi tanda tangan. Ambil contoh ini, seperti yang ditarik dari Belajar Haskell[...]'s bagian pada fungsi sintaks:

bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"  
    | weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"  
    | otherwise                   = "You're a whale, congratulations!"

Saya menyadari bahwa ini adalah contoh yang bodoh yang hanya dibuat untuk tujuan menjelaskan penjaga dan kelas kendala, tetapi jika anda adalah untuk memeriksa hanya tanda tangan dari fungsi itu, anda akan memiliki tidak tahu yang mana dari argumen ini dimaksudkan untuk menjadi berat atau tinggi. Bahkan jika anda menggunakan Float atau Double dan bukan dari jenis apa pun, ia tetap tidak dapat segera dilihat.

Pada awalnya, saya pikir saya akan menjadi lucu dan pintar dan brilian dan mencoba untuk menipunya lagi menggunakan jenis variabel nama dengan beberapa kendala kelas:

bmiTell :: (RealFloat weight, RealFloat height) => weight -> height -> String

Ini meludahkan kesalahan (sebagai samping, jika ada yang bisa menjelaskan kesalahan saya, saya'd bersyukur):

Could not deduce (height ~ weight)
    from the context (RealFloat weight, RealFloat height)
      bound by the type signature for
                 bmiTell :: (RealFloat weight, RealFloat height) =>
                            weight -> height -> String
      at example.hs:(25,1)-(27,27)
      `height' is a rigid type variable bound by
               the type signature for
                 bmiTell :: (RealFloat weight, RealFloat height) =>
                            weight -> height -> String
               at example.hs:25:1
      `weight' is a rigid type variable bound by
               the type signature for
                 bmiTell :: (RealFloat weight, RealFloat height) =>
                            weight -> height -> String
               at example.hs:25:1
    In the first argument of `(^)', namely `height'
    In the second argument of `(/)', namely `height ^ 2'
    In the first argument of `(<=)', namely `weight / height ^ 2'

Tidak memahami sepenuhnya mengapa tidak't bekerja, saya mulai Googling, dan saya bahkan menemukan ini sedikit posting yang menunjukkan parameter bernama, khusus, spoofing bernama parameter melalui type, tapi itu tampaknya sedikit banyak.

Apakah tidak ada cara yang dapat diterima untuk kerajinan fungsi informatif tanda tangan? Adalah "Haskell Cara" hanya untuk Haddock omong kosong dari segala sesuatu?

Mengomentari pertanyaan (4)
Larutan

Jenis tanda tangan yang bukan Jawa-gaya signature. Java-gaya tanda tangan anda akan memberitahu anda yang parameter berat badan dan yang lebih tinggi hanya karena bercampur dengan nama parameter dengan parameter jenis. Haskell dapat't melakukan hal ini sebagai aturan umum, karena fungsi yang didefinisikan menggunakan pencocokan pola dan beberapa persamaan, seperti dalam:

map :: (a -> b) -> [a] -> [b]
map f (x:xs) = f x : map f xs
map _ [] = []

Berikut parameter pertama bernama f dalam persamaan pertama dan _ (yang cukup banyak berarti "tidak disebutkan namanya") yang kedua. Parameter kedua doesn't memiliki nama dalam salah satu persamaan; di bagian pertama ini memiliki nama-nama (dan programmer mungkin akan berpikir itu sebagai "xs daftar"), sementara di kedua itu's benar-benar ekspresi literal.

Dan kemudian ada's point-definisi gratis seperti:

concat :: [[a]] -> [a]
concat = foldr (++) []

Jenis signature memberitahu kita dibutuhkan suatu parameter yang tipe [[a]], tapi tidak ada nama untuk parameter ini muncul saja dalam sistem.

Di luar individu persamaan untuk fungsi, nama-nama ini digunakan untuk merujuk pada argumen yang tidak relevan pula kecuali sebagai dokumentasi. Sejak ide "nama kanonik" untuk fungsi's parameter isn't didefinisikan dengan baik di Haskell, tempat untuk informasi "parameter pertama dari bmiTell mewakili berat badan sementara yang kedua merupakan ketinggian" di dokumentasi, tidak dalam jenis tanda tangan.

Saya benar-benar setuju bahwa apa fungsi tidak harus jernih dari "semua" informasi yang tersedia tentang hal itu. Di Jawa, yang merupakan fungsi's nama, dan parameter jenis dan nama. Jika (seperti biasa) pengguna akan membutuhkan informasi lebih dari itu, anda menambahkannya dalam dokumentasi. Di Haskell informasi publik tentang fungsi adalah fungsi's nama dan jenis parameter. Jika pengguna akan membutuhkan informasi lebih dari itu, anda menambahkannya dalam dokumentasi. Catatan IDEs untuk Haskell seperti Leksah akan dengan mudah menunjukkan Haddock komentar.


Perhatikan bahwa pilihan hal yang harus dilakukan dalam bahasa yang kuat dan ekspresif jenis sistem seperti Haskell's lebih sering untuk mencoba membuat banyak kesalahan mungkin terdeteksi sebagai jenis kesalahan. Dengan demikian, fungsi seperti bmiTell segera set off tanda-tanda peringatan untuk saya, untuk alasan berikut:

  1. Dibutuhkan dua parameter dari jenis yang sama yang mewakili hal-hal yang berbeda
  2. Ia akan melakukan hal yang salah jika melewati parameter dalam urutan yang salah
  3. Dua jenis don't memiliki posisi alami (sebagai dua [a] argumen ++ do)

Satu hal yang sering dilakukan untuk meningkatkan safety tipe ini memang untuk membuat newtypes, seperti dalam link yang anda temukan. Saya don't benar-benar berpikir ini memiliki banyak yang harus dilakukan dengan parameter bernama lewat, lebih dari itu adalah tentang membuat datatype yang secara eksplisit merupakan tinggi, daripada setiap jumlah lain yang mungkin anda ingin mengukur dengan nomor. Jadi saya tidak't memiliki newtype nilai-nilai yang muncul hanya pada panggilan; aku akan menggunakan newtype nilai di mana pun saya punya data ketinggian dari juga, dan mengedarkannya sebagai data ketinggian daripada sebagai angka, sehingga saya mendapatkan jenis-keselamatan (dan dokumentasi) manfaat di mana-mana. Saya hanya akan membuka nilai ke nomor baku ketika saya harus lulus itu untuk sesuatu yang beroperasi pada angka-angka dan bukan pada ketinggian (seperti operasi aritmatika dalam bmiTell).

Catatan bahwa ini tidak memiliki runtime overhead; newtypes diwakili identik dengan data "dalam" type wrapper, jadi bungkus/membuka operasi ada-ops yang mendasari representasi dan hanya dihapus selama kompilasi. Itu hanya menambahkan karakter tambahan dalam kode sumber, namun karakter-karakter tersebut persis dokumentasi anda're apapun, dengan manfaat tambahan yang diberlakukan oleh compiler; Jawa-gaya tanda tangan memberitahu anda yang parameter berat badan dan yang lebih tinggi, tetapi compiler masih won't dapat memberitahu jika anda secara tidak sengaja melewati mereka yang salah jalan di sekitar!

Komentar (10)

Ada pilihan lain, tergantung pada bagaimana konyol dan/atau bertele-tele anda ingin mendapatkan dengan anda jenis.

Misalnya, anda bisa melakukan ini...

type Meaning a b = a

bmiTell :: (RealFloat a) => a `Meaning` weight -> a `Meaning` height -> String  
bmiTell weight height = -- etc.

...tapi yang's sangat konyol, berpotensi membingungkan, dan doesn't membantu dalam kebanyakan kasus. Hal yang sama berlaku untuk ini, yang juga memerlukan menggunakan ekstensi bahasa:

bmiTell :: (RealFloat weight, RealFloat height, weight ~ height) 
        => weight -> height -> String  
bmiTell weight height = -- etc.

Sedikit lebih masuk akal akan hal ini:

type Weight a = a
type Height a = a

bmiTell :: (RealFloat a) => Weight a -> Height a -> String  
bmiTell weight height = -- etc.

...tapi yang's masih agak konyol dan cenderung untuk tersesat ketika GHC memperluas jenis sinonim.

Masalah sebenarnya di sini adalah bahwa anda're melampirkan tambahan konten semantik untuk nilai yang berbeda dari yang sama polymorphic, yang akan melawan arus dari bahasa itu sendiri dan, seperti itu, biasanya tidak idiomatik.

Salah satu pilihan, tentu saja, adalah untuk hanya berurusan dengan tidak informatif jenis variabel. Tapi yang's sangat tidak memuaskan jika ada's yang signifikan perbedaan antara dua hal dari jenis yang sama yang's tidak jelas dari urutan mereka're diberikan dalam.

Apa yang saya'd sarankan anda mencoba, sebaliknya, menggunakan type pembungkus untuk menentukan semantik:

newtype Weight a = Weight { getWeight :: a }
newtype Height a = Height { getHeight :: a }

bmiTell :: (RealFloat a) => Weight a -> Height a -> String  
bmiTell (Weight weight) (Height height)

Lakukan ini adalah tempat di dekat sebagai umum sebagai layak untuk menjadi, saya pikir. It's sedikit ekstra mengetik (ha ha) tapi tidak hanya membuat anda jenis tanda tangan yang lebih informatif bahkan dengan jenis sinonim diperluas, memungkinkan jenis checker menangkap jika anda salah menggunakan berat badan sebagai tinggi, atau seperti. Dengan GeneralizedNewtypeDeriving ekstensi anda bahkan bisa mendapatkan otomatis kasus bahkan untuk jenis kelas yang dapat't biasanya dapat diperoleh.

Komentar (12)

Haddock dan/atau juga melihat fungsi persamaan (nama anda terikat untuk hal-hal) ini adalah cara yang saya katakan apa yang's terjadi. Anda dapat Haddock parameter individu, seperti,

bmiTell :: (RealFloat a) => a      -- ^ your weight
                         -> a      -- ^ your height
                         -> String -- ^ what I'd think about that

jadi itu's tidak hanya gumpalan teks yang menjelaskan semua hal.

Alasan lucu jenis variabel didn't bekerja adalah bahwa anda fungsi:

(RealFloat a) => a -> a -> String

Tapi anda berusaha mengubah:

(RealFloat weight, RealFloat height) => weight -> height -> String

ini setara dengan ini:

(RealFloat a, RealFloat b) => a -> b -> String

Jadi, dalam hal ini jenis tanda tangan yang anda telah mengatakan bahwa dua yang pertama memiliki argumen yang berbeda * jenis, tetapi GHC telah ditentukan bahwa (berdasarkan penggunaan anda) mereka harus sama ** jenis. Jadi itu mengeluh bahwa ia tidak dapat menentukan yang berat dan tinggi adalah tipe yang sama, meskipun mereka harus (yaitu, diusulkan jenis tanda tangan tidak cukup ketat dan akan memungkinkan tidak valid menggunakan function).

Komentar (2)

berat harus jenis yang sama sebagai tinggi karena anda're membagi mereka (tidak ada implisit gips). berat ~ tinggi berarti mereka're jenis yang sama. ghc telah pergi sedikit menjelaskan bagaimana ia sampai pada kesimpulan bahwa berat ~ tinggi diperlukan, maaf. Anda diperbolehkan untuk memberitahu apa itu/anda ingin menggunakan sintaks dari jenis keluarga ekstensi:


{-# LANGUAGE TypeFamilies #-}
bmiTell :: (RealFloat weight, RealFloat height,weight~height) => weight -> height -> String
bmiTell weight height  
  | weight / height ^ 2 
Komentar (4)

Coba ini:

type Height a = a
type Weight a = a

bmiTell :: (RealFloat a) => Weight a -> Height a -> String
Komentar (0)

Mungkin tidak relevan untuk fungsi dengan piffling dua argumen, however... Jika anda memiliki fungsi yang membutuhkan banyak argumen, dari jenis yang sama atau hanya tidak jelas memesan, mungkin layak mendefinisikan sebuah struktur data yang mewakili mereka. Misalnya,

data Body a = Body {weight, height :: a}

bmiTell :: (RealFloat a) => Body a -> String

Sekarang anda dapat menulis baik

bmiTell (Body {weight = 5, height = 2})

atau

bmiTell (Body {height = 2, weight = 5})

dan itu akan bernilai benar kedua-duanya, dan juga menjadi damed jelas untuk siapa pun mencoba untuk membaca kode anda.

It's mungkin lebih layak untuk fungsi-fungsi dengan jumlah yang lebih besar dari argumen, meskipun. Untuk hanya dua, aku akan pergi dengan orang lain dan hanya type itu jadi jenis tanda tangan dokumen yang benar parameter order dan anda mendapatkan compile-time error jika anda mencampur mereka.

Komentar (1)