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

Я изучаю Haskell, надеюсь, что это позволит мне приблизиться к функциональному программированию, прежде чем перейти к нему, я в основном использую языки C-sytanx, такие как языки программирования C, Java или D.

Я следил за учебником по Викиучебнику, это хорошо, до сих пор я мог понять большинство из них до главы "Простой ввод и вывод",

Но у меня есть небольшой вопрос о стиле кодирования if/else контрольного блока, используемого в учебнике.

В wikibook код выглядит следующим образом:

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-sytnax, таком как язык программирования, где мы должны идентифицировать if/else if/else в одном столбце.

Я знаю, что это просто не работает в Haskell, потому что это вызовет ошибку синтаксического анализа, если я укажу "else" в том же столбце "if".

Но как насчет следующего? Я думаю, что это намного яснее, чем выше. Но так как вышеупомянутое используется Wikibook и еще одним учебником 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!"

Итак, мне интересно, какой стиль кодирования используется чаще или есть стиль кодирования anthoer для этой части кода? Я тоже хотел бы это знать.

Ответ 1

Стиль 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!

Ответ 2

Незначительное улучшение для оператора 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: какой тип doGuessing?

Ответ 3

Вы можете использовать "case" -construct:

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

Ответ 4

Способ интерпретации Haskell if ... then ... else в блоке do очень похож на весь синтаксис Haskell.

Но многие люди предпочитают несколько иной синтаксис, позволяя then и else появляться на том же уровне отступов, что и соответствующий if. Поэтому GHC поставляется с дополнительным языковым расширением, называемым DoAndIfThenElse, что позволяет использовать этот синтаксис.

Расширение DoAndIfThenElse является частью основного языка в последней версии спецификации Haskell, Haskell 2010.

Ответ 5

Обратите внимание, что факт, что вы должны отступать "then" и "else" внутри блока "do", считается ошибкой многих. Вероятно, он будет исправлен в Haskell '(Haskell prime), следующей версии спецификации Haskell.

Ответ 6

Вы также можете использовать явную группировку с фигурными фигурными скобками. См. Раздел макета http://www.haskell.org/tutorial/patterns.html

Я бы не рекомендовал этого. Я никогда не видел, чтобы кто-то использовал явную группировку, кроме того, в нескольких особых случаях. Я обычно смотрю Стандартный код Prelude для примеров стиля.

Ответ 7

Я использую стиль кодирования, например, ваш пример из Wikibooks. Конечно, это не соответствует рекомендациям C, но Haskell не C, и это довольно читаемо, особенно когда вы привыкаете к нему. Он также был создан после стиля алгоритмов, используемых во многих учебниках, таких как Cormen.

Ответ 8

Вы увидите кучу разных стилей отступов для Haskell. Большинство из них очень трудно поддерживать без редактора, который настроен на отступ точно в любом стиле.

Стиль, который вы показываете, намного проще и менее требовательна к редактору, и я думаю, вы должны придерживаться его. Единственная несогласованность, которую я вижу, - это то, что вы делаете первое действие в своей собственной строке, пока вы добавляете другие dos после then/else.

Соблюдайте другой совет о том, как думать о коде в Haskell, но придерживайтесь стиля отступов.