Sebagai seorang praktisi, mengapa saya harus peduli dengan Haskell? Apa itu monad dan mengapa saya membutuhkannya? [Tutup]

9

Saya hanya tidak mendapatkan masalah apa yang mereka pecahkan.

Pekerjaan
sumber
1
Hampir merupakan duplikat dari: programmers.stackexchange.com/questions/25569/…
Orbling
2
Saya pikir suntingan ini agak ekstrem. Saya pikir pertanyaan Anda pada dasarnya adalah pertanyaan yang bagus. Hanya saja beberapa bagiannya sedikit ... argumentatif. Yang mungkin hanya merupakan hasil dari frustrasi mencoba mempelajari sesuatu yang tidak Anda pahami.
Jason Baker
@SnOrfus, akulah yang mengajukan pertanyaan itu. Saya terlalu malas untuk mengeditnya dengan benar.
Pekerjaan

Jawaban:

34

Monad tidak baik atau buruk. Mereka memang begitu. Mereka alat yang digunakan untuk memecahkan masalah seperti banyak konstruksi bahasa pemrograman lainnya. Satu aplikasi yang sangat penting dari mereka adalah untuk membuat hidup lebih mudah bagi programmer yang bekerja dalam bahasa yang murni fungsional. Tetapi mereka berguna dalam bahasa non-fungsional; hanya saja orang jarang menyadari bahwa mereka menggunakan Monad.

Apa itu Monad? Cara terbaik untuk memikirkan Monad adalah sebagai pola desain. Dalam kasus I / O, Anda mungkin bisa menganggapnya sebagai sedikit lebih dari pipa yang dimuliakan di mana negara global adalah apa yang dilewati di antara tahapan.

Misalnya, mari kita ambil kode yang Anda tulis:

do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

Ada banyak hal yang terjadi di sini daripada yang terlihat. Misalnya, Anda akan melihat bahwa putStrLnmemiliki tanda tangan berikut: putStrLn :: String -> IO (). Kenapa ini?

Pikirkan seperti ini: mari kita berpura-pura (demi kesederhanaan) bahwa stdout dan stdin adalah satu-satunya file yang dapat kita baca dan tulis. Dalam bahasa imperatif, ini bukan masalah. Tetapi dalam bahasa fungsional, Anda tidak dapat mengubah keadaan global. Fungsi hanyalah sesuatu yang mengambil nilai (atau nilai) dan mengembalikan nilai (atau nilai). Salah satu cara mengatasi hal ini adalah dengan menggunakan negara global sebagai nilai yang masuk dan keluar dari setiap fungsi. Jadi, Anda bisa menerjemahkan baris kode pertama menjadi sesuatu seperti ini:

global_state <- (\(stdin, stdout) -> (stdin, stdout ++ "What is your name?")) global_state

... dan kompiler akan tahu untuk mencetak apa pun yang ditambahkan ke elemen kedua global_state. Sekarang saya tidak tahu tentang Anda, tetapi saya akan benci untuk memprogram seperti itu. Cara ini dibuat lebih mudah adalah dengan menggunakan Monads. Di Monad, Anda meneruskan nilai yang mewakili semacam keadaan dari satu tindakan ke tindakan berikutnya. Inilah sebabnya mengapa putStrLnmemiliki jenis pengembalian IO (): mengembalikan negara global baru.

Jadi mengapa kamu peduli? Nah, kelebihan pemrograman fungsional daripada program imperatif telah diperdebatkan sampai mati di beberapa tempat, jadi saya tidak akan menjawab pertanyaan itu secara umum (tetapi lihat makalah ini jika Anda ingin mendengar kasus pemrograman fungsional). Namun untuk kasus khusus ini, mungkin membantu jika Anda memahami apa yang ingin dicapai Haskell.

Banyak programmer merasa bahwa Haskell mencoba untuk mencegah mereka dari menulis kode penting atau menggunakan efek samping. Itu tidak sepenuhnya benar. Pikirkan seperti ini: bahasa imperatif adalah bahasa yang memungkinkan efek samping secara default, tetapi memungkinkan Anda untuk menulis kode fungsional jika Anda benar-benar ingin (dan bersedia untuk berurusan dengan beberapa contortions yang akan memerlukan). Haskell murni berfungsi secara default, tetapi memungkinkan Anda untuk menulis kode penting jika Anda benar-benar ingin (yang Anda lakukan jika program Anda berguna). Intinya bukan untuk membuatnya sulit untuk menulis kode yang memiliki efek samping. Ini untuk memastikan Anda secara eksplisit memiliki efek samping (dengan sistem tipe yang memberlakukan ini).

Jason Baker
sumber
6
Paragraf terakhir itu adalah emas. Untuk mengekstrak dan memparafrasekannya sedikit: "Bahasa imperatif adalah bahasa yang memungkinkan efek samping secara default, tetapi memungkinkan Anda untuk menulis kode fungsional jika Anda benar-benar menginginkannya. Bahasa fungsional murni berfungsi secara default, tetapi memungkinkan Anda untuk menulis kode imperatif jika Anda benar-benar mau. "
Frank Shearar
Perlu dicatat bahwa makalah yang Anda tautkan secara khusus menolak gagasan "kekekalan sebagai kebajikan pemrograman fungsional" tepat di awal.
Mason Wheeler
@MasonWheeler: Saya membaca paragraf-paragraf itu, bukan sebagai mengabaikan pentingnya keabadian, tetapi menolaknya sebagai argumen yang meyakinkan untuk menunjukkan superioritas pemrograman fungsional. Bahkan, ia mengatakan hal yang sama tentang penghapusan goto(sebagai argumen untuk pemrograman terstruktur) sedikit kemudian di koran, mengkarakterisasi argumen seperti "sia-sia." Namun tidak satupun dari kita diam-diam berharap untuk gotokembali. Hanya saja Anda tidak bisa berdebat yang gototidak perlu bagi orang yang menggunakannya secara luas.
Robert Harvey
7

Saya akan menggigit !!! Monads sendiri sebenarnya bukan raison d'etre untuk Haskell (versi awal Haskell bahkan tidak memilikinya).

Pertanyaan Anda agak seperti mengatakan "C ++ ketika saya melihat sintaks, saya bosan. Tapi template adalah fitur yang sangat diiklankan dari C ++ jadi saya melihat implementasi dalam bahasa lain".

Evolusi seorang programmer Haskell adalah sebuah lelucon, itu tidak dimaksudkan untuk dianggap serius.

Monad untuk tujuan program di Haskell adalah turunan dari kelas tipe Monad, artinya, itu tipe yang terjadi untuk mendukung serangkaian kecil operasi tertentu. Haskell memiliki dukungan khusus untuk tipe yang mengimplementasikan kelas tipe Monad, khususnya dukungan sintaksis. Praktis apa hasil ini adalah apa yang telah disebut sebagai "semi-colon yang dapat diprogram". Ketika Anda menggabungkan fungsi ini dengan beberapa fitur Haskell lainnya (fungsi kelas satu, malas secara default) yang Anda dapatkan adalah kemampuan untuk mengimplementasikan hal-hal tertentu sebagai perpustakaan yang secara tradisional dianggap sebagai fitur bahasa. Misalnya, Anda dapat menerapkan mekanisme pengecualian. Anda dapat menerapkan dukungan untuk kelanjutan dan coroutine sebagai perpustakaan. Haskell, bahasa tidak memiliki dukungan untuk variabel yang bisa berubah:

Anda bertanya tentang "Mungkin / Identity / Safe Division monads ???". Mungkin monad adalah contoh bagaimana Anda menerapkan (sangat sederhana, hanya satu pengecualian) penanganan pengecualian sebagai perpustakaan.

Anda benar, menulis pesan dan membaca input pengguna tidak terlalu unik. IO adalah contoh buruk "monad sebagai fitur".

Tetapi untuk beralih, satu "fitur" dengan sendirinya (misalnya Monads) dalam isolasi dari sisa bahasa tidak serta merta langsung tampak berguna (fitur baru C ++ 0x yang hebat adalah referensi nilai, tidak berarti Anda dapat mengambil mereka keluar dari konteks C ++ karena sintaks membuat Anda bosan dan perlu melihat utilitas). Bahasa pemrograman bukanlah sesuatu yang Anda dapatkan dengan melemparkan banyak fitur ke dalam sebuah ember.

Logan Capaldo
sumber
Sebenarnya, haskell memang memiliki dukungan untuk variabel yang bisa berubah melalui ST monad (salah satu dari beberapa bagian magis yang tidak murni dan aneh dari bahasa yang dimainkan oleh aturannya sendiri).
sara
4

Pemrogram semua menulis program, tetapi kesamaan berakhir di sana. Saya pikir programmer berbeda jauh lebih dari kebanyakan programmer bisa bayangkan. Ambil "pertempuran" lama, seperti mengetik variabel statis vs tipe runtime saja, skrip vs dikompilasi, gaya-C vs berorientasi objek. Anda akan merasa mustahil untuk secara rasional berpendapat bahwa satu kubu lebih rendah, karena beberapa dari mereka mengeluarkan kode yang sangat baik dalam beberapa sistem pemrograman yang tampaknya tidak berguna atau bahkan tidak dapat digunakan oleh saya.

Saya pikir orang yang berbeda hanya berpikir secara berbeda, dan jika Anda tidak tergoda oleh gula sintaksis atau terutama abstraksi yang hanya ada untuk kenyamanan dan benar-benar memiliki biaya runtime yang signifikan, maka dengan segala cara menjauh dari bahasa tersebut.

Namun saya akan merekomendasikan bahwa Anda setidaknya mencoba membiasakan diri dengan konsep yang Anda menyerah. Saya tidak membenci seseorang yang sangat pro-murni-C, asalkan mereka benar-benar mengerti apa pentingnya ekspresi lambda. Saya curiga bahwa sebagian besar dari mereka tidak akan langsung menjadi penggemar, tetapi paling tidak itu akan ada di benak mereka ketika mereka menemukan masalah sempurna apa yang akan terjadi, jadi jauh lebih mudah untuk diselesaikan dengan lambda.

Dan, di atas semua itu, cobalah untuk tidak merasa terganggu oleh ucapan fanboy, terutama oleh orang-orang yang tidak benar-benar tahu apa yang mereka bicarakan.

Roman Starkov
sumber
4

Haskell memberlakukan Transparansi Referensial : diberikan parameter yang sama, setiap fungsi selalu mengembalikan hasil yang sama, tidak peduli berapa kali Anda memanggil fungsi itu.

Itu berarti, misalnya, bahwa pada Haskell (dan tanpa Monads) Anda tidak dapat mengimplementasikan generator nomor acak. Di C ++ atau Java Anda bisa melakukannya menggunakan variabel global, menyimpan nilai "seed" perantara dari generator acak.

Di Haskell, mitra variabel global adalah Monads.

vz0
sumber
Jadi ... bagaimana jika Anda menginginkan generator nomor acak? Bukankah itu berfungsi juga? Bahkan jika tidak, bagaimana cara mendapatkan generator nomor acak?
Pekerjaan
@Job Anda dapat membuat generator angka acak di dalam monad (yang pada dasarnya adalah pelacak negara), atau Anda dapat menggunakan unsafePerformIO, iblis Haskell yang seharusnya tidak pernah digunakan (dan pada kenyataannya mungkin akan merusak program Anda jika Anda menggunakan keacakan di dalamnya!)
alternatif
Di Haskell, Anda juga membagikan 'RandGen' yang pada dasarnya adalah kondisi RNG saat ini. Jadi fungsi yang menghasilkan nomor acak baru mengambil RandGen, dan mengembalikan tuple dengan RandGen baru dan nomor yang dihasilkan. Alternatifnya adalah menentukan di suatu tempat yang Anda inginkan daftar angka acak antara nilai min dan maksimum. Ini akan mengembalikan aliran angka tak terbatas yang dievaluasi malas, jadi kita bisa menelusuri daftar ini setiap kali kita membutuhkan nomor acak baru.
Qqwy
Cara yang sama Anda mendapatkannya dalam bahasa lain! Anda mendapatkan beberapa algoritma penghasil angka pseudo-acak, dan kemudian Anda menyemainya dengan beberapa nilai dan menuai angka "acak" yang muncul! Satu-satunya perbedaan adalah bahwa bahasa seperti C # dan Java secara otomatis menaburkan PRNG untuk Anda menggunakan jam sistem atau hal-hal seperti itu. Itu dan fakta bahwa di haskell Anda juga mendapatkan PRNG baru yang dapat Anda gunakan untuk mendapatkan nomor "berikutnya", sedangkan di C # / Java, ini semua dilakukan secara internal menggunakan variabel yang bisa berubah dalam Randomobjek.
sara
4

Agak pertanyaan lama tapi itu benar-benar bagus, jadi saya akan jawab.

Anda dapat menganggap monads sebagai blok kode yang Anda miliki kendali penuh atas cara mereka dieksekusi: apa yang harus dikembalikan setiap baris kode, apakah eksekusi harus berhenti pada titik mana pun, apakah beberapa pemrosesan lain harus terjadi di antara setiap baris.

Saya akan memberikan beberapa contoh hal-hal yang dimungkinkan oleh monad yang akan sulit sebaliknya. Tak satu pun dari contoh-contoh ini ada di Haskell, hanya karena pengetahuan Haskell saya agak goyah, tetapi mereka semua adalah contoh bagaimana Haskell mengilhami penggunaan monad.

Parser

Biasanya, jika Anda ingin menulis semacam parser, katakanlah untuk mengimplementasikan bahasa pemrograman, Anda harus membaca spesifikasi BNF dan menulis sejumlah besar kode gila untuk menguraikannya, atau Anda harus menggunakan kompiler kompiler seperti Flex, Bison, yacc dll. Tetapi dengan monads, Anda dapat membuat semacam "kompiler parser" tepat di Haskell.

Parser tidak dapat dilakukan tanpa monad atau bahasa tujuan khusus seperti yacc, bison dll.

Misalnya, saya mengambil spesifikasi bahasa BNF untuk protokol IRC :

message    =  [ ":" prefix SPACE ] command [ params ] crlf
prefix     =  servername / ( nickname [ [ "!" user ] "@" host ] )
command    =  1*letter / 3digit
params     =  *14( SPACE middle ) [ SPACE ":" trailing ]
           =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]

nospcrlfcl =  %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
                ; any octet except NUL, CR, LF, " " and ":"
middle     =  nospcrlfcl *( ":" / nospcrlfcl )
trailing   =  *( ":" / " " / nospcrlfcl )

SPACE      =  %x20        ; space character
crlf       =  %x0D %x0A   ; "carriage return" "linefeed"

Dan mengubahnya menjadi sekitar 40 baris kode dalam F # (yang merupakan bahasa lain yang mendukung monads):

type UserIdentifier = { Name : string; User: string; Host: string }

type Message = { Prefix : UserIdentifier option; Command : string; Params : string list }

let space = character (char 0x20)

let parameters =
    let middle = parser {
        let! c = sat <| fun c -> c <> ':' && c <> (char 0x20)
        let! cs = many <| sat ((<>)(char 0x20))
        return (c::cs)
    }
    let trailing = many item
    let parameter = prefixed space ((prefixed (character ':') trailing) +++ middle)
    many parameter

let command = atLeastOne letter +++ (count 3 digit)

let prefix = parser {
    let! name = many <| sat (fun c -> c <> '!' && c <> '@' && c <> (char 0x20))   //this is more lenient than RFC2812 2.3.1
    let! uh = parser {
        let! user = maybe <| prefixed (character '!') (many <| sat (fun c -> c <> '@' && c <> (char 0x20)))
        let! host = maybe <| prefixed (character '@') (many <| sat ((<>) ' '))
        return (user, host)
    }
    let nullstr = function | Some([]) -> null | Some(s) -> charsString s | _ -> null
    return { Name = charsString name; User = nullstr (fst uh); Host = nullstr (snd uh) }
}

let message = parser {
    let! p = maybe (parser {
        let! _ = character ':'
        let! p = prefix
        let! _ = space
        return p
    })
    let! c = command
    let! ps = parameters
    return { Prefix = p; Command = charsString c; Params = List.map charsString ps }
}

Sintaks monad F # sangat jelek dibandingkan dengan Haskell, dan saya mungkin bisa sedikit memperbaiki ini - tetapi intinya untuk dibawa pulang adalah secara struktural, kode parser identik dengan BNF. Tidak hanya ini akan membutuhkan lebih banyak pekerjaan tanpa monad (atau generator parser), itu akan hampir tidak memiliki kemiripan dengan spesifikasi, dan dengan demikian mengerikan untuk dibaca dan dipelihara.

Multitasking khusus

Biasanya, multitasking dianggap sebagai fitur OS - tetapi dengan monads, Anda dapat menulis penjadwal Anda sendiri sehingga setelah setiap instruksi monad, program akan memberikan kontrol ke penjadwal, yang kemudian akan memilih monad lain untuk dieksekusi.

Seorang pria membuat monad "tugas" untuk mengontrol loop game (sekali lagi dalam F #), sehingga daripada harus menulis semuanya sebagai mesin negara yang bekerja pada setiap Update()panggilan, ia hanya dapat menulis semua instruksi seolah-olah itu adalah fungsi tunggal .

Dengan kata lain, alih-alih harus melakukan sesuatu seperti:

class Robot
{
   enum State { Walking, Shooting, Stopped }

   State state = State.Stopped;

   public void Update()
   {
      switch(state)
      {
         case State.Stopped:
            Walk();
            state = State.Walking;
            break;
         case State.Walking:
            if (enemyInSight)
            {
               Shoot();
               state = State.Shooting;
            }
            break;
      }
   }
}

Anda dapat melakukan sesuatu seperti:

let robotActions = task {
   while (not enemyInSight) do
      Walk()
   while (enemyInSight) do
      Shoot()
}

LINQ ke SQL

LINQ to SQL sebenarnya adalah contoh dari monad, dan fungsi serupa dapat dengan mudah diimplementasikan di Haskell.

Saya tidak akan membahas detailnya karena saya tidak mengingat semua itu dengan akurat, tetapi Erik Meijer menjelaskannya dengan cukup baik .

Rei Miyasaka
sumber
1

Jika Anda terbiasa dengan pola GoF, monad seperti pola dekorator dan Builder digabungkan, pada steroid, digigit oleh musang radioaktif.

Ada jawaban yang lebih baik di atas, tetapi beberapa manfaat spesifik yang saya lihat adalah:

  • Monads menghias beberapa tipe inti dengan properti tambahan tanpa mengubah tipe inti. Misalnya, monad mungkin "mengangkat" String dan menambahkan nilai seperti "isWellFormed", "isProfanity" atau "isPalindrome" dll.

  • sama halnya, monad memungkinkan konglomerasi tipe sederhana menjadi tipe koleksi

  • Monad memungkinkan pengikatan fungsi yang terlambat ke ruang tingkat tinggi ini

  • monads memungkinkan pencampuran fungsi dan argumen sewenang-wenang dengan tipe data sewenang-wenang, di ruang orde tinggi

  • monads memungkinkan memadukan fungsi murni, stateless dengan basis yang tidak murni, stateful, sehingga Anda dapat melacak di mana masalahnya

Contoh akrab dari monad di Jawa adalah Daftar. Dibutuhkan beberapa kelas inti, seperti String, dan "mengangkat" ke ruang monad Daftar, menambahkan informasi tentang daftar. Kemudian ia mengikat fungsi-fungsi baru ke dalam ruang itu seperti get (), getFirst (), add (), empty (), dll.

Pada skala besar, bayangkan bahwa alih-alih menulis program, Anda hanya menulis Builder besar (sebagai pola GoF), dan metode build () pada akhirnya memuntahkan jawaban apa pun yang seharusnya dihasilkan oleh program. Dan Anda dapat menambahkan metode baru ke ProgramBuilder Anda tanpa mengkompilasi ulang kode asli. Itu sebabnya monad adalah model desain yang kuat.

rampok
sumber