Чому Хаскелла (іноді) називають «найкращою імперативною мовою»?


83

(Сподіваюся, це запитання є тематичним - я намагався знайти відповідь, але не знайшов остаточної відповіді. Якщо це трапляється поза темою або на нього вже відповіли, будь ласка, модеруйте / видаліть його.)

Я пам’ятаю, що кілька разів чув / читав напівжартома коментар про те, що Хаскелл є найкращою імперативною мовою , що, звичайно, звучить дивно, оскільки Хаскелл зазвичай найбільш відомий своїми функціональними особливостями.

Отже, моє запитання полягає в тому, які якості / особливості (якщо такі є) Haskell дають підставу виправдовувати Haskell, який вважається найкращою імперативною мовою - чи це насправді жарт?


3
donsbot.wordpress.com/2007/03/10/… <- Програмована крапка з комою.
vivian

10
Ця цитата, мабуть, бере початок із кінця Введення в боротьбі з незграбним загоном: монадичного введення / виведення, паралельності, винятків та дзвінків на іноземній мові в Haskell, де сказано: "Коротше кажучи, Haskell - це найважливіша мова програмування у світі".
Рассел О'Коннор,

@Russel: спасибі, що вказали на найбільш ймовірне походження (як, здається, сам SPJ) цієї приказки!
hvr

ви можете зробити суворий імперативний ОО з Хаскеллом
Янус Трольсен

Відповіді:


90

Я вважаю це напівправдою. Хаскелл має дивовижну здатність абстрагуватися, і це включає абстракцію над імперативними ідеями. Наприклад, у 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

Він не може друкувати на консолі, встановлювати мережеві підключення тощо. У поєднанні з можливістю абстрагування ви можете писати функції, які діють на "будь-яке обчислення, яке створює потік" тощо.

Насправді все, що стосується абстракційних здібностей Хаскелла, робить його дуже тонкою імперативною мовою.

Однак помилкова половина - це синтаксис. Я вважаю Хаскелла досить багатослівним і незручним для використання в імперативному стилі. Ось приклад обчислення в імперативному стилі з використанням наведеного вище 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.

На закінчення, із синтаксичним підтяжкою обличчя Хаскелл цілком може бути найкращою імперативною мовою. Але, за характером підтяжки обличчя, це було б заміною чогось внутрішнього красивого та справжнього чимось зовні гарним та підробленим.

EDIT : Контраст 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

Для чого це варте, ось як виглядає чиста заміна в 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"

(.)Оператор функціональної композиції .


2
Ви можете зробити шум IORef набагато менш дратівливим, зробивши більше абстракцій.
серпень

1
@augustss, хм, мені це цікаво. Ви маєте на увазі більше абстракцій на рівні домену, або просто будуючи більш багату імперативну підмову ". З першими, я згоден - але мій розум пов'язує імперативне програмування з низькою абстракцією (моя робоча гіпотеза полягає в тому, що зі збільшенням абстракції стиль Що стосується останнього, мені було б дуже цікаво дізнатись, що ви маєте на увазі, тому що я не можу подумати, як з голови.
luqui

2
@luqui Використання ST було б хорошим прикладом для характеристики дозволених побічних ефектів. Як бонус можна повернутися до чистого обчислення від ST.
fuz

4
Використання Python як порівняння не зовсім справедливе - як ви кажете, воно добре продумане, одна із синтаксично найчистіших імперативних мов, з якою я знайомий. Те саме порівняння стверджує, що більшість імперативних мов незручно використовувати в імперативному стилі ... хоча, можливо, це саме те, що ви мали на увазі. ;]
CA McCann

5
Виноска до бесіди для нащадків: @augustss, використовуючи спеціальний поліморфізм, щоб зробити його IORefнеявним , або, принаймні, намагаючись і перешкодити змінам GHC. : [
CA McCann

22

Це не жарт, і я в це вірю. Я постараюся зробити це доступним для тих, хто не знає жодного Хаскелла. 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така функція, яка діє деякий час (істинно), та різні інші функції, котрі можна передавати код і виконувати його різними способами. Тож цикли іншими мовами замінюються цими функціями управління в Haskell (які не є особливими - їх можна визначити самостійно дуже легко). Взагалі, це ускладнює помилку в циклі, так само як і для кожного циклу важче помилитися, ніж довгий еквівалент ітераторів (наприклад, на Java) або цикли індексації масивів (наприклад, на С).

  • Прив'язка не присвоєння. В основному, ви можете призначити змінну лише один раз (скоріше, як одне статичне призначення). Це усуває багато плутанини щодо можливих значень змінної в будь-якій заданій точці (її значення встановлюється лише в одному рядку).
  • Містить побічні ефекти. Скажімо, я хочу прочитати рядок із stdin і записати його на stdout після застосування до нього якоїсь функції (ми будемо називати це foo). Ви можете написати:

    do line <- getLine
       putStrLn (foo line)
    

    Я відразу знаю, що fooне має будь-яких несподіваних побічних ефектів (наприклад, оновлення глобальної змінної, або вивільнення пам'яті, або що завгодно), оскільки його тип має бути String -> String, що означає, що це чиста функція; яке б значення я не передав, воно повинно кожного разу повертати однаковий результат, без побічних ефектів. Хаскелл красиво відокремлює побічний код від чистого коду. У чомусь на кшталт C, або навіть Java, це не очевидно (чи метод getFoo () змінює стан? Ви сподіваєтесь, що ні, але це може зробити ...).

  • Вивіз сміття. Багато мов сміття збирається в наші дні, але варто згадати: жодних клопотів з виділенням та вивільненням пам'яті.

Окрім цього, мабуть, є ще кілька переваг, але саме ці приходять мені на думку.


9
Я б додав до сильного типу безпеки. Haskell дозволяє компілятору усувати великий клас помилок. Після недавньої роботи над деяким кодом Java мені нагадали, наскільки жахливими є нульові вказівники та скільки OOP відсутнє без типів сум.
Michael Snoyman

1
Дякуємо за розробку! Ваші згадані переваги зводиться до того, що Haskell розглядає `` імперативні '' ефекти як першокласні об'єкти (які, таким чином, можна поєднати) разом із можливістю `` утримувати '' ці ефекти до обмеженого обсягу. Це адекватний стислий резюме?
hvr

19
@Michael Snoyman: Але типи суми в ООП прості! Просто визначте абстрактний клас, який представляє церковне кодування вашого типу даних, підкласи для справ, інтерфейси для класів, які можуть обробляти кожен випадок, а потім передайте об’єкти, що підтримують кожен інтерфейс, об’єкту суми, використовуючи політип підтипу для потоку управління (як ти повинен). Не може бути простіше. Чому ви ненавидите шаблони дизайну?
CA McCann,

9
@camccann Я знаю, що ти жартуєш, але це по суті те, що я реалізував у своєму проекті.
Michael Snoyman

9
@ Michael Snoyman: Тоді хороший вибір! Справжній жарт полягає в тому, що я описував, що є майже найкращим кодуванням, таким чином, що звучало як жарт. Ха-ха! Сміючись аж до шибениці ...
CA McCann

16

На додаток до того, що вже згадували інші, іноді корисно мати побічні дії, які мають бути першокласними. Ось безглуздий приклад, щоб продемонструвати ідею:

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()). Головна відмінність, яку я бачу, полягає в тому, що нам потрібно кілька додаткових () =>.
Hjulle
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.