Lebih baik menggunakan kesalahan monad dengan validasi dalam fungsi monadik Anda, atau mengimplementasikan monad Anda sendiri dengan validasi langsung di bind Anda?

9

Saya bertanya-tanya apa desain yang lebih baik untuk kegunaan / pemeliharaan, dan apa yang lebih baik sejauh sesuai dengan komunitas.

Diberikan model data:

type Name = String

data Amount = Out | Some | Enough | Plenty deriving (Show, Eq)
data Container = Container Name deriving (Show, Eq)
data Category = Category Name deriving (Show, Eq)
data Store = Store Name [Category] deriving (Show, Eq)
data Item = Item Name Container Category Amount Store deriving Show
instance Eq (Item) where
  (==) i1 i2 = (getItemName i1) == (getItemName i2)

data User = User Name [Container] [Category] [Store] [Item] deriving Show
instance Eq (User) where
  (==) u1 u2 = (getName u1) == (getName u2)

Saya dapat mengimplementasikan fungsi monadik untuk mengubah Pengguna misalnya dengan menambahkan item atau toko dll, tetapi saya mungkin berakhir dengan pengguna yang tidak valid sehingga fungsi monadik tersebut perlu memvalidasi pengguna yang mereka dapatkan dan atau buat.

Jadi, saya harus:

  • membungkusnya dalam kesalahan monad dan membuat fungsi monadik menjalankan validasi
  • membungkusnya dalam kesalahan monad dan membuat konsumen mengikat fungsi validasi monadik dalam urutan yang melemparkan respons kesalahan yang sesuai (sehingga mereka dapat memilih untuk tidak memvalidasi dan membawa-bawa objek pengguna yang tidak valid)
  • sebenarnya membuatnya menjadi instance bind di User secara efektif membuat jenis kesalahan saya sendiri yang mengeksekusi validasi dengan setiap bind secara otomatis

Saya dapat melihat positif dan negatif dari masing-masing dari 3 pendekatan tetapi ingin tahu apa yang lebih umum dilakukan untuk skenario ini oleh masyarakat.

Jadi dalam istilah kode seperti ini, opsi 1:

addStore s (User n1 c1 c2 s1 i1) = validate $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ someUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"]

pilihan 2:

addStore s (User n1 c1 c2 s1 i1) = Right $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ Right someUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"] >>= validate
-- in this choice, the validation could be pushed off to last possible moment (like inside updateUsersTable before db gets updated)

opsi 3:

data ValidUser u = ValidUser u | InvalidUser u
instance Monad ValidUser where
    (>>=) (ValidUser u) f = case return u of (ValidUser x) -> return f x; (InvalidUser y) -> return y
    (>>=) (InvalidUser u) f = InvalidUser u
    return u = validate u

addStore (Store s, User u, ValidUser vu) => s -> u -> vu
addStore s (User n1 c1 c2 s1 i1) = return $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ someValidUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"]
Jimmy Hoffa
sumber

Jawaban:

5

Fist saya akan bertanya pada diri sendiri: Apakah memiliki Userbug kode yang tidak valid atau situasi yang biasanya dapat terjadi (misalnya seseorang memasukkan input yang salah ke aplikasi Anda). Jika itu bug, saya akan mencoba memastikan bahwa itu tidak akan pernah terjadi (seperti menggunakan konstruktor pintar atau membuat tipe yang lebih canggih).

Jika itu skenario yang valid maka beberapa pemrosesan kesalahan selama runtime sesuai. Lalu aku bertanya: Apa benar-benar berarti bagi saya bahwa Useradalah tidak valid ?

  1. Apakah itu berarti tidak valid Userdapat membuat beberapa kode gagal? Apakah bagian dari kode Anda mengandalkan fakta bahwa a Userselalu valid?
  2. Atau apakah itu hanya berarti bahwa ini merupakan ketidakkonsistenan yang perlu diperbaiki nanti, tetapi tidak merusak apa pun selama perhitungan?

Jika 1., saya pasti akan mencari kesalahan jenis (baik standar atau milik Anda sendiri), jika tidak, Anda akan kehilangan jaminan bahwa kode Anda berfungsi dengan baik.

Membuat monad Anda sendiri atau menggunakan tumpukan transformer monad adalah masalah lain, mungkin ini akan membantu: Adakah yang pernah mengalami Monad Transformer di alam liar? .


Pembaruan: Melihat opsi yang diperluas:

  1. Tampak sebagai cara terbaik untuk pergi. Mungkin, agar benar-benar aman, saya lebih suka menyembunyikan konstruktor Userdan alih-alih hanya mengekspor beberapa fungsi yang tidak memungkinkan untuk membuat contoh yang tidak valid. Dengan cara ini Anda akan yakin bahwa setiap kali itu terjadi akan ditangani dengan benar. Misalnya, fungsi generik untuk membuat Userbisa menjadi sesuatu seperti

    user :: ... -> Either YourErrorType User
    -- more generic:
    user :: (MonadError YourErrorType m) ... -> m User
    -- Or if you actually don't need to differentiate errors:
    user :: ... -> Maybe User
    -- or more generic:
    user :: (MonadPlus m) ... -> m User
    -- etc.
    

    Banyak perpustakaan mengambil pendekatan yang serupa, misalnya Map, Setatau Seqmenyembunyikan implementasi yang mendasarinya sehingga tidak mungkin untuk membuat struktur yang tidak mematuhi invarian mereka.

  2. Jika Anda menunda validasi sampai akhir, dan gunakan di Right ...mana-mana, Anda tidak perlu monad lagi. Anda bisa melakukan perhitungan murni dan menyelesaikan kemungkinan kesalahan di bagian akhir. IMHO pendekatan ini sangat berisiko, karena nilai pengguna yang tidak valid dapat menyebabkan memiliki data yang tidak valid di tempat lain, karena Anda tidak segera menghentikan perhitungan. Dan, jika itu terjadi bahwa beberapa metode lain memperbarui pengguna sehingga valid lagi, Anda akan berakhir dengan memiliki data yang tidak valid di suatu tempat dan bahkan tidak mengetahuinya.

  3. Ada beberapa masalah di sini.

    • Yang paling penting adalah bahwa monad harus menerima parameter tipe apa pun, bukan hanya User. Jadi, Anda validateharus mengetik u -> ValidUser utanpa batasan u. Jadi tidak mungkin untuk menulis monad seperti itu yang memvalidasi input return, karena returnharus sepenuhnya polimorfik.
    • Selanjutnya, apa yang saya tidak mengerti adalah bahwa Anda cocok case return u ofdalam definisi >>=. Poin utama ValidUserseharusnya adalah untuk membedakan nilai yang valid dan tidak valid, dan karenanya monad harus memastikan bahwa ini selalu benar. Jadi bisa jadi sederhana saja

      (>>=) (ValidUser u) f = f u
      (>>=) (InvalidUser u) f = InvalidUser u
      

    Dan ini sudah sangat mirip Either.

Secara umum, saya akan menggunakan monad khusus hanya jika

  • Tidak ada monad yang menyediakan fungsionalitas yang Anda butuhkan. Monad yang ada biasanya memiliki banyak fungsi pendukung, dan yang lebih penting, mereka memiliki transformer monad sehingga Anda dapat menyusunnya menjadi tumpukan monad.
  • Atau jika Anda membutuhkan monad yang terlalu rumit untuk digambarkan sebagai tumpukan monad.
Petr Pudlák
sumber
Dua poin terakhir Anda sangat berharga dan saya tidak memikirkannya! Pasti kebijaksanaan yang saya cari, terima kasih sudah berbagi pemikiran, saya pasti akan memilih # 1!
Jimmy Hoffa
Baru saja mengikat seluruh modul tadi malam dan Anda benar. Saya memunculkan metode validasi saya ke sejumlah kecil kombinator kunci saya telah melakukan semua pembaruan model dan itu sebenarnya jauh lebih masuk akal seperti ini. Saya benar-benar akan pergi setelah # 3 dan sekarang saya melihat bagaimana ... tidak fleksibel pendekatan itu, jadi terima kasih satu ton untuk meluruskan saya!
Jimmy Hoffa