Подтвердить что ты не робот

Следует ли избегать упоминания в Haskell?

В большинстве учебных пособий Haskell рассказывается об использовании do-notation для IO.

Я также начал с do-notation, но это делает мой код более похожим на императивный язык больше, чем на FP-язык.

На этой неделе я увидел, что учебник использует IO с <$>

stringAnalyzer <$> readFile "testfile.txt"

вместо do

main = do
    strFile <- readFile "testfile.txt"
    let analysisResult = stringAnalyzer strFile
    return analysisResult

И инструмент анализа журнала завершен без do.

Итак, мой вопрос: " Должны ли мы избегать использования нотации в любом случае?".

Я знаю, возможно, do сделает код лучше в некоторых случаях.

Кроме того, почему большинство учебных пособий обучают IO с помощью do?

По-моему <$> и <*> делает код более FP, чем IO.

4b9b3361

Ответ 1

do в дескрипторах Haskell довольно простым способом.

do
  x <- foo
  e1 
  e2
  ...

превращается в

 foo >>= \x ->
 do
   e1
   e2

и

do
  x
  e1
  e2
  ...

в

x >>
do 
  e1
  e2
  ....

Это означает, что вы можете написать какие-либо монадические вычисления с помощью >>= и return. Единственная причина, почему мы этого не делаем, - это просто больший синтаксис. Монады полезны для имитации императивного кода, нотация do делает его похожим на него.

Синтаксис C-ish значительно облегчает понимание новичков. Вы правы, это выглядит не так функционально, но требуя, чтобы кто-то правильно набрал монады, прежде чем они смогут использовать IO, является довольно большим сдерживающим фактором.

С другой стороны, причина, по которой мы будем использовать >>= и return, состоит в том, что она намного более компактна для 1 - 2 лайнеров. Однако, как правило, он становится немного более нечитаемым для чего-либо слишком большого. Поэтому, чтобы ответить на ваш вопрос, нет, пожалуйста, не избегайте обозначения, если это необходимо.

Наконец, два оператора, которые вы видели, <$> и <*>, на самом деле являются fmap и аппликативными соответственно, а не монадическими. На самом деле они не могут быть использованы для представления того, что делают записи. Они более компактны, чтобы быть уверенными, но они не позволяют вам легко назвать промежуточные значения. Лично я использую их примерно в 80% случаев, главным образом потому, что я склонен писать очень маленькие составные функции в любом случае, какие приложения подходят для.

Ответ 2

По-моему <$> и <*> делает код более FP, чем IO.

Haskell не является чисто функциональным языком, потому что это "выглядит лучше". Иногда это происходит, часто это не так. Причиной оставаться функциональным является не его синтаксис, а его семантика. Это дает нам ссылочную прозрачность, что значительно упрощает доказательство инвариантов, позволяет очень оптимизировать на высоком уровне, упрощает создание кода общего назначения и т.д.

Ничто из этого не имеет большого отношения к синтаксису. Монадические вычисления по-прежнему являются чисто функциональными - независимо от того, записываете ли вы их с обозначением do или с помощью <$>, <*> и >>=, поэтому мы получаем преимущества Haskell в любом случае.

Однако, несмотря на вышеупомянутые преимущества FP, часто более интуитивно думать о алгоритмах с императивной точки зрения, даже если вы привыкли к тому, как это реализуется через монады. В этих случаях обозначение do дает вам краткое представление о "порядке вычислений", "происхождении данных", "точке изменения", но тривиально, чтобы вручную отбросить его в голове до версии >>=, чтобы понять, что происходит функционально.

Аппликативный стиль, безусловно, велик во многих отношениях, однако он по сути не имеет смысла. Это часто бывает хорошо, но особенно в более сложных проблемах может быть очень полезно дать имена "временным" переменным. При использовании только синтаксиса "FP" Haskell для этого требуется либо lambdas, либо явно названные функции. Оба имеют хорошие прецеденты, но первый вводит довольно много шума прямо в середине вашего кода, а последний скорее нарушает "поток", так как для него требуется where или let, расположенный где-то еще, где вы его используете, do, с другой стороны, позволяет вам ввести именованную переменную прямо там, где она вам нужна, без какого-либо шума вообще.

Ответ 3

Я часто нахожу, что сначала пишу монадическое действие в нотации do, а затем рефакторинг его до простого монадического (или функториального) выражения. Это происходит, главным образом, когда блок do оказывается короче, чем я ожидал. Иногда я реорганизую в противоположном направлении; это зависит от кода, о котором идет речь.

Мое общее правило: если блок do - это всего лишь несколько строк, он обычно опрятный как короткое выражение. Длинный do -блок, вероятно, более читабельен, если только вы не можете найти способ разбить его на более мелкие и более сложные функции.


Как обработанный пример, как мы можем превратить ваш фрагмент кода в ваш простой.

main = do
    strFile <- readFile "testfile.txt"
    let analysisResult = stringAnalyzer strFile
    return analysisResult

Во-первых, обратите внимание, что последние две строки имеют форму let x = y in return x. Это, конечно, можно преобразовать просто в return y.

main = do
    strFile <- readFile "testfile.txt"
    return (stringAnalyzer strFile)

Это очень короткий блок do: мы привязываем readFile "testfile.txt" к имени, а затем делаем что-то с этим именем в самой следующей строке. Попробуйте "дезасолить" его, как компилятор:

main = readFile "testFile.txt" >>= \strFile -> return (stringAnalyser strFile)

Посмотрите на лямбда-форму в правой части >>=. Он просил переписываться в стиле без точек: \x -> f $ g x становится \x -> (f . g) x, который становится f . g.

main = readFile "testFile.txt" >>= (return . stringAnalyser)

Это уже намного опережает исходный блок do, но мы можем идти дальше.

Здесь единственный шаг, который требует небольшой мысли (хотя, как только вы знакомы с монадами и функторами, это должно быть очевидно). Вышеупомянутая функция наводит на размышления об одном из законов монады : (m >>= return) == m. Единственное отличие состоит в том, что функция в правой части >>= не просто return - мы делаем что-то с объектом внутри монады, а затем завершаем ее в return. Но шаблон "делать что-то с завернутым значением без влияния на его оболочку" - это именно то, для чего Functor. Все монады являются функторами, поэтому мы можем реорганизовать это так, чтобы нам не понадобился экземпляр Monad:

main = fmap stringAnalyser (readFile "testFile.txt")

Наконец, обратите внимание, что <$> - это еще один способ записи fmap.

main = stringAnalyser <$> readFile "testFile.txt"

Я думаю, что эта версия намного понятнее исходного кода. Его можно прочитать как предложение: "main stringAnalyser применяется к результату чтения "testFile.txt"". Исходная версия влечет вас в процедурные подробности ее работы.


Добавление: мой комментарий о том, что "все монады являются функторами", на самом деле может быть оправдан наблюдением, что m >>= (return . f) (как стандартная библиотека liftM) совпадает с fmap f m. Если у вас есть экземпляр Monad, вы получите экземпляр Functor 'бесплатно' - просто определите fmap = liftM! Если кто-то определил экземпляр Monad для своего типа, но не экземпляры для Functor и Applicative, я бы назвал это ошибкой. Клиенты ожидают, что смогут использовать методы Functor для экземпляров Monad без лишних хлопот.

Ответ 4

Аппликативный стиль следует поощрять, потому что он сочиняет (и это красивее). Монадический стиль необходим в определенных случаях. См. fooobar.com/questions/63344/... для подробного объяснения.

Ответ 5

Должны ли мы избегать обозначения в любом случае?

Я бы сказал определенно нет. Для меня наиболее важным критерием в таких случаях является , чтобы сделать код максимально понятным и понятным. Была введена опция do, чтобы сделать монадический код более понятным, и это важно. Конечно, во многих случаях использование Applicative точечной нотации очень хорошо, например, вместо

do
    f <- [(+1), (*7)]
    i <- [1..5]
    return $ f i

мы будем писать только [(+1), (*7)] <*> [1..5].

Но есть много примеров, когда не использовать do -notation сделает код очень нечитаемым. Рассмотрим этот пример:

nameDo :: IO ()
nameDo = do putStr "What is your first name? "
            first <- getLine
            putStr "And your last name? "
            last <- getLine
            let full = first++" "++last
            putStrLn ("Pleased to meet you, "++full++"!")

Здесь совершенно ясно, что происходит и как действия IO упорядочиваются. A do -бесконечная нотация выглядит как

name :: IO ()
name = putStr "What is your first name? " >>
       getLine >>= f
       where
       f first = putStr "And your last name? " >>
                 getLine >>= g
                 where
                 g last = putStrLn ("Pleased to meet you, "++full++"!")
                          where
                          full = first++" "++last

или как

nameLambda :: IO ()
nameLambda = putStr "What is your first name? " >>
             getLine >>=
             \first -> putStr "And your last name? " >>
             getLine >>=
             \last -> let full = first++" "++last
                          in  putStrLn ("Pleased to meet you, "++full++"!")

которые обе менее читаемы. Конечно, здесь do -нотация здесь намного предпочтительнее.

Если вы хотите избежать использования do, попробуйте структурировать свой код во множество небольших функций. В любом случае, это хорошая привычка, и вы можете уменьшить ваш блок do, чтобы содержать только 2-3 строки, которые можно затем красиво заменить на >>=, <$>, < * > `и т.д. Например, выше может быть переписана как

name = getName >>= welcome
  where
    ask :: String -> IO String
    ask s = putStr s >> getLine

    join :: [String] -> String
    join  = concat . intersperse " "

    getName :: IO String
    getName  = join <$> traverse ask ["What is your first name? ",
                                      "And your last name? "]

    welcome :: String -> IO ()
    welcome full = putStrLn ("Pleased to meet you, "++full++"!")

Это немного дольше и, возможно, немного понятно для начинающих Haskell (из-за intersperse, concat и traverse), но во многих случаях эти новые маленькие функции могут быть повторно использованы в других местах вашего код, который сделает его более структурированным и составным.


Я бы сказал, что ситуация очень похожа на то, использовать ли точечную нотацию или нет. Во многих случаях (например, в самом верхнем примере [(+1), (*7)] <*> [1..5]) точечная нотация замечательна, но если вы попытаетесь преобразовать сложное выражение, вы получите результаты, например

f = ((ite . (<= 1)) `flip` 1) <*>
     (((+) . (f . (subtract 1))) <*> (f . (subtract 2)))
  where
    ite e x y = if e then x else y

Мне понадобилось бы довольно много времени, чтобы понять это, не запустив код. [Спойлер ниже:]

f x = if (x <= 1) then 1 else f (x-1) + f (x-2)


Кроме того, почему большинство учебных пособий обучают IO с помощью do?

Потому что IO точно спроектирован для имитации императивных вычислений с побочными эффектами, поэтому их упорядочение с помощью do очень естественно.

Ответ 6

Обозначение

do - это просто синтаксический сахар. Во всех случаях можно избежать . Однако в некоторых случаях замена do на >>= и return делает код менее читаемым.

Итак, для ваших вопросов:

"В любом случае мы избежим оператора Do?".

Сосредоточьтесь на том, чтобы сделать ваш код понятным и читаемым. Используйте do, когда это помогает, избегайте этого в противном случае.

И у меня был другой вопрос: почему большинство учебников будут преподавать IO с помощью?

Поскольку do делает код IO более удобным для чтения во многих случаях.

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

Ответ 7

Обозначение do расширяется до выражения, используя функции (>>=) и (>>), и выражение let. Поэтому он не является частью ядра языка.

(>>=) и (>>) используются для объединения действий последовательно, и они необходимы, когда результат действия изменяет структуру следующих действий.

В примере, приведенном в вопросе, это не очевидно, поскольку существует только одно действие IO, поэтому не требуется секвенирование.

Рассмотрим, например, выражение

do x <- getLine
   print (length x)
   y <- getLine
   return (x ++ y)

который переводится на

getLine >>= \x ->
print (length x) >>
getLine >>= \y ->
return (x ++ y)

В этом примере обозначение do (или функции (>>=) и (>>)) необходимо для последовательности действий IO.

Так скоро или позже программисту это понадобится.