Почему Haskell (иногда) упоминается как «Лучший императивный язык»? - PullRequest
81 голосов
/ 08 июля 2011

(Я надеюсь, что этот вопрос является тематическим - я пытался найти ответ, но не нашел однозначного ответа. Если это не по теме или уже дан ответ, просьба модерировать / удалить его.)

Я помню, что слышал / читал полушутливый комментарий о том, что Haskell был лучшим императивным языком несколько раз, что, конечно, звучит странно, поскольку Haskell обычно лучше всего известенего функциональные функции.

Так что мой вопрос в том, какие качества / особенности (если таковые имеются) Haskell дают основания для оправдания того, что Haskell считается лучшим императивным языком -или это на самом деле больше шутка?

Ответы [ 3 ]

87 голосов
/ 08 июля 2011

Я считаю это полуправдой. 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

Это может быть «императивная» функция, но мы знаем, что она может делать только две вещи:

  • «Вывод» потока строк
  • вернуть Int

Он не может печатать на консоль или устанавливать сетевые подключения и т. Д. В сочетании с возможностью абстрагирования вы можете писать функции, которые действуют на «любое вычисление, создающее поток» и т. Д.

Это действительно все о способностях абстракции Хаскелла, что делает его очень хорошим императивным языком.

Однако ложная половина - это синтаксис. Я нахожу 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 в императивном стиле просто не «течет», как хорошо продуманный императивный язык, например питон.

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

РЕДАКТИРОВАТЬ : Контраст lastElt с этой транслитерацией питона:

def last_elt(xs):
    assert xs, "Empty list!!"
    lst = xs
    ret = xs.head
    while lst:
        ret = lst.head
        lst = lst.tail
    return ret 

Одинаковое количество строк, но в каждой строке уровень шума немного меньше.


РЕДАКТИРОВАТЬ 2

Что стоит, вот как выглядит замена pure в 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"

Оператор (.) - это композиция функций .

22 голосов
/ 08 июля 2011

Это не шутка, и я верю в это. Я постараюсь сделать это доступным для тех, кто не знает Хаскелла. 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 () меняет состояние? Можно надеяться, что нет, но это возможно ...).

  • Сборка мусора. В наши дни многие языки собираются мусором, но стоит упомянуть: нет проблем с выделением и освобождением памяти.

Кроме того, вероятно, есть еще несколько преимуществ, но это те, которые приходят на ум.

15 голосов
/ 08 июля 2011

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

f = sequence_ (reverse [print 1, print 2, print 3])

В этом примере показано, как вы можете создавать вычисления с побочными эффектами (в этом примере print), а затем помещать в структуры данных или манипулировать ими вдругие способы, прежде чем их выполнять.

...