Хороший стиль кодирования блока управления if/else на Haskell?
-
02-07-2019 - |
Вопрос
Я изучаю Haskell в надежде, что это поможет мне приблизиться к функциональному программированию.Раньше я в основном использовал языки с синтаксисом, подобным 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
в том же столбце.
Я знаю, что это просто не работает в Haskell, потому что если бы я сделал отступ, это было бы ошибкой анализа. else
в том же столбце, что и if
.
А как насчет следующего стиля?Я думаю, что это гораздо более понятно, чем приведенное выше.Но поскольку приведенное выше используется в Wikibooks и «Еще одно руководство по Haskell», отмеченное как «лучшее учебное пособие, доступное в Интернете» на официальном веб-сайте 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!"
Итак, мне интересно, какой стиль кодирования используется чаще — или существует ли другой стиль кодирования для этого фрагмента кода?
Решение
Стиль Haskell функционален, а не императивен!Вместо того, чтобы «сделай то, потом то», подумайте об объединении функций и описании что ваша программа будет работать, а не как.
В игре ваша программа просит пользователя угадать.Правильное предположение является победителем.В противном случае пользователь пытается еще раз.Игра продолжается до тех пор, пока пользователь не угадает правильно, поэтому пишем так:
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!
Другие советы
Вы можете использовать конструкцию «case»:
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!"
Небольшое улучшение оператора case Маттиаста (я бы отредактировал, но мне не хватает кармы) заключается в использовании функции сравнения, которая возвращает одно из трех значений: 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!"
Мне очень нравятся эти вопросы по Haskell, и я бы посоветовал другим писать больше.Часто вам кажется, что есть получил чтобы лучше выразить то, что вы думаете, но Haskell изначально настолько чужд, что ничего не приходит на ум.
Бонусный вопрос для любителей Haskell:какой тип догадки?
Как интерпретирует Haskell if ... then ... else
в течение do
block во многом соответствует всему синтаксису Haskell.
Но многие люди предпочитают немного другой синтаксис, позволяющий then
и else
появляться на том же уровне отступа, что и соответствующий if
.Поэтому GHC поставляется с дополнительным языковым расширением под названием DoAndIfThenElse
, что допускает этот синтаксис.
А DoAndIfThenElse
расширение стало частью основного языка в последней версии спецификации Haskell, Хаскелл 2010.
Обратите внимание, что тот факт, что вам нужно делать отступы между «then» и «else» внутри блока «do», многие считают ошибкой.Вероятно, это будет исправлено в Haskell (Haskell prime), следующей версии спецификации Haskell.
Вы также можете использовать явную группировку с фигурными скобками.См. раздел макета http://www.haskell.org/tutorial/patterns.html
Хотя я бы не рекомендовал этого делать.Я никогда не видел, чтобы кто-нибудь использовал явную группировку, за исключением нескольких особых случаев.Я обычно смотрю на Стандартный код прелюдии для примеров стиля.
Я использую стиль кодирования, подобный вашему примеру из Wikibooks.Конечно, он не соответствует рекомендациям C, но Haskell — это не C, и он вполне читаем, особенно если к нему привыкнуть.Он также создан по образцу алгоритмов, используемых во многих учебниках, таких как Кормен.
Вы увидите множество различных стилей отступов для Haskell.Большинство из них очень сложно поддерживать без редактора, настроенного на отступы в любом стиле.
Стиль, который вы демонстрируете, намного проще и менее требователен к редактору, и я думаю, вам следует его придерживаться.Единственное несоответствие, которое я вижу, это то, что вы помещаете первое действие в отдельную строку, а остальные помещаете после then/else.
Прислушайтесь к другому совету о том, как думать о коде на Haskell, но придерживайтесь своего стиля отступов.