أسلوب ترميز Haskell الجيد لكتلة التحكم if/else؟
-
02-07-2019 - |
سؤال
أنا أتعلم هاسكل على أمل أن يساعدني ذلك في الاقتراب من البرمجة الوظيفية.في السابق، كنت أستخدم غالبًا لغات ذات بناء جملة شبيه بلغة C، مثل C وJava وD.
لدي سؤال صغير حول أسلوب الترميز لـ if
/else
كتلة التحكم المستخدمة من قبل البرنامج التعليمي على ويكي الكتب.يبدو الرمز كما يلي:
doGuessing num = do
putStrLn "Enter your guess:"
guess <- getLine
if (read guess) < num
then do putStrLn "Too low!"
doGuessing num
else if (read guess) > num
then do putStrLn "Too high!"
doGuessing num
else do putStrLn "You Win!"
هذا يجعلني في حيرة من أمري، لأن أسلوب الترميز هذا ينتهك تمامًا النمط الموصى به في اللغات المشابهة للغة C، حيث يجب أن نضع مسافة بادئة if
, else if
, ، و else
في نفس العمود.
أعلم أنه لا يعمل في هاسكل، لأنه سيكون خطأ تحليلي إذا قمت بوضع مسافة بادئة else
في نفس العمود كما if
.
ولكن ماذا عن النمط التالي؟أعتقد أن الأمر أوضح بكثير من المذكور أعلاه.ولكن بما أن ما ورد أعلاه يتم استخدامه بواسطة Wikibooks وYet Another Haskell Tutorial، والذي تم وضع علامة "أفضل برنامج تعليمي متاح عبر الإنترنت" على موقع Haskell الرسمي، فأنا لست متأكدًا مما إذا كان أسلوب الترميز هذا هو العرف في برامج Haskell.
doGuessing num = do
putStrLn "Enter your guess:"
guess <- getLine
if (read guess) < num then
do
putStrLn "Too low!"
doGuessing num
else if (read guess) > num then do
putStrLn "Too high!"
doGuessing num
else do
putStrLn "You Win!"
لذا، أشعر بالفضول لمعرفة نمط الترميز الذي يتم استخدامه في كثير من الأحيان، أم أن هناك نمط ترميز آخر لهذا الجزء من التعليمات البرمجية؟
المحلول
أسلوب هاسكل عملي وليس ضروريًا!بدلاً من "افعل هذا ثم ذاك"، فكر في الجمع بين الوظائف والوصف ماذا برنامجك سوف يفعل، وليس كيف.
في اللعبة، يطلب برنامجك من المستخدم التخمين.التخمين الصحيح هو الفائز.وبخلاف ذلك، يحاول المستخدم مرة أخرى.تستمر اللعبة حتى يخمن المستخدم بشكل صحيح، لذلك نكتب ما يلي:
main = untilM (isCorrect 42) (read `liftM` getLine)
يستخدم هذا أداة الدمج التي تقوم بتشغيل الإجراء بشكل متكرر (getLine
تسحب خط الإدخال و read
يحول تلك السلسلة إلى عدد صحيح في هذه الحالة) ويتحقق من نتيجتها:
untilM :: Monad m => (a -> m Bool) -> m a -> m ()
untilM p a = do
x <- a
done <- p x
if done
then return ()
else untilM p a
المسند (مطبق جزئيا في main
) يتحقق من التخمين مقابل القيمة الصحيحة ويستجيب وفقًا لذلك:
isCorrect :: Int -> Int -> IO Bool
isCorrect num guess =
case compare num guess of
EQ -> putStrLn "You Win!" >> return True
LT -> putStrLn "Too high!" >> return False
GT -> putStrLn "Too low!" >> return False
الإجراء الذي سيتم تنفيذه حتى يخمن اللاعب بشكل صحيح هو
read `liftM` getLine
لماذا لا تبقي الأمر بسيطًا وتؤلف الوظيفتين فقط؟
*Main> :type read . getLine <interactive>:1:7: Couldn't match expected type `a -> String' against inferred type `IO String' In the second argument of `(.)', namely `getLine' In the expression: read . getLine
نوع من getLine
يكون IO String
, ، لكن read
يريد نقيا String
.
الوظيفة liftM
من Control.Monad يأخذ وظيفة خالصة و"يرفعها" إلى موناد.يخبرنا نوع التعبير كثيرًا عما يفعله:
*Main> :type read `liftM` getLine read `liftM` getLine :: (Read a) => IO a
إنه إجراء إدخال/إخراج يعيد لنا قيمة تم تحويلها بها عند التشغيل read
, ، ان Int
في حالتنا هذه.أذكر ذلك readLine
هو إجراء الإدخال/الإخراج الذي ينتج String
القيم، حتى تتمكن من التفكير liftM
كما يسمح لنا بتقديم الطلب read
"داخل" IO
مناد.
لعبة عينة:
1 Too low! 100 Too high! 42 You Win!
نصائح أخرى
يمكنك استخدام "الحالة" - البناء:
doGuessing num = do
putStrLn "Enter your guess:"
guess <- getLine
case (read guess) of
g | g < num -> do
putStrLn "Too low!"
doGuessing num
g | g > num -> do
putStrLn "Too high!"
doGuessing num
otherwise -> do
putStrLn "You Win!"
هناك تحسين بسيط في بيان حالة ماتياست (سأقوم بالتحرير، لكني أفتقر إلى الكارما) وهو استخدام وظيفة المقارنة، التي تُرجع إحدى القيم الثلاث، LT، أو GT، أو EQ:
doGuessing num = do
putStrLn "Enter your guess:"
guess <- getLine
case (read guess) `compare` num of
LT -> do putStrLn "Too low!"
doGuessing num
GT -> do putStrLn "Too high!"
doGuessing num
EQ -> putStrLn "You Win!"
تعجبني حقًا أسئلة هاسكل هذه، وأشجع الآخرين على نشر المزيد.في كثير من الأحيان تشعر أن هناك يملك لتكون طريقة أفضل للتعبير عما تفكر فيه، لكن هاسكل كان في البداية غريبًا جدًا لدرجة أنه لن يتبادر إلى ذهنك أي شيء.
سؤال إضافي لصحيفة هاسكل:ما هو نوع التخمين؟
الطريقة التي يفسر بها هاسكل if ... then ... else
داخل أ do
block يتماشى إلى حد كبير مع بناء جملة هاسكل بالكامل.
لكن الكثير من الناس يفضلون بناء جملة مختلفًا قليلاً، إذا سمحوا بذلك then
و else
لتظهر على نفس مستوى المسافة البادئة المقابلة if
.لذلك، تأتي GHC مع ملحق لغة اختياري يسمى DoAndIfThenElse
, ، الذي يسمح ببناء الجملة هذا.
ال DoAndIfThenElse
تم تحويل الامتداد إلى جزء من اللغة الأساسية في المراجعة الأخيرة لمواصفات هاسكل، هاسكل 2010.
لاحظ أن حقيقة أنه يتعين عليك وضع مسافة بادئة لـ "ثم" و"آخر" داخل كتلة "do" تعتبر خطأً من قبل الكثيرين.من المحتمل أن يتم إصلاحه في Haskell' (Haskell Prime)، الإصدار التالي من مواصفات Haskell.
يمكنك أيضًا استخدام التجميع الواضح مع الأقواس المتعرجة.راجع قسم التخطيط http://www.haskell.org/tutorial/patterns.html
لا أوصي بذلك بالرغم من ذلك.لم أر مطلقًا أي شخص يستخدم التجميع الصريح إلا في بعض الحالات الخاصة.أنا عادة أنظر إلى كود المقدمة القياسي للحصول على أمثلة من الاسلوب.
أستخدم أسلوب ترميز مثل مثالك من Wikibooks.من المؤكد أنها لا تتبع إرشادات لغة C، لكن لغة Haskell ليست لغة C، وهي قابلة للقراءة إلى حد ما، خاصة بمجرد أن تعتاد عليها.كما أنها منقوشة وفقًا لأسلوب الخوارزميات المستخدمة في العديد من الكتب المدرسية، مثل كورمين.
سترى مجموعة من أنماط المسافة البادئة المختلفة لهاسكل.من الصعب جدًا الحفاظ على معظمها بدون محرر تم إعداده لوضع مسافة بادئة تمامًا بأي نمط.
النمط الذي تعرضه أبسط بكثير وأقل تطلبًا من المحرر، وأعتقد أنه يجب عليك الالتزام به.التناقض الوحيد الذي يمكنني رؤيته هو أنك تضع الإجراء الأول في السطر الخاص به بينما تضع المهام الأخرى بعدthen/else.
انتبه إلى النصيحة الأخرى حول كيفية التفكير في التعليمات البرمجية في هاسكل، ولكن التزم بأسلوب المسافة البادئة.