Хороший стиль кодирования блока управления if/else на Haskell?

StackOverflow https://stackoverflow.com/questions/127190

  •  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, но придерживайтесь своего стиля отступов.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top