Newtype kekuatan besar

Newtype adalah deklarasi tipe data khusus. Sehingga hanya berisi satu konstruktor dan bidang.

newtype Foo a = Bar a newtype Id = MkId Word 


Pertanyaan Pemula yang Umum


Apa perbedaan dari data tipe data?

 data Foo a = Bar a data Id = MkId Word 

Spesifisitas utama dari tipe baru adalah bahwa ia terdiri dari bagian yang sama dengan satu-satunya bidang. Lebih tepatnya, ini berbeda dari yang asli di tingkat tipe, tetapi memiliki representasi yang sama dalam memori, dan dihitung secara ketat (tidak malas).
Singkatnya - jenis baru lebih efektif karena presentasi mereka.

Ya, itu tidak ada artinya bagi saya ... Saya akan menggunakan data
Tidak, well, pada akhirnya, Anda selalu dapat mengaktifkan bidang ekstensi -funpack-strict- :) untuk bidang ketat (tidak malas) atau tentukan secara langsung

 data Id = MkId !Word 

Namun, kekuatan tipe baru tidak terbatas pada efisiensi komputasi. Mereka jauh lebih kuat!

3 peran tipe baru




Menyembunyikan Implementasi


 module Data.Id (Id()) where newtype Id = MkId Word 

newtype berbeda dari yang asli, hanya Word secara internal.
Tapi kami menyembunyikan konstruktor MkId di luar modul.

Implementasi implementasi


 {-# LANGUAGE GeneralizedNewtypeDeriving #-} newtype Id = MkId Word deriving (Num, Eq) 

Meskipun ini tidak dalam standar Haskell2010, dengan memperluas output tipe baru generik, Anda dapat secara otomatis menyimpulkan perilaku tipe baru sama dengan perilaku bidang internal. Dalam kasus kami, perilaku Eq Id dan Num Id sama dengan Eq Word dan Num Word .

Jauh lebih banyak yang bisa dicapai melalui perluasan pemuliaan yang disempurnakan ( DerivingVia ), tetapi lebih banyak tentang itu nanti.

Implementasi pilihan


Terlepas dari konstruktornya sendiri, dalam beberapa kasus Anda dapat menggunakan representasi internal Anda.

Tantangan


Ada daftar bilangan bulat. Temukan jumlah maksimum dan total hanya dalam satu pass pada daftar.
Dan jangan gunakan paket foldl and folds .

Jawaban khas


Tentu saja, lipat ! :)

 foldr :: Foldable t => (a -> b -> b) -> b -> ta -> b {- -- instance Foldable [] foldr :: (a -> b -> b) -> b -> [a] -> b -} 

Dan, fungsi terakhir dijelaskan seperti ini:

 aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = foldr (\el (m, s) -> (Just el `max` m, el + s)) (Nothing, 0) {- ghci> aggregate [1, 2, 3, 4] (Just 4, 10) -} 

Jika Anda melihat lebih dekat, Anda dapat melihat operasi serupa di kedua sisi: Hanya el `max` m dan el + s . Dalam kedua kasus, pemetaan dan operasi biner. Dan elemen yang kosong adalah Nothing dan 0 .

Ya, ini adalah monoids!

Monoid dan Semigroup lebih terinci
Semigroup adalah properti dari operasi biner asosiatif

 x β‹„ (y β‹„ z) == (x β‹„ y) β‹„ z 

Monoid adalah properti dari operasi asosiatif (mis., Semigroup)

 x β‹„ (y β‹„ z) == (x β‹„ y) β‹„ z 

yang memiliki elemen kosong yang tidak mengubah elemen apa pun di kanan atau di sebelah kiri

 x β‹„ empty == x == empty β‹„ x 


Maks dan (+) keduanya asosiatif, keduanya memiliki elemen kosong - Tidak ada dan 0 .

Dan kombinasi pemetaan monoids bersama dengan konvolusi Lipat !

Lipat lebih detail
Ingat definisi lipat:

 class Foldable t where foldMap :: (Monoid m) => (a -> m) -> ta -> m ... 


Mari kita terapkan perilaku lipat ke max dan (+) . Kami dapat mengatur tidak lebih dari satu implementasi monoid Word . Saatnya mengambil keuntungan dari penerapan pilihan jenis baru!

 {-# LANGUAGE GeneralizedNewtypeDeriving #-} -- already in Data.Semigroup & Data.Monoid newtype Sum a = Sum {getSum :: a} deriving (Num, Eq, Ord) instance (Num a, Ord a) => Semigroup (Sum a) where (<>) = (+) instance (Num a, Ord a) => Monoid (Sum a) where mempty = Sum 0 newtype Max a = Max {getMax :: a} deriving (Num, Eq, Ord) instance (Num a, Ord a) => Semigroup (Max a) where (<>) = max 

Perlu untuk membuat komentar.

Faktanya adalah bahwa untuk menjadi monoid untuk tipe data Max , kita membutuhkan elemen minimal, yaitu, agar elemen kosong ada. Jadi, hanya Max a yang terbatas yang bisa menjadi monoid.

Secara teori benar maksimum elemen monoid
 newtype Max a = Max a instance Ord a => Semigroup (Max a) instance Bounded a => Monoid (Max a) 


Jadi entah bagaimana kita harus mengubah tipe data kita sehingga elemen kosong muncul dan kita dapat menggunakan koagulasi.

 -- already in Prelude data Maybe a = Nothing | Just a instance Semigroup a => Semigroup (Maybe a) where Nothing <> b = b b <> Nothing = b (Just a) <> (Just b) = Just (a <> b) instance Semigroup a => Monoid (Maybe a) where mempty = Nothing -- ------ instance Functor Maybe where fmap _ Nothing = Nothing fmap f (Just b) = Just (fb) 

Unsur konjugat Mungkin mengubah semi-grup menjadi monoid!

Liberalisasi pembatasan dalam versi terbaru GHC
Kembali di GHC 8.2, diperlukan batasan tipe monoid

 instance Monoid a => Monoid (Maybe a) 

yang berarti kami membutuhkan jenis baru lainnya:

 -- already in Data.Semigroup & Data.Monoid newtype Option a = Option {getOption :: Maybe a} deriving (Eq, Ord, Semigroup) instance (Ord a, Semigroup a) => Monoid (Option a) where mempty = Option Nothing 

Dan itu sudah jauh lebih sederhana di GHC 8.4, di mana hanya diperlukan semigroup dalam pembatasan jenis, dan bahkan tidak perlu membuat jenis Opsi.

 instance Semigroup a => Monoid (Maybe a) 


Respon Lipat


Nah, sekarang perbarui kode menggunakan collapsibility dan panah.
Kami ingat bahwa (.) Hanya komposisi fungsional:

  (.) :: (b -> c) -> (a -> b) -> a -> c f . g = \x -> f (gx) 

Dan ingat bahwa fmap adalah functor:

 fmap :: Functor f => (a -> b) -> fa -> fb 

dan implementasinya untuk Maybe dijelaskan sedikit lebih tinggi.

Panah lebih detail
Panah adalah properti dari beberapa fungsi yang memungkinkan Anda untuk bekerja dengannya dalam diagram blok.
Lebih detail dapat ditemukan di sini: Panah: Antarmuka Umum ke Komputasi
Dalam kasus kami, kami menggunakan fungsi Panah
Yaitu

 instance Arrow (->) 

Kami akan menggunakan fungsi:

 (***) :: Arrow a => abc -> ab' c' -> a (b, b') (c, c') (&&&) :: Arrow a => abc -> abc' -> ab (c, c') 

Untuk kasus kami
 abc == (->) bc == b -> c 

Dan, karenanya, tanda tangan dari fungsi kami dikurangi menjadi:

 (***) :: (b -> c) -> (b' -> c') -> ((b, b') -> (c, c')) (&&&) :: (b -> c) -> (b -> c') -> (b -> (c, c')) 

Atau dengan kata-kata yang cukup sederhana, fungsi (***) menggabungkan dua fungsi dengan satu argumen (dan satu tipe output) ke dalam fungsi dengan kerja sepasang argumen pada input dan pada output, masing-masing, sepasang tipe output.

Fungsi (&&&) adalah versi singkat (***) , di mana tipe argumen input dari kedua fungsi adalah sama, dan pada input kita tidak memiliki sepasang argumen, tetapi satu argumen.

Total, fungsi pemersatu telah memperoleh formulir:

 import Data.Semigroup import Data.Monoid import Control.Arrow aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = (fmap getMax *** getSum) . (foldMap (Just . Max &&& Sum)) {- -- for GHC 8.2 aggregate = (fmap getMax . getOption *** getSum) . (foldMap (Option . Just . Max &&& Sum)) -} 

Ternyata sangat singkat!

Namun, masih melelahkan untuk membungkus dan membalikkan data dari tipe bersarang!
Anda dapat menguranginya lebih jauh, dan konversi paksa bebas sumber daya akan membantu kami!

Konversi paksa yang aman dan bebas sumber daya dan mengetikkan peran


Ada fungsi dari paket Unsafe.Coerce - unsafeCoerce

 import Unsafe.Coerce(unsafeCoerce) unsafeCoerce :: a -> b 

Fungsi secara paksa tidak aman mengubah tipe: dari a ke b .
Bahkan, fungsinya ajaib, ia memberitahu kompiler untuk mempertimbangkan data tipe a sebagai tipe b , tanpa mempertimbangkan konsekuensi dari langkah ini.

Ini dapat digunakan untuk mengonversi tipe bersarang, tetapi Anda harus sangat berhati-hati.

Pada 2014, ada revolusi dengan tipe baru , yaitu konversi yang aman dan tanpa sumber daya!

 import Data.Coerce(coerce) coerce :: Coercible ab => a -> b 

Fitur ini telah membuka era baru dalam bekerja dengan tipe baru .

Coercible Forced Converter bekerja dengan tipe yang memiliki struktur yang sama dalam memori. Itu terlihat seperti kelas tipe, tetapi pada kenyataannya GHC mengkonversi tipe pada waktu kompilasi dan tidak mungkin untuk mendefinisikan instance sendiri.
Fungsi Data.Coerce.coerce memungkinkan Anda untuk mengkonversi tipe tanpa sumber daya, tetapi untuk ini kita perlu memiliki akses ke konstruktor tipe.

Sekarang sederhanakan fungsi kami:

 import Data.Semigroup import Data.Monoid import Control.Arrow import Data.Coerce aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = coerce . (foldMap (Just . Max &&& Sum)) -- coerce :: (Maybe (Max Integer), Sum Integer) -> (Maybe Integer, Integer) 

Kami menghindari rutinitas menarik tipe bersarang, kami melakukan ini tanpa menghabiskan sumber daya hanya dengan satu fungsi.

Peran Tipe Data Bersarang


Dengan fungsi paksaan, kami dapat memaksa konversi dari semua tipe bersarang.
Tetapi apakah perlu menggunakan fitur ini secara luas?

 -- already in Data.Ord -- Down a - reversed order newtype Down a = Down a deriving (Eq, Show) instance Ord a => Ord (Down a) where compare (Down x) (Down y) = y `compare` x import Data.List(sort) -- Sorted data Sorted a = Sorted [a] deriving (Show, Eq, Ord) fromList2Sorted :: Ord a => [a] -> Sorted a fromList2Sorted = Sorted . sort -- minimum: O(1) ! minView :: Sorted a -> Maybe a minView (Sorted []) = Nothing minView (Sorted (a : _)) = Just a 

Secara semantik, tidak masuk akal untuk mengonversi ke Sorted a from Sorted (Down a) .
Namun, Anda dapat mencoba:

 ghci> let h = fromList2Sorted [1,2,3] :: Sorted Int ghci> let hDown = fromList2Sorted $ fmap Down [1,2,3] :: Sorted (Down Int) ghci> minView h Just (Down 1) ghci> minView (coerce h :: Sorted (Down Int)) Just (Down 1) ghci> minView hDown Just (Down 3) 

Semuanya akan baik-baik saja, tetapi jawaban yang benar adalah Just (Down 3) .
Yaitu, untuk memotong perilaku yang salah, ketik peran diperkenalkan.

 {-# LANGUAGE RoleAnnotations #-} type role Sorted nominal 

Mari kita coba sekarang:

 ghci> minView (coerce h :: Sorted (Down Int)) error: Couldn't match type 'Int' with 'Down Int' arising from a use of 'coerce' 

Secara signifikan lebih baik!

Total ada 3 peran ( tipe peran ):

  • representasional - setara jika representasi yang sama
  • nominal - harus memiliki tipe yang persis sama
  • phantom - independen dari konten nyata. Setara dengan apa pun

Dalam kebanyakan kasus, kompiler cukup pintar untuk mengungkapkan peran tipe, tetapi dapat membantu.

Perilaku Deriving DerivingVia yang Dimurnikan


Berkat perluasan bahasa DerivingVia, peran distribusi tipe baru telah meningkat.

Dimulai dengan GHC 8.6, yang baru-baru ini dirilis, ekstensi baru ini telah muncul.

 {-# LANGUAGE DerivingVia #-} newtype Id = MkId Word deriving (Semigroup, Monoid) via Max Word 

Seperti yang Anda lihat, perilaku tipe disimpulkan secara otomatis karena penyempurnaan cara output.
DerivingVia dapat diterapkan untuk semua jenis yang mendukung Coercible dan yang paling penting - sepenuhnya tanpa konsumsi sumber daya!

Terlebih lagi, DerivingVia dapat diterapkan tidak hanya untuk tipe baru , tetapi juga untuk semua jenis isomorfik jika mereka mendukung generik generik dan konversi paksa yang dipaksakan.

Kesimpulan


Jenis tipe baru adalah kekuatan yang sangat menyederhanakan dan meningkatkan kode, menghilangkan rutin dan mengurangi konsumsi sumber daya.

Terjemahan asli : The Great Power oftype (Hiromi Ishii)

PS Saya pikir, setelah artikel ini, diterbitkan lebih dari setahun yang lalu [bukan oleh artikel saya], Keajaiban tipe baru di Haskell tentang Tipe baru akan menjadi sedikit lebih jelas!

Source: https://habr.com/ru/post/id425405/


All Articles