Penjaga vs. if-then-else vs. kasus di Haskell

104

Saya memiliki tiga fungsi yang menemukan elemen ke-n dari sebuah daftar:

nthElement :: [a] -> Int -> Maybe a 
nthElement [] a = Nothing
nthElement (x:xs) a | a <= 0 = Nothing
                    | a == 1 = Just x
                    | a > 1 = nthElement xs (a-1)

nthElementIf :: [a] -> Int -> Maybe a
nthElementIf [] a = Nothing
nthElementIf (x:xs) a = if a <= 1
                        then if a <= 0 
                             then Nothing
                             else Just x -- a == 1
                        else nthElementIf xs (a-1)                           

nthElementCases :: [a] -> Int -> Maybe a
nthElementCases [] a = Nothing
nthElementCases (x:xs) a = case a <= 0 of
                             True -> Nothing
                             False -> case a == 1 of
                                        True -> Just x
                                        False -> nthElementCases xs (a-1)

Menurut saya, fungsi pertama adalah implementasi terbaik karena paling ringkas. Tetapi apakah ada sesuatu tentang dua implementasi lainnya yang akan membuatnya lebih disukai? Dan selanjutnya, bagaimana Anda akan memilih antara menggunakan penjaga, pernyataan if-then-else, dan kasus?

nukleartida
sumber
5
Anda dapat menciutkan casepernyataan bertingkat Anda jika Anda menggunakancase compare a 0 of LT -> ... | EQ -> ... | GT -> ...
rampion
5
@rampion: maksudmucase compare a 1 of ...
new

Jawaban:

121

Dari sudut pandang teknis, ketiga versi itu setara.

Karena itu, aturan praktis saya untuk gaya adalah jika Anda dapat membacanya seolah-olah itu adalah bahasa Inggris (dibaca |sebagai "when", | otherwisesebagai "else" dan =sebagai "is" atau "be"), Anda mungkin sedang melakukan sesuatu Baik.

if..then..elseadalah saat Anda memiliki satu ketentuan biner , atau satu keputusan tunggal yang perlu Anda buat. if..then..elseEkspresi- bersarang sangat tidak umum di Haskell, dan sebagai gantinya, pelindung hampir selalu digunakan.

let absOfN =
  if n < 0 -- Single binary expression
  then -n
  else  n

Setiap if..then..elseekspresi dapat diganti dengan penjaga jika berada di tingkat atas suatu fungsi, dan ini biasanya lebih disukai, karena Anda dapat menambahkan lebih banyak kasus dengan lebih mudah:

abs n
  | n < 0     = -n
  | otherwise =  n

case..ofadalah ketika Anda memiliki beberapa jalur kode , dan setiap jalur kode dipandu oleh struktur nilai, yaitu melalui pencocokan pola. Anda sangat jarang cocok Truedan False.

case mapping of
  Constant v -> const v
  Function f -> map f

Penjaga melengkapi case..ofekspresi, artinya jika Anda perlu membuat keputusan yang rumit bergantung pada nilai, pertama - tama buat keputusan berdasarkan struktur masukan Anda, lalu buat keputusan tentang nilai dalam struktur tersebut.

handle  ExitSuccess = return ()
handle (ExitFailure code)
  | code < 0  = putStrLn . ("internal error " ++) . show . abs $ code
  | otherwise = putStrLn . ("user error " ++)     . show       $ code

BTW. Sebagai tip gaya, selalu buat baris baru setelah a =atau sebelum a |jika barang setelah =/ |terlalu panjang untuk satu baris, atau gunakan lebih banyak baris untuk beberapa alasan lain:

-- NO!
nthElement (x:xs) a | a <= 0 = Nothing
                    | a == 1 = Just x
                    | a > 1 = nthElement xs (a-1)

-- Much more compact! Look at those spaces we didn't waste!
nthElement (x:xs) a
  | a <= 0    = Nothing
  | a == 1    = Just x
  | otherwise = nthElement xs (a-1)
dflemstr.dll
sumber
1
"Anda sangat jarang cocok Truedan False" adakah kesempatan di mana Anda akan melakukan itu? Bagaimanapun, keputusan semacam ini selalu bisa dilakukan dengan if, dan dengan penjaga juga.
kiri sekitar sekitar
2
Misalnyacase (foo, bar, baz) of (True, False, False) -> ...
dflemstr
@dflemstr Apakah tidak ada perbedaan yang lebih halus, mis. penjaga memerlukan MonadPlus dan dan mengembalikan turunan monad sementara if-then-else tidak? Tapi saya tidak yakin.
J Fritsch
2
@JFritsch: guardfungsi memerlukan MonadPlus, tetapi yang kita bicarakan di sini adalah penjaga seperti dalam | test =klausa, yang tidak terkait.
Ben Millwood
Terima kasih atas tip gayanya, sekarang dikonfirmasi oleh keraguan.
truthadjustr
22

Saya tahu ini adalah pertanyaan tentang gaya untuk fungsi rekursif secara eksplisit, tetapi saya akan menyarankan bahwa gaya terbaik adalah menemukan cara untuk menggunakan kembali fungsi rekursif yang ada.

nthElement xs n = guard (n > 0) >> listToMaybe (drop (n-1) xs)
Daniel Wagner
sumber
2

Ini hanya masalah pemesanan tapi saya rasa ini sangat mudah dibaca dan memiliki struktur yang sama dengan penjaga.

nthElement :: [a] -> Int -> Maybe a 
nthElement [] a = Nothing
nthElement (x:xs) a = if a  < 1 then Nothing else
                      if a == 1 then Just x
                      else nthElement xs (a-1)

Yang terakhir tidak perlu dan jika karena tidak ada kemungkinan lain, fungsi juga harus memiliki "kasus pilihan terakhir" jika Anda melewatkan sesuatu.

Cristian Garcia
sumber
4
Pernyataan if bersarang adalah anti-pola saat Anda dapat menggunakan pelindung kasus.
pengguna76284