Jenis Haskell vs Pembuat Data

124

Saya mempelajari Haskell dari learnyouahaskell.com . Saya kesulitan memahami konstruktor tipe dan konstruktor data. Misalnya, saya tidak begitu mengerti perbedaan antara ini:

data Car = Car { company :: String  
               , model :: String  
               , year :: Int  
               } deriving (Show) 

dan ini:

data Car a b c = Car { company :: a  
                     , model :: b  
                     , year :: c   
                     } deriving (Show)  

Saya mengerti bahwa yang pertama hanya menggunakan satu konstruktor ( Car) untuk membangun tipe data Car. Saya tidak begitu mengerti yang kedua.

Juga, bagaimana tipe data didefinisikan seperti ini:

data Color = Blue | Green | Red

cocok dengan semua ini?

Dari apa yang saya mengerti, contoh ketiga ( Color) adalah jenis yang dapat di tiga negara: Blue, Greenatau Red. Tapi itu bertentangan dengan bagaimana saya memahami dua contoh pertama: apakah tipe Carhanya bisa dalam satu keadaan Car, yang dapat mengambil berbagai parameter untuk dibangun? Jika ya, bagaimana contoh kedua cocok?

Pada dasarnya, saya mencari penjelasan yang menyatukan tiga contoh / konstruksi kode di atas.

Aristides
sumber
18
Contoh Mobil Anda mungkin sedikit membingungkan karena Carmerupakan konstruktor tipe (di sisi kiri =) dan konstruktor data (di sisi kanan). Pada contoh pertama, Carkonstruktor tipe tidak membutuhkan argumen, pada contoh kedua dibutuhkan tiga argumen. Dalam kedua contoh tersebut, Carkonstruktor data mengambil tiga argumen (tetapi tipe argumen tersebut dalam satu kasus diperbaiki dan dalam kasus lain diparameterisasi).
Simon Shine
yang pertama hanya menggunakan satu konstruktor data ( Car :: String -> String -> Int -> Car) untuk membangun tipe data Car. yang kedua hanya menggunakan satu konstruktor data ( Car :: a -> b -> c -> Car a b c) untuk membangun tipe data Car a b c.
Will Ness

Jawaban:

228

Dalam datadeklarasi, konstruktor tipe adalah benda di sisi kiri dari tanda sama dengan. The Data konstruktor (s) adalah hal-hal di sisi kanan tanda sama. Anda menggunakan konstruktor tipe di mana sebuah tipe diharapkan, dan Anda menggunakan konstruktor data di mana sebuah nilai diharapkan.

Konstruktor data

Untuk menyederhanakannya, kita bisa mulai dengan contoh tipe yang mewakili warna.

data Colour = Red | Green | Blue

Di sini, kami memiliki tiga konstruktor data. Colouradalah tipe, dan Greenmerupakan konstruktor yang berisi nilai tipe Colour. Demikian pula, Reddan Bluekeduanya merupakan konstruktor yang membangun nilai tipe Colour. Kita bisa membayangkan membumbuinya!

data Colour = RGB Int Int Int

Kami masih memiliki tipe Colour, tetapi RGBbukan nilai - ini adalah fungsi yang mengambil tiga Ints dan mengembalikan nilai! RGBmemiliki tipe

RGB :: Int -> Int -> Int -> Colour

RGBadalah konstruktor data yang merupakan fungsi yang mengambil beberapa nilai sebagai argumennya, dan kemudian menggunakannya untuk membuat nilai baru. Jika Anda pernah melakukan pemrograman berorientasi objek, Anda harus mengenali ini. Dalam OOP, konstruktor juga mengambil beberapa nilai sebagai argumen dan mengembalikan nilai baru!

Dalam hal ini, jika kita menerapkan RGBke tiga nilai, kita mendapatkan nilai warna!

Prelude> RGB 12 92 27
#0c5c1b

Kami telah membangun nilai tipe Colourdengan menerapkan konstruktor data. Konstruktor data berisi nilai seperti variabel, atau menggunakan nilai lain sebagai argumennya dan membuat nilai baru . Jika Anda telah melakukan pemrograman sebelumnya, konsep ini seharusnya tidak terlalu aneh bagi Anda.

Istirahat

Jika Anda ingin membangun pohon biner untuk menyimpan String, Anda dapat membayangkan melakukan sesuatu seperti

data SBTree = Leaf String
            | Branch String SBTree SBTree

Apa yang kita lihat di sini adalah tipe SBTreeyang berisi dua konstruktor data. Dengan kata lain, ada dua fungsi (yaitu Leafdan Branch) yang akan membentuk nilai dari SBTreetipe tersebut. Jika Anda tidak terbiasa dengan cara kerja pohon biner, bertahanlah di sana. Anda sebenarnya tidak perlu tahu cara kerja pohon biner, hanya yang satu ini menyimpan Stringdengan cara tertentu.

Kita juga melihat bahwa kedua konstruktor data mengambil Stringargumen - ini adalah String yang akan mereka simpan di pohon.

Tapi! Bagaimana jika kita juga ingin bisa menyimpan Bool, kita harus membuat pohon biner baru. Ini bisa terlihat seperti ini:

data BBTree = Leaf Bool
            | Branch Bool BBTree BBTree

Ketik konstruktor

Keduanya SBTreedan BBTreemerupakan konstruktor tipe. Tapi ada masalah yang mencolok. Apakah Anda melihat betapa miripnya mereka? Itu pertanda bahwa Anda benar-benar menginginkan parameter di suatu tempat.

Jadi kita bisa melakukan ini:

data BTree a = Leaf a
             | Branch a (BTree a) (BTree a)

Sekarang kami memperkenalkan variabel tipe a sebagai parameter ke tipe konstruktor. Dalam deklarasi ini, BTreetelah menjadi fungsi. Ini mengambil tipe sebagai argumennya dan mengembalikan tipe baru .

Penting di sini untuk mempertimbangkan perbedaan antara tipe konkret (contoh termasuk Int, [Char]dan Maybe Bool) yang merupakan tipe yang dapat ditetapkan ke nilai dalam program Anda, dan fungsi konstruktor tipe yang Anda perlukan untuk memberi makan tipe agar bisa menjadi ditetapkan ke suatu nilai. Sebuah nilai tidak pernah bisa berjenis "daftar", karena ia harus berupa "daftar sesuatu ". Dalam semangat yang sama, suatu nilai tidak pernah bisa berjenis "pohon biner", karena ia harus berupa "pohon biner yang menyimpan sesuatu ".

Jika kita meneruskan, katakanlah, Boolsebagai argumen ke BTree, ia mengembalikan tipe BTree Bool, yang merupakan pohon biner yang menyimpan Bools. Gantikan setiap kemunculan variabel tipe adengan tipe Bool, dan Anda dapat melihat sendiri bagaimana itu benar.

Jika mau, Anda dapat melihat BTreesebagai fungsi dengan jenis

BTree :: * -> *

Jenis agak mirip jenis - yang *menunjukkan jenis beton, jadi kami katakan BTreeadalah dari jenis beton ke jenis beton.

Membungkus

Mundur sejenak ke sini dan catat persamaannya.

  • Sebuah konstruktor Data adalah "fungsi" yang mengambil 0 atau lebih nilai-nilai dan memberikan Anda kembali nilai baru.

  • Sebuah tipe konstruktor adalah "fungsi" yang mengambil 0 atau lebih jenis dan memberikan Anda kembali jenis baru.

Konstruktor data dengan parameter itu keren jika kita ingin sedikit variasi dalam nilai kita - kita menempatkan variasi dalam parameter dan membiarkan orang yang membuat nilai memutuskan argumen apa yang akan mereka masukkan. Dalam pengertian yang sama, jenis konstruktor dengan parameter itu keren jika kita ingin sedikit variasi dalam tipe kita! Kami menempatkan variasi tersebut sebagai parameter dan membiarkan orang yang membuat jenis memutuskan argumen apa yang akan mereka masukkan.

Studi kasus

Sebagai peregangan rumah di sini, kita dapat mempertimbangkan Maybe ajenisnya. Definisinya adalah

data Maybe a = Nothing
             | Just a

Di sini, Maybeadalah tipe konstruktor yang mengembalikan tipe beton. Justadalah konstruktor data yang mengembalikan nilai. Nothingadalah konstruktor data yang berisi nilai. Jika kita melihat jenisnya Just, kita melihatnya

Just :: a -> Maybe a

Dengan kata lain, Justmengambil nilai tipe adan mengembalikan nilai tipe Maybe a. Jika kita melihat jenisnya Maybe, kita melihatnya

Maybe :: * -> *

Dengan kata lain, Maybemengambil tipe konkret dan mengembalikan tipe konkret.

Sekali lagi! Perbedaan antara tipe beton dan fungsi tipe konstruktor. Anda tidak dapat membuat daftar Maybes - jika Anda mencoba untuk mengeksekusi

[] :: [Maybe]

Anda akan mendapatkan kesalahan. Namun Anda dapat membuat daftar Maybe Int, atau Maybe a. Itu karena Maybemerupakan fungsi konstruktor tipe, tetapi daftar harus berisi nilai dari tipe konkret. Maybe Intdan Maybe amerupakan tipe konkret (atau jika Anda ingin, panggilan ke fungsi konstruktor tipe yang mengembalikan tipe konkret.)

kqr
sumber
2
Dalam contoh pertama Anda, baik MERAH HIJAU maupun BIRU adalah konstruktor yang tidak memerlukan argumen.
OllieB
3
Klaim bahwa dalam data Colour = Red | Green | Blue"kami tidak memiliki konstruktor sama sekali" jelas salah. Konstruktor tipe dan konstruktor data tidak perlu mengambil argumen, lihat misalnya haskell.org/haskellwiki/Constructor yang menunjukkan bahwa di data Tree a = Tip | Node a (Tree a) (Tree a), "ada dua konstruktor data, Tip dan Node".
Frerich Raabe
1
@CMCDragonkai Anda benar sekali! Jenis adalah "jenis jenis". Pendekatan umum untuk menggabungkan konsep tipe dan nilai disebut pengetikan dependen . Idris adalah bahasa yang diketik dengan ketergantungan yang terinspirasi dari Haskell. Dengan ekstensi GHC yang tepat, Anda juga bisa mendekati pengetikan dependen di Haskell. (Beberapa orang telah bercanda bahwa "Penelitian Haskell adalah tentang mencari tahu seberapa dekat dengan tipe dependen yang bisa kita dapatkan tanpa memiliki tipe dependen.")
kqr
1
@CMCDragonkai Sebenarnya tidak mungkin memiliki deklarasi data kosong di Haskell standar. Tetapi ada ekstensi GHC ( -XEmptyDataDecls) yang memungkinkan Anda melakukan itu. Karena, seperti yang Anda katakan, tidak ada nilai dengan tipe itu, sebuah fungsi f :: Int -> Zmungkin misalnya tidak pernah kembali (karena apa yang akan dikembalikannya?) Namun fungsi tersebut dapat berguna saat Anda menginginkan tipe tetapi tidak terlalu peduli dengan nilai .
kqr
1
Benarkah itu tidak mungkin? Saya baru saja mencoba di GHC, dan menjalankannya tanpa kesalahan. Saya tidak perlu memuat ekstensi GHC, cukup vanilla GHC. Saya kemudian bisa menulis :k Zdan itu memberi saya bintang.
CMCDragonkai
42

Haskell memiliki tipe data aljabar , yang sangat sedikit dimiliki bahasa lain. Ini mungkin yang membingungkan Anda.

Dalam bahasa lain, Anda biasanya dapat membuat "record", "struct" atau sejenisnya, yang memiliki sekumpulan kolom bernama yang menampung berbagai jenis data. Kadang-kadang Anda juga dapat membuat "enumerasi", yang memiliki satu set (kecil) nilai yang mungkin tetap (misalnya, Anda Red, Greendan Blue).

Di Haskell, Anda dapat menggabungkan keduanya secara bersamaan. Aneh, tapi benar!

Mengapa disebut "aljabar"? Nah, para nerd berbicara tentang "jenis jumlah" dan "jenis produk". Sebagai contoh:

data Eg1 = One Int | Two String

Sebuah Eg1nilai pada dasarnya adalah baik integer atau string. Jadi himpunan semua Eg1nilai yang mungkin adalah "jumlah" dari himpunan semua kemungkinan nilai integer dan semua kemungkinan nilai string. Jadi, para 'nerd' menyebut Eg1sebagai "tipe jumlah". Di samping itu:

data Eg2 = Pair Int String

Setiap Eg2nilai terdiri dari baik integer dan string. Jadi himpunan semua Eg2nilai yang mungkin adalah produk Cartesian dari himpunan semua bilangan bulat dan himpunan semua string. Kedua set tersebut "dikalikan" bersama-sama, jadi ini adalah "tipe produk".

Tipe aljabar Haskell adalah tipe penjumlahan dari tipe produk . Anda memberi konstruktor beberapa bidang untuk membuat tipe produk, dan Anda memiliki beberapa konstruktor untuk menjumlahkan (produk).

Sebagai contoh mengapa itu mungkin berguna, misalkan Anda memiliki sesuatu yang mengeluarkan data sebagai XML atau JSON, dan itu membutuhkan catatan konfigurasi - tetapi jelas, pengaturan konfigurasi untuk XML dan JSON sama sekali berbeda. Jadi Anda mungkin melakukan sesuatu seperti ini:

data Config = XML_Config {...} | JSON_Config {...}

(Dengan beberapa bidang yang sesuai di sana, tentunya.) Anda tidak dapat melakukan hal-hal seperti ini dalam bahasa pemrograman normal, itulah sebabnya kebanyakan orang tidak terbiasa dengannya.

MathematicalOrchid
sumber
4
Bagus! hanya satu hal, "Mereka dapat ... dibuat dalam hampir semua bahasa", kata Wikipedia . :) Dalam contoh C / ++, itu unions, dengan disiplin tag. :)
Will Ness
5
Ya, tapi setiap kali saya menyebutkan union, orang melihat saya seperti "siapa sih yang pernah menggunakan itu ??" ;-)
MathematicalOrchid
1
Saya telah melihat banyak unionpenggunaan dalam karir C. Tolong jangan membuatnya terdengar tidak perlu karena bukan itu masalahnya.
truthadjustr
26

Mulailah dengan kasus paling sederhana:

data Color = Blue | Green | Red

Ini mendefinisikan "tipe konstruktor" Coloryang tidak membutuhkan argumen - dan memiliki tiga "konstruktor data" Blue,, Greendan Red. Tidak ada konstruktor data yang membutuhkan argumen apa pun. Ini berarti bahwa ada tiga jenis Color: Blue, Greendan Red.

Sebuah konstruktor data digunakan saat Anda perlu membuat semacam nilai. Suka:

myFavoriteColor :: Color
myFavoriteColor = Green

membuat nilai myFavoriteColormenggunakan Greenkonstruktor data - dan myFavoriteColorakan berjenis Colorkarena itulah jenis nilai yang dihasilkan oleh konstruktor data.

Konstruktor tipe digunakan ketika Anda perlu membuat tipe dari beberapa jenis. Ini biasanya terjadi saat menulis tanda tangan:

isFavoriteColor :: Color -> Bool

Dalam kasus ini, Anda memanggil Colorkonstruktor tipe (yang tidak membutuhkan argumen).

Masih bersamaku?

Sekarang, bayangkan Anda tidak hanya ingin membuat nilai merah / hijau / biru tetapi Anda juga ingin menentukan "intensitas". Seperti, nilai antara 0 dan 256. Anda dapat melakukannya dengan menambahkan argumen ke setiap konstruktor data, sehingga Anda mendapatkan:

data Color = Blue Int | Green Int | Red Int

Sekarang, masing-masing dari tiga konstruktor data mengambil argumen tipe Int. Type constructor ( Color) masih tidak mengambil argumen apa pun. Jadi, warna favorit saya adalah hijau tua, saya bisa menulis

    myFavoriteColor :: Color
    myFavoriteColor = Green 50

Dan lagi, itu memanggil Greenkonstruktor data dan saya mendapatkan nilai tipe Color.

Bayangkan jika Anda tidak ingin mendikte bagaimana orang mengekspresikan intensitas warna. Beberapa mungkin menginginkan nilai numerik seperti yang baru saja kita lakukan. Orang lain mungkin baik-baik saja dengan hanya boolean yang menunjukkan "cerah" atau "tidak begitu cerah". Solusi untuk ini adalah dengan tidak melakukan hardcode Intdi konstruktor data melainkan menggunakan variabel tipe:

data Color a = Blue a | Green a | Red a

Sekarang, konstruktor tipe kami mengambil satu argumen (tipe lain yang baru saja kami panggil a!) Dan semua konstruktor data akan mengambil satu argumen (nilai!) Dari tipe itu a. Jadi Anda bisa melakukannya

myFavoriteColor :: Color Bool
myFavoriteColor = Green False

atau

myFavoriteColor :: Color Int
myFavoriteColor = Green 50

Perhatikan bagaimana kita memanggil Colorkonstruktor tipe dengan argumen (tipe lain) untuk mendapatkan tipe "efektif" yang akan dikembalikan oleh konstruktor data. Ini menyentuh konsep jenis yang mungkin ingin Anda baca sambil minum kopi.

Sekarang kita menemukan apa itu konstruktor data dan konstruktor tipe, dan bagaimana konstruktor data dapat mengambil nilai lain sebagai argumen dan konstruktor tipe dapat menggunakan tipe lain sebagai argumen. HTH.

Frerich Raabe
sumber
Saya tidak yakin saya berteman dengan gagasan Anda tentang konstruktor data nullary. Saya tahu ini adalah cara umum untuk membicarakan konstanta di Haskell, tetapi bukankah itu sudah beberapa kali terbukti salah?
kqr
@kqr: Sebuah konstruktor data bisa jadi nullary, tapi itu bukan fungsi lagi. Fungsi adalah sesuatu yang mengambil argumen dan menghasilkan nilai, yaitu sesuatu dengan ->tanda tangan.
Frerich Raabe
Bisakah sebuah nilai menunjuk ke beberapa jenis? Atau apakah setiap nilai hanya dikaitkan dengan 1 jenis dan hanya itu?
CMCDragonkai
1
@jrg Ada beberapa tumpang tindih, tetapi tidak secara spesifik karena konstruktor tipe tetapi karena variabel tipe, misalnya ain data Color a = Red a. aadalah placeholder untuk tipe arbitrer. Anda dapat memiliki yang sama dalam fungsi biasa, misalnya fungsi tipe (a, b) -> amengambil tupel dari dua nilai (tipe adan b) dan menghasilkan nilai pertama. Ini adalah fungsi "generik" yang tidak menentukan jenis elemen tupel - hanya menentukan bahwa fungsi tersebut menghasilkan nilai dengan jenis yang sama seperti elemen tupel pertama.
Frerich Raabe
1
+1 Now, our type constructor takes one argument (another type which we just call a!) and all of the data constructors will take one argument (a value!) of that type a.Ini sangat membantu.
Jonas
5

Seperti yang ditunjukkan orang lain, polimorfisme tidak terlalu berguna di sini. Mari kita lihat contoh lain yang mungkin sudah Anda kenal:

Maybe a = Just a | Nothing

Tipe ini memiliki dua konstruktor data. Nothingagak membosankan, tidak berisi data yang berguna. Di sisi lain Justmengandung nilai a- apapun jenisnya a. Mari kita tulis fungsi yang menggunakan tipe ini, misalnya mendapatkan kepala dari sebuah Intdaftar, jika ada (saya harap Anda setuju ini lebih berguna daripada membuat kesalahan):

maybeHead :: [Int] -> Maybe Int
maybeHead [] = Nothing
maybeHead (x:_) = Just x

> maybeHead [1,2,3]    -- Just 1
> maybeHead []         -- None

Jadi dalam kasus ini aadalah Int, tetapi akan bekerja dengan baik untuk jenis lainnya. Faktanya Anda dapat membuat fungsi kami berfungsi untuk setiap jenis daftar (bahkan tanpa mengubah implementasinya):

maybeHead :: [t] -> Maybe t
maybeHead [] = Nothing
maybeHead (x:_) = Just x

Di sisi lain, Anda dapat menulis fungsi yang hanya menerima jenis tertentu Maybe, misalnya

doubleMaybe :: Maybe Int -> Maybe Int
doubleMaybe Just x = Just (2*x)
doubleMaybe Nothing= Nothing

Singkat cerita, dengan polimorfisme Anda memberi tipe Anda fleksibilitas untuk bekerja dengan nilai dari tipe lain yang berbeda.

Dalam contoh Anda, Anda mungkin memutuskan di beberapa titik yang Stringtidak cukup untuk mengidentifikasi perusahaan, tetapi perlu memiliki jenisnya sendiri Company(yang menyimpan data tambahan seperti negara, alamat, akun belakang, dll). Penerapan pertama Anda dari Carperlu diubah untuk digunakan, CompanybukanString untuk nilai pertamanya. Implementasi kedua Anda baik-baik saja, Anda menggunakannya sebagaimana adanya Car Company String Intdan akan berfungsi seperti sebelumnya (tentu saja fungsi yang mengakses data perusahaan perlu diubah).

Landei
sumber
Dapatkah Anda menggunakan konstruktor tipe dalam konteks data deklarasi data lain? Sesuatu seperti data Color = Blue ; data Bright = Color? Saya mencobanya di ghci, dan tampaknya Color dalam konstruktor tipe tidak ada hubungannya dengan konstruktor data Warna dalam definisi Bright. Hanya ada 2 konstruktor Warna, yang satu adalah Data dan yang lainnya adalah Jenis.
CMCDragonkai
@CMCDragonkai Saya tidak berpikir bahwa Anda dapat melakukan ini, dan saya bahkan tidak yakin apa yang ingin Anda capai dengan ini. Anda dapat "membungkus" tipe yang sudah ada menggunakan dataatau newtype(mis. data Bright = Bright Color), Atau Anda dapat menggunakan typeuntuk mendefinisikan sinonim (mis type Bright = Color.).
Landei
5

Yang kedua memiliki pengertian "polimorfisme" di dalamnya.

The a b cdapat dari jenis apa pun. Misalnya, adapat menjadi [String], bdapat [Int] dan cdapat[Char] .

Sedangkan tipe yang pertama tetap: perusahaan adalah a String, model adalah a Stringdan tahunInt .

Contoh Mobil mungkin tidak menunjukkan pentingnya penggunaan polimorfisme. Tapi bayangkan data Anda adalah tipe daftar. Sebuah daftar bisa berisiString, Char, Int ... Dalam situasi tersebut, Anda akan membutuhkan cara kedua untuk mendefinisikan data Anda.

Untuk cara ketiga saya rasa tidak perlu menyesuaikan dengan tipe sebelumnya. Ini hanyalah satu cara lain untuk mendefinisikan data di Haskell.

Ini adalah pendapat saya yang sederhana sebagai pemula.

Btw: Pastikan Anda melatih otak Anda dengan baik dan merasa nyaman untuk ini. Itu kunci untuk memahami Monad nanti.

McBear Holden
sumber
1

Ini tentang jenis : Dalam kasus pertama, Anda mengatur jenis String(untuk perusahaan dan model) dan Intuntuk tahun. Dalam kasus kedua, Anda lebih umum. a,, bdan cmungkin jenis yang sama seperti di contoh pertama, atau sesuatu yang sama sekali berbeda. Misalnya, mungkin berguna untuk memberikan tahun sebagai string daripada integer. Dan jika Anda mau, Anda bahkan dapat menggunakan Colortipe Anda .

Matthias
sumber