أنواع القوة العظمى

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 {- -- instance Foldable [] foldr :: (a -> b -> b) -> b -> [a] -> b -} 

ويتم وصف الوظيفة النهائية على النحو التالي:

 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) -} 

إذا نظرت عن كثب ، يمكنك أن ترى عمليات مشابهة على كلا الجانبين: فقط 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 #-} -- 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 

من الضروري إبداء ملاحظة.

والحقيقة هي أنه من أجل أن يكون أحاديًا لنوع بيانات Max ، نحتاج إلى عنصر صغير ، أي لوجود عنصر فارغ. لذا ، لا يمكن إلا أن يكون الحد الأقصى المحدود a أحاديًا.

تصحيح العنصر الأقصى الصحيح نظريا
 newtype Max a = Max a instance Ord a => Semigroup (Max a) instance Bounded a => Monoid (Max a) 


بطريقة ما سنضطر إلى تحويل نوع بياناتنا بحيث يظهر عنصر فارغ ويمكننا استخدام التخثر.

 -- 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) 

العنصر المتقارب ربما يحول شبه مجموعة إلى أحادية!

تحرير القيود في الإصدارات الأخيرة من GHC
بالعودة إلى GHC 8.2 ، كان هناك حاجة إلى قيد أحادي في النوع

 instance Monoid a => Monoid (Maybe a) 

مما يعني أننا بحاجة إلى نوع جديد آخر:

 -- 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 

وهو أبسط بكثير بالفعل في 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)) {- -- for GHC 8.2 aggregate = (fmap getMax . getOption *** getSum) . (foldMap (Option . 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)) -- coerce :: (Maybe (Max Integer), Sum Integer) -> (Maybe Integer, Integer) 

لقد تجنبنا روتين سحب الأنواع المتداخلة ؛ قمنا بذلك دون إضاعة الموارد بوظيفة واحدة فقط.

أدوار أنواع البيانات المتداخلة


باستخدام وظيفة الإكراه ، يمكننا فرض تحويل أي أنواع متداخلة.
ولكن هل من الضروري استخدام هذه الميزة على نطاق واسع؟

 -- 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 

دلاليًا ، من السخف التحويل إلى 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 حول الأنواع الجديدة سيصبح أكثر وضوحًا قليلاً!

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


All Articles