Newtype هو إعلان نوع بيانات متخصص. بحيث تحتوي على منشئ وحقل واحد فقط.
newtype Foo a = Bar a newtype Id = MkId Word

أسئلة مبتدئة نموذجية
ما الفرق من بيانات نوع البيانات؟ data Foo a = Bar a data Id = MkId Word
إن الخصوصية الرئيسية
للنوع الجديد هي أنه يتكون من نفس الأجزاء
كحقله الوحيد. بتعبير أدق ، يختلف عن الأصل على مستوى النوع ، ولكن له نفس التمثيل في الذاكرة ، ويتم حسابه بدقة (وليس كسول).
باختصار -
newtype أكثر فعالية بسبب عرضها.
نعم ، هذا لا يعني شيئاً بالنسبة لي ... سأستخدم البياناتلا ، حسنًا ، في النهاية ، يمكنك دائمًا تمكين
الحقول -funpack-صارمة- :)
للحقول الصارمة (غير الكسولة) أو تحديدها مباشرةً
data Id = MkId !Word
ومع ذلك ، لا تقتصر قوة
النوع الجديد على الكفاءة الحسابية. إنهم أقوى بكثير!
3 أدوار من نوع جديد

إخفاء التنفيذ
module Data.Id (Id()) where newtype Id = MkId Word
يختلف
newtype عن
Word الأصلي داخليًا فقط.
لكننا نخفي مُنشئ
MkId خارج الوحدة النمطية.
تنفيذ التنفيذ
{-# LANGUAGE GeneralizedNewtypeDeriving #-} newtype Id = MkId Word deriving (Num, Eq)
على الرغم من أن هذا ليس في معيار Haskell2010 ، من خلال توسيع ناتج الأنواع الجديدة العامة ، يمكنك تلقائيًا استنتاج سلوك
نوع جديد مثل سلوك المجال الداخلي. في حالتنا ، فإن سلوك
Eq Id و
Num Id هو نفس سلوك
Eq Word و
Num Word .
يمكن تحقيق المزيد من خلال التوسع في التكاثر المكرر (
DerivingVia ) ، ولكن أكثر من ذلك لاحقًا.
تنفيذ الاختيار
على الرغم من منشئه الخاص ، يمكنك في بعض الحالات استخدام تمثيلك الداخلي.
التحدي
هناك قائمة بالأعداد الصحيحة. ابحث عن الحد الأقصى والمبلغ الإجمالي بتمريرة واحدة فقط في القائمة.
ولا تستخدم
حزم foldl و
folds .
إجابة نموذجية
بالطبع ،
أضعاف ! :)
foldr :: Foldable t => (a -> b -> b) -> b -> ta -> b
ويتم وصف الوظيفة النهائية على النحو التالي:
aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = foldr (\el (m, s) -> (Just el `max` m, el + s)) (Nothing, 0)
إذا نظرت عن كثب ، يمكنك أن ترى عمليات مشابهة على كلا الجانبين:
فقط el `max` m و
el + s . في كلتا الحالتين ، رسم الخرائط والتشغيل الثنائي. والعناصر الفارغة لا
شيء و
0 .
نعم ، هذه أحادية!
Monoid و Semigroup بمزيد من التفاصيلالمجموعة شبه هي خاصية لعملية ثنائية ترابطية
x ⋄ (y ⋄ z) == (x ⋄ y) ⋄ z
monoid هو خاصية لعملية ترابطية (أي شبه مجموعة)
x ⋄ (y ⋄ z) == (x ⋄ y) ⋄ z
الذي يحتوي على عنصر فارغ لا يغير أي عنصر سواء على اليمين أو على اليسار
x ⋄ empty == x == empty ⋄ x
كل من
الحد الأقصى و
(+) مترابطان ، كلاهما يحتويان على عناصر فارغة -
لا شيء و
0 .
والجمع بين رسم خرائط الأحاديات مع الالتواء قابل
للطي !
قابلة للطي أكثر تفصيلاأذكر تعريف الطي:
class Foldable t where foldMap :: (Monoid m) => (a -> m) -> ta -> m ...
دعونا نطبق سلوك الطي على
الحد الأقصى و
(+) . لا يمكننا تنظيم أكثر من تطبيق واحد
Word monoid. حان الوقت للاستفادة من تنفيذ خيار
newtype !
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
من الضروري إبداء ملاحظة.
والحقيقة هي أنه من أجل أن يكون أحاديًا لنوع بيانات
Max ، نحتاج إلى عنصر صغير ، أي لوجود عنصر فارغ. لذا ، لا يمكن إلا أن يكون
الحد الأقصى المحدود a أحاديًا.
تصحيح العنصر الأقصى الصحيح نظريا newtype Max a = Max a instance Ord a => Semigroup (Max a) instance Bounded a => Monoid (Max a)
بطريقة ما سنضطر إلى تحويل نوع بياناتنا بحيث يظهر عنصر فارغ ويمكننا استخدام التخثر.
العنصر المتقارب
ربما يحول شبه مجموعة إلى أحادية!
تحرير القيود في الإصدارات الأخيرة من GHCبالعودة إلى GHC 8.2 ، كان هناك حاجة إلى قيد أحادي في النوع
instance Monoid a => Monoid (Maybe a)
مما يعني أننا بحاجة إلى نوع جديد آخر:
وهو أبسط بكثير بالفعل في GHC 8.4 ، حيث لا يلزم سوى نصف مجموعة في تقييد النوع ، وحتى ليست هناك حاجة لإنشاء نوع خيار.
instance Semigroup a => Monoid (Maybe a)
استجابة قابلة للطي
حسنًا ، قم الآن بتحديث الرمز باستخدام قابلية للطي والسهام.
نتذكر أن (.) مجرد تكوين وظيفي:
(.) :: (b -> c) -> (a -> b) -> a -> c f . g = \x -> f (gx)
وتذكر أن
fmap هو
جنازة :
fmap :: Functor f => (a -> b) -> fa -> fb
وتنفيذها
لعلها موصوفة أعلى قليلا.
سهم أكثر تفصيلاًالسهام هي خصائص لبعض الوظائف التي تسمح لك بالعمل معها في مخطط كتلة.
يمكن العثور على مزيد من التفاصيل هنا:
الأسهم: واجهة عامة للحسابفي حالتنا ، نستخدم وظيفة الأسهم
هذا هو
instance Arrow (->)
سوف نستخدم الوظائف:
(***) :: Arrow a => abc -> ab' c' -> a (b, b') (c, c') (&&&) :: Arrow a => abc -> abc' -> ab (c, c')
لقضيتنا
abc == (->) bc == b -> c
وبالتالي ، يتم تقليل توقيع وظائفنا إلى:
(***) :: (b -> c) -> (b' -> c') -> ((b, b') -> (c, c')) (&&&) :: (b -> c) -> (b -> c') -> (b -> (c, c'))
أو بكلمات بسيطة ، تجمع الدالة
(***) دالتين مع وسيطة واحدة (ونوع إخراج واحد) في دالة مع عمل زوج من الوسيطات عند الإدخال وعند الإخراج ، على التوالي ، زوج من أنواع الإخراج.
الدالة
(&&&) هي نسخة مجردة
(***) ، حيث يكون نوع وسيطات الإدخال للوظيفتين هو نفسه ، وعند الإدخال ليس لدينا زوج من الوسيطات ، ولكن وسيطة واحدة.
المجموع ، اكتسبت الوظيفة الموحدة النموذج:
import Data.Semigroup import Data.Monoid import Control.Arrow aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = (fmap getMax *** getSum) . (foldMap (Just . Max &&& Sum))
اتضح لفترة وجيزة جدا!
ولكن ، لا يزال الأمر متعبًا للالتفاف وعكس البيانات من الأنواع المتداخلة!
يمكنك تقليله أكثر ، وسوف يساعدنا التحويل القسري الخالي من الموارد!
تحويل قسري آمن وخالي من الموارد وأدوار النوع
هناك وظيفة من حزمة
Unsafe.Coerce -
unsafeCoerce import Unsafe.Coerce(unsafeCoerce) unsafeCoerce :: a -> b
تقوم الوظيفة غير الآمنة بالقوة بتحويل النوع: من
أ إلى
ب .
في الواقع ، الوظيفة هي السحر ، فهي تخبر المترجم أن ينظر في البيانات من النوع
a كنوع
b ، دون مراعاة عواقب هذه الخطوة.
يمكن استخدامه لتحويل الأنواع المتداخلة ، ولكن يجب أن تكون حذرًا للغاية.
في عام 2014 ، كانت هناك ثورة مع
نوع جديد ، ألا وهو التحويل الإجباري الآمن والقليل من الموارد!
import Data.Coerce(coerce) coerce :: Coercible ab => a -> b
لقد فتحت هذه الميزة حقبة جديدة في العمل مع
newtype .
يعمل محول القسرية القسرية مع الأنواع التي لها نفس البنية في الذاكرة. يبدو وكأنه فئة نوع ، ولكن في الواقع يقوم GHC بتحويل الأنواع في وقت الترجمة ولا يمكن تحديد المثيلات من تلقاء نفسها.
تتيح لك وظيفة
Data.Coerce.coerce تحويل الأنواع بدون موارد ، ولكن من أجل هذا نحتاج إلى الوصول إلى
منشئي النوع.
الآن تبسيط وظيفتنا:
import Data.Semigroup import Data.Monoid import Control.Arrow import Data.Coerce aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = coerce . (foldMap (Just . Max &&& Sum))
لقد تجنبنا روتين سحب الأنواع المتداخلة ؛ قمنا بذلك دون إضاعة الموارد بوظيفة واحدة فقط.
أدوار أنواع البيانات المتداخلة
باستخدام وظيفة
الإكراه ، يمكننا فرض تحويل أي أنواع متداخلة.
ولكن هل من الضروري استخدام هذه الميزة على نطاق واسع؟
دلاليًا ، من السخف التحويل إلى
Sorted a من
Sorted (Down a) .
ومع ذلك ، يمكنك تجربة:
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)
كل شيء سيكون على ما يرام ، لكن الإجابة الصحيحة هي
فقط (أسفل 3) .
وبالتحديد ، من أجل قطع السلوك غير الصحيح ، تم إدخال أدوار الكتابة.
{-# LANGUAGE RoleAnnotations #-} type role Sorted nominal
دعنا نحاول الآن:
ghci> minView (coerce h :: Sorted (Down Int)) error: Couldn't match type 'Int' with 'Down Int' arising from a use of 'coerce'
أفضل بكثير!
في المجموع هناك 3 أدوار (
نوع الدور ):
- تمثيل - مكافئ إذا كان التمثيل نفسه
- الاسمية - يجب أن يكون لها نفس النوع بالضبط
- فانتوم - مستقل عن المحتوى الحقيقي. تعادل أي شيء
في معظم الحالات ، يكون المترجم ذكيًا بما يكفي للكشف عن دور النوع ، ولكن يمكن مساعدته.
سلوك اشتقاق مكرر
بفضل التوسع في لغة
DerivingVia ، تحسن دور التوزيع
للنوع الجديد .
بدءًا من GHC 8.6 ، الذي تم إصداره مؤخرًا ، ظهر هذا التمديد الجديد.
{-# LANGUAGE DerivingVia #-} newtype Id = MkId Word deriving (Semigroup, Monoid) via Max Word
كما ترى ، يتم استدلال سلوك النوع تلقائيًا بسبب تحسين كيفية الإخراج.
يمكن تطبيق
DerivingVia على أي نوع يدعم
الإكراه والأهم من ذلك - تمامًا بدون استهلاك الموارد!
والأكثر من ذلك ، يمكن تطبيق
DerivingVia ليس فقط على
النوع الجديد ، ولكن أيضًا على أي أنواع
متشابهة إذا كانت تدعم
Generics Generics والتحويل القسري القسري.
الاستنتاجات
أنواع
newtype هي قوة قوية تبسط وتحسن بشكل كبير التعليمات البرمجية ، وتزيل الروتين وتقلل من استهلاك الموارد.
الترجمة الأصلية :
القوة العظمى للأنواع الجديدة (هيرومي إيشي)PS أعتقد أنه بعد هذا المقال ، المنشور قبل أكثر من عام [ليس عن طريق مقالي] ، فإن سحر newtype في Haskell حول الأنواع الجديدة سيصبح أكثر وضوحًا قليلاً!