هل هناك مكافئ هاسكل لفئات OOP المجردة ، باستخدام أنواع البيانات الجبرية أو تعدد الأشكال؟
سؤال
في Haskell ، هل من الممكن كتابة وظيفة بتوقيع يمكن أن يقبل نوعين مختلفين (على الرغم من مشابه) ، ويعمل بشكل مختلف اعتمادًا على النوع الذي يتم تمريره؟
مثال قد يجعل سؤالي أكثر وضوحًا. إذا كان لدي وظيفة اسمها myFunction
, ، ونوعين يدعى MyTypeA
و MyTypeB
, ، هل يمكنني تحديد myFunction
بحيث لا يمكن أن تقبل سوى البيانات من النوع MyTypeA
أو MyTypeB
كمعلمة لها الأولى؟
type MyTypeA = (Int, Int, Char, Char)
type MyTypeB = ([Int], [Char])
myFunction :: MyTypeA_or_MyTypeB -> Char
myFunction constrainedToTypeA = something
myFunction constrainedToTypeB = somethingElse
في لغة OOP ، يمكنك كتابة ما أحاول تحقيقه مثل:
public abstract class ConstrainedType {
}
public class MyTypeA extends ConstrainedType {
...various members...
}
public class MyTypeB extends ConstrainedType {
...various members...
}
...
public Char myFunction(ConstrainedType a) {
if (a TypeOf MyTypeA) {
return doStuffA();
}
else if (a TypeOf MyTypeB) {
return doStuffB();
}
}
لقد قرأت عن أنواع البيانات الجبرية وأعتقد أنني بحاجة إلى تحديد هاسكل يكتب, ، لكنني لست متأكدًا من كيفية تحديد تحديد ذلك حتى يتمكن من تخزين نوع أو آخر ، وكذلك كيف أستخدمه في وظائف بلدي.
المحلول
نعم ، أنت محق ، فأنت تبحث عن أنواع البيانات الجبرية. هناك برنامج تعليمي رائع عليهم في تعلم لك هاسكل.
بالنسبة للسجل ، فإن مفهوم فئة مجردة من OOP لديه بالفعل ثلاث ترجمات مختلفة إلى Haskell ، و ADTS هي مجرد واحدة. فيما يلي نظرة عامة سريعة على التقنيات.
أنواع البيانات الجبرية
تقوم أنواع البيانات الجبرية بتشفير نمط فئة مجردة تعرف فئاتها الفرعية ، وحيث تحقق الوظائف من مثال معين هو الكائن هو عضو من خلال الصب.
abstract class IntBox { }
class Empty : IntBox { }
class Full : IntBox {
int inside;
Full(int inside) { this.inside = inside; }
}
int Get(IntBox a) {
if (a is Empty) { return 0; }
if (a is Full) { return ((Full)a).inside; }
error("IntBox not of expected type");
}
ترجم إلى:
data IntBox = Empty | Full Int
get :: IntBox -> Int
get Empty = 0
get (Full x) = x
سجل الوظائف
هذا النمط لا يسمح بالضرب ، لذا فإن Get
الوظيفة أعلاه لن تكون قابلة للتعبير في هذا النمط. إذن هذا شيء مختلف تمامًا.
abstract class Animal {
abstract string CatchPhrase();
virtual void Speak() { print(CatchPhrase()); }
}
class Cat : Animal {
override string CatchPhrase() { return "Meow"; }
}
class Dog : Animal {
override string CatchPhrase() { return "Woof"; }
override void Speak() { print("Rowwrlrw"); }
}
ترجمتها في Haskell لا تعين أنواع الأنواع. Animal
هو النوع الوحيد ، و Dog
و Cat
يتم سحقها في وظائف مُنشئها:
data Animal = Animal {
catchPhrase :: String,
speak :: IO ()
}
protoAnimal :: Animal
protoAnimal = Animal {
speak = putStrLn (catchPhrase protoAnimal)
}
cat :: Animal
cat = protoAnimal { catchPhrase = "Meow" }
dog :: Animal
dog = protoAnimal { catchPhrase = "Woof", speak = putStrLn "Rowwrlrw" }
هناك بعض التباديل المختلفة لهذا المفهوم الأساسي. الثابت هو أن النوع التجريدي هو نوع سجل حيث تكون الأساليب هي حقول السجل.
تحرير: هناك مناقشة جيدة في التعليقات على بعض التفاصيل الدقيقة لهذا النهج ، بما في ذلك خطأ في الكود أعلاه.
typeclasses
هذا هو أقل تشفير المفضل لدي لأفكار OO. إنه مريح لمبرمجي OO لأنه يستخدم كلمات وأنواع الخرائط المألوفة لأنواع. لكن سجل الوظائف المذكور أعلاه يميل إلى أن يكون أسهل في العمل عندما تصبح الأمور معقدة.
سأقوم بتشفير مثال الحيوان مرة أخرى:
class Animal a where
catchPhrase :: a -> String
speak :: a -> IO ()
speak a = putStrLn (catchPhrase a)
data Cat = Cat
instance Animal Cat where
catchPhrase Cat = "Meow"
data Dog = Dog
instance Animal Dog where
catchPhrase Dog = "Woof"
speak Dog = putStrLn "Rowwrlrw"
هذا يبدو لطيفا ، أليس كذلك؟ تأتي الصعوبة عندما تدرك أنه على الرغم من أن الأمر يبدو وكأنه لا يعمل حقًا مثل OO. قد ترغب في الحصول على قائمة بالحيوانات ، ولكن أفضل ما يمكنك فعله الآن هو Animal a => [a]
, ، قائمة بالحيوانات المتجانسة ، على سبيل المثال. قائمة القطط فقط أو الكلاب فقط. ثم تحتاج إلى جعل هذا النوع الغلاف:
{-# LANGUAGE ExistentialQuantification #-}
data AnyAnimal = forall a. Animal a => AnyAnimal a
instance Animal AnyAnimal where
catchPhrase (AnyAnimal a) = catchPhrase a
speak (AnyAnimal a) = speak a
وثم [AnyAnimal]
هو ما تريده لقائمة الحيوانات الخاصة بك. ومع ذلك ، اتضح ذلك AnyAnimal
يعرض بالضبط نفس المعلومات عن نفسها مثل Animal
سجل في المثال الثاني ، لقد ذهبنا للتو بطريقة دورية. وبالتالي ، لماذا لا أعتبر أن typeclasses يكون ترميزًا جيدًا جدًا لـ OO.
وبالتالي يختتم إصدار هذا الأسبوع من طريقة الكثير من المعلومات!
نصائح أخرى
يبدو أنك قد ترغب في القراءة typeclasses.
النظر في هذا المثال باستخدام typeclasses.
نحدد C ++-مثل "فئة مجردة" MVC
بناءً على ثلاثة أنواع (ملاحظة MultiParamTypeClasses
): tState
tAction
tReaction
من أجل تحديد وظيفة رئيسية tState -> tAction -> (tState, tReaction)
(عندما يتم تطبيق إجراء على الدولة ، تحصل على حالة جديدة ورد فعل.
يحتوي typeclass على ثلاث وظائف "C ++ Abstract" ، وبعضها أكثر تعريفًا على "التجريدية". سيتم تعريف وظائف "التجريد" متى و instance MVC
وهناك حاجة.
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, NoMonomorphismRestriction #-}
-- -------------------------------------------------------------------------------
class MVC tState tAction tReaction | tState -> tAction tReaction where
changeState :: tState -> tAction -> tState -- get a new state given the current state and an action ("abstract")
whatReaction :: tState -> tReaction -- get the reaction given a new state ("abstract")
view :: (tState, tReaction) -> IO () -- show a state and reaction pair ("abstract")
-- get a new state and a reaction given an state and an action (defined using previous functions)
runModel :: tState -> tAction -> (tState, tReaction)
runModel s a = let
ns = (changeState s a)
r = (whatReaction ns)
in (ns, r)
-- get a new state given the current state and an action, calling 'view' in the middle (defined using previous functions)
run :: tState -> tAction -> IO tState
run s a = do
let (s', r) = runModel s a
view (s', r)
return s'
-- get a new state given the current state and a function 'getAction' that provides actions from "the user" (defined using previous functions)
control :: tState -> IO (Maybe tAction) -> IO tState
control s getAction = do
ma <- getAction
case ma of
Nothing -> return s
Just a -> do
ns <- run s a
control ns getAction
-- -------------------------------------------------------------------------------
-- concrete instance for MVC, where
-- tState=Int tAction=Char ('u' 'd') tReaction=Char ('z' 'p' 'n')
-- Define here the "abstract" functions
instance MVC Int Char Char where
changeState i c
| c == 'u' = i+1 -- up: add 1 to state
| c == 'd' = i-1 -- down: add -1 to state
| otherwise = i -- no change in state
whatReaction i
| i == 0 = 'z' -- reaction is zero if state is 0
| i < 0 = 'n' -- reaction is negative if state < 0
| otherwise = 'p' -- reaction is positive if state > 0
view (s, r) = do
putStrLn $ "view: state=" ++ (show s) ++ " reaction=" ++ (show r) ++ "\n"
--
-- define here the function "asking the user"
getAChar :: IO (Maybe Char) -- return (Just a char) or Nothing when 'x' (exit) is typed
getAChar = do
putStrLn "?"
str <- getLine
putStrLn ""
let c = str !! 0
case c of
'x' -> return Nothing
_ -> return (Just c)
-- --------------------------------------------------------------------------------------------
-- --------------------------------------------------------------------------------------------
-- call 'control' giving the initial state and the "input from the user" function
finalState = control 0 getAChar :: IO Int
--
main = do
s <- finalState
print s