@ chepner уже включил в свой ответ довольно много того, что я бы сказал, но я хотел бы sh подчеркнуть другой аспект, который, как мне кажется, весьма уместен в этом вопросе: тот факт, что запись do
является для большинства разработчиков гораздо более простой и понятный способ работы с любым выражением monadi c, которое хотя бы в меру сложное.
Причина этого в том, что почти чудесным образом do
блоки в конечном итоге очень напоминают код, написанный на императивном языке. Код императивного стиля гораздо легче понять для большинства разработчиков, и не только потому, что он является самой распространенной парадигмой: он дает явный «рецепт» того, что делает кусок кода, тогда как более типичные выражения Haskell, особенно монади c, включающие вложенные лямбды и >>=
повсюду, очень легко понять трудно.
Говоря об этом, я, конечно, не имею в виду, что нужно кодировать на императивном языке, а не Haskell , Преимущества чисто функционального стиля хорошо документированы и, по-видимому, хорошо понятны ОП, поэтому я не буду вдаваться в них здесь. Но нотация Haskell do
позволяет писать код в императивно-выглядящем «стиле», который, следовательно, является явным и его легче понять - по крайней мере, в небольшом масштабе - не жертвуя ни одним из преимуществ использования чистого функциональный язык.
Этот "императивный стиль" нотации do
, я чувствую, более заметен для некоторых монад, чем для других, и я sh проиллюстрирую свою точку зрения на примерах из нескольких монад, которые Я нахожу костюм "императивным стилем" хорошо. Во-первых, IO
, где я могу привести такой простой пример:
greet :: IO ()
greet = do
putStrLn "Hello, what is your name?"
name <- readLine
putStrLn $ "Pleased to meet you, " ++ name ++ "!"
Надеюсь, сразу становится очевидно, что делает этот код при выполнении в среде выполнения Haskell. sh Подчеркну, насколько он похож на императивный код, например, этот перевод Python (который не самый идиоматический c, но был выбран так, чтобы точно соответствовать строке кода Haskell для строка).
def greet():
print("Hello, what is your name?")
name = input()
print("Pleased to meet you, " + name + "!")
Теперь спросите себя, насколько легко будет понять код в его отлаженном виде, без do
?
greet = putStrLn "Hello, what is your name?" >> readLine >>= \name -> putStrLn $ "Pleased to meet you, " ++ name ++ "!"
Это не особенно сложно, предоставлено - но я надеюсь, что вы согласны с тем, что он гораздо более "шумный", чем блок do
выше. Я не могу говорить за других, но я очень сомневаюсь, что я один, говоря, что последняя версия может занять у меня 10-20 секунд или около того, чтобы полностью понять, тогда как блок do
мгновенно понятен. И это, конечно, чрезвычайно простое действие - что-либо более сложное, что встречается во многих реальных приложениях, значительно увеличивает разницу в понятности.
Я, конечно, выбрал IO по причине - думаю, именно при работе с IO, в частности, наиболее естественно думать в терминах «выполнить это действие, а затем, если в результате получится следующее действие, в противном случае ...». Хотя семантика монады IO
идеально подходит для этого, гораздо проще преобразовать код во что-то подобное, когда он написан в квазиимперативной записи, чем использовать >>=
напрямую. И запись do
проще записать тоже.
Но хотя IO
является ярким примером этого, он, конечно, не единственный. Другой замечательный пример - монада State
. Вот простой пример его использования для нахождения суммы в списке целых чисел (и я знаю, что на самом деле вы бы этого не делали, но это всего лишь очень простой пример не совсем тривиального кода, который использовал эту монаду) :
sumList :: State [Int] Int
sumList = go 0
where go subtotal = do
remaining <- get
case remaining of
[] -> return subtotal
(x:xs) -> do
put xs
go $ subtotal + X
Здесь, на мой взгляд, шаги очень понятны - вспомогательная функция go
последовательно добавляет первый элемент списка к промежуточной сумме, одновременно обновляя внутреннее состояние хвостом список. Если списка больше нет, возвращается промежуточный итог. (Учитывая вышеизложенное, функция evalState sumList
будет принимать фактический список и суммировать его.)
Вероятно, можно придумать более удачные примеры (особенно те, в которых расчеты не так просты для выполнения другими способами), но я надеюсь, что все еще ясно: переписав вышеприведенное с помощью >>=
, и лямбда сделает его намного менее понятным.
do
нотация, по моему мнению, почему часто цитируемая шутка о том, что Haskell является "самым лучшим императивным языком в мире", имеет больше, чем зерно правды. Используя и определяя разные монады, можно написать легко понятный «императивный» код в самых разных ситуациях - при этом сохраняя гарантии того, что различные функции не могут, например, изменить глобальное состояние. Во многом это лучшее из обоих миров.