(Надеюсь, этот вопрос по теме - я попытался найти ответ, но не нашел окончательного ответа. Если это не по теме или уже дан ответ, модерируйте / удалите его.)
Я помню, как несколько раз слышал / читал полушутливые комментарии о том, что Haskell является лучшим императивным языком , что, конечно, звучит странно, поскольку Haskell обычно наиболее известен своими функциональными возможностями.
Итак, мой вопрос: какие качества / особенности (если таковые имеются) в Haskell дают основание оправдать то, что Haskell считается лучшим императивным языком - или это на самом деле скорее шутка?
Ответы:
Считаю это полуправдой. Haskell обладает удивительной способностью к абстракции, в том числе абстракцией над императивными идеями. Например, в Haskell нет встроенного императивного цикла while, но мы можем просто написать его, и теперь он есть:
while :: (Monad m) => m Bool -> m () -> m () while cond action = do c <- cond if c then action >> while cond action else return ()
Такой уровень абстракции труден для многих императивных языков. Это можно сделать в императивных языках с замыканиями; например. Python и C #.
Но Haskell также обладает (весьма уникальной) способностью характеризовать разрешенные побочные эффекты , используя классы Monad. Например, если у нас есть функция:
foo :: (MonadWriter [String] m) => m Int
Это может быть «императивная» функция, но мы знаем, что она может делать только две вещи:
Он не может печатать на консоли или устанавливать сетевые соединения и т. Д. В сочетании с возможностью абстракции вы можете писать функции, которые действуют на «любые вычисления, производящие поток» и т. Д.
На самом деле все дело в абстракционных способностях Haskell, которые делают его очень хорошим императивным языком.
Однако ложная половина - это синтаксис. Я считаю Haskell довольно многословным и неудобным для использования в императивном стиле. Вот пример вычисления в императивном стиле с использованием вышеуказанного
while
цикла, который находит последний элемент связанного списка:lastElt :: [a] -> IO a lastElt [] = fail "Empty list!!" lastElt xs = do lst <- newIORef xs ret <- newIORef (head xs) while (not . null <$> readIORef lst) $ do (x:xs) <- readIORef lst writeIORef lst xs writeIORef ret x readIORef ret
Весь этот мусор IORef, двойное чтение, необходимость привязать результат чтения, fmapping (
<$>
) для работы с результатом встроенного вычисления ... все это выглядит очень сложно. Это имеет большой смысл с функциональной точки зрения, но императивные языки, как правило, скрывают большинство этих деталей, чтобы упростить их использование.По общему признанию, возможно, если бы мы использовали
while
комбинатор другого стиля, он был бы чище. Но если вы зайдете в эту философию достаточно далеко (используя богатый набор комбинаторов, чтобы четко выразить себя), вы снова придете к функциональному программированию. Haskell в императивном стиле просто не «течет», как хорошо разработанный императивный язык, например, python.В заключение, с синтаксической подтяжкой лица Haskell вполне может быть лучшим императивным языком. Но по своей природе подтяжки лица заменят что-то внутренне красивое и настоящее на что-то внешне красивое и фальшивое.
РЕДАКТИРОВАТЬ : Сравните
lastElt
с этой транслитерацией python:def last_elt(xs): assert xs, "Empty list!!" lst = xs ret = xs.head while lst: ret = lst.head lst = lst.tail return ret
Такое же количество строк, но каждая строка имеет немного меньше шума.
ИЗМЕНИТЬ 2
Как бы то ни было, вот как выглядит чистая замена в Haskell:
lastElt = return . last
Вот и все. Или, если вы запретите мне использовать
Prelude.last
:lastElt [] = fail "Unsafe lastElt called on empty list" lastElt [x] = return x lastElt (_:xs) = lastElt xs
Или, если вы хотите, чтобы он работал с любой
Foldable
структурой данных и понимал, что вам на самом деле не нужноIO
обрабатывать ошибки:import Data.Foldable (Foldable, foldMap) import Data.Monoid (Monoid(..), Last(..)) lastElt :: (Foldable t) => t a -> Maybe a lastElt = getLast . foldMap (Last . Just)
с
Map
, например:λ➔ let example = fromList [(10, "spam"), (50, "eggs"), (20, "ham")] :: Map Int String λ➔ lastElt example Just "eggs"
(.)
Оператор функциональной композиции .источник
IORef
s неявным , или, по крайней мере, пытается это сделать, и ему мешают изменения в GHC. : [Это не шутка, и я этому верю. Я постараюсь сделать это доступным для тех, кто не знает Haskell. Haskell использует do-notation (среди прочего), чтобы вы могли писать императивный код (да, он использует монады, но не беспокойтесь об этом). Вот некоторые из преимуществ, которые дает вам Haskell:
Простое создание подпрограмм. Допустим, я хочу, чтобы функция выводила значение в stdout и stderr. Я могу написать следующее, определяя подпрограмму одной короткой строкой:
do let printBoth s = putStrLn s >> hPutStrLn stderr s printBoth "Hello" -- Some other code printBoth "Goodbye"
Легко передавать код. Учитывая, что я написал выше, если я теперь хочу использовать
printBoth
функцию для распечатки всего списка строк, это легко сделать, передав мою подпрограммуmapM_
функции:mapM_ printBoth ["Hello", "World!"]
Другой пример, хотя и не обязательный, - это сортировка. Допустим, вы хотите отсортировать строки только по длине. Ты можешь написать:
sortBy (\a b -> compare (length a) (length b)) ["aaaa", "b", "cc"]
Что даст вам ["b", "cc", "aaaa"]. (Вы можете написать это и короче, но пока неважно.)
Легко использовать повторно код. Эта
mapM_
функция используется очень часто и заменяет циклы for-each в других языках. Также есть функции,forever
которые действуют как while (true), и различные другие функции, которым можно передавать код и выполнять его по-разному. Таким образом, циклы на других языках заменяются этими управляющими функциями в Haskell (которые не являются специальными - вы можете очень легко определить их сами). В общем, это затрудняет неправильное выполнение условия цикла, так же, как циклы for-each труднее ошибиться, чем эквиваленты длинных итераторов (например, в Java) или циклы индексации массивов (например, в C).Имеются побочные эффекты. Допустим, я хочу прочитать строку из stdin и записать ее на stdout после применения к ней некоторой функции (мы назовем ее foo). Ты можешь написать:
do line <- getLine putStrLn (foo line)
Я сразу понимаю, что у
foo
этого нет никаких неожиданных побочных эффектов (например, обновление глобальной переменной, освобождение памяти или что-то еще), потому что его тип должен быть String -> String, что означает, что это чистая функция; какое бы значение я ни передал, он должен каждый раз возвращать один и тот же результат без побочных эффектов. Haskell прекрасно отделяет код с побочными эффектами от чистого кода. В чем-то вроде C или даже Java это неочевидно (меняет ли состояние метода getFoo () состояние? Вы бы надеялись, что нет, но это может случиться ...).Вероятно, есть еще несколько преимуществ, но это те, которые приходят на ум.
источник
В дополнение к тому, что уже упоминалось другими, иногда полезно иметь первоклассные побочные действия. Вот глупый пример, демонстрирующий идею:
f = sequence_ (reverse [print 1, print 2, print 3])
В этом примере показано, как вы можете создавать вычисления с побочными эффектами (в этом примере
print
), а затем помещать их в структуры данных или манипулировать ими другими способами до их фактического выполнения.источник
call = x => x(); sequence_ = xs => xs.forEach(call) ;print = console.log; f = () => sequence_([()=> print(1), () => print(2), () => print(3)].reverse())
. Основное отличие, которое я вижу, в том, что нам нужно несколько дополнительных() =>
.