Как мързеливата оценка принуди Haskell да бъде чист

Спомням си, че видях презентация, в която SPJ каза, че мързеливата оценка ги е принудила да поддържат Haskell чист (или нещо подобно). Често виждам много хаскелери да казват същото.

И така, бих искал да разбера как стратегията за мързелива оценка ги е принудила да поддържат Haskell чист, за разлика от стриктната стратегия за оценка?


person Sibi    schedule 17.07.2015    source източник
comment
Ако имате мързелива оценка, това означава, че всички нечисти действия се случват в произволен ред. Това донякъде спира хората, които се опитват да промъкнат нечисти действия; действията, които се случват в произволен ред, не са много полезни.   -  person MathematicalOrchid    schedule 17.07.2015
comment
@MathematicalOrchid Не разбирам това... Haskell вече разделя нечистите действия в IO монадата, така че те се подреждат независимо от стратегията за оценка...   -  person Bakuriu    schedule 17.07.2015
comment
@Bakuriu Haskell разделя нечистите действия в IO монадата защото в противен случай подреждането им би било напълно произволно, което е поради на мързелива оценка. Ако не беше мързеливата оценка, бихте могли просто да мамите и да пишете преструващи се функции, които всъщност са нечисти. Но поради мързела е безнадеждно да опитвате, колкото и да сте изкушени.   -  person MathematicalOrchid    schedule 17.07.2015
comment
Както и да е, едно нещо за мързеливото срещу стриктното оценяване е, че при мързеливото оценяване можете да мислите по-композиционно (т.е. по по-декларативен/функционален начин) и да получите ефективно решение. Използването на стриктна стратегия за оценка може да не е възможно. Например с мързелива оценка можете да правите неща като: getFirstTrue p = head . filter p без да се притеснявате, че ще премине през целия списък, преди да върне първия елемент. Ако използвате строга оценка, искате да избегнете това нещо. Това става по-важно с неща като гънки/карти и т.н., смесени заедно.   -  person Bakuriu    schedule 17.07.2015
comment
@MathematicalOrchid От моя гледна точка Io има смисъл да се разделят чистите и нечистите независимо от стратегията за оценка. Умно е да го направите дори на строг език. Фактът, че решава дори този проблем, е просто плюс.   -  person Bakuriu    schedule 17.07.2015
comment
@Bakuriu Разделянето им е много умен ход. Мисълта ми е, че мързеливата оценка някак си принуждава да го направите, докато на строг език може да се изкушите да заобиколите раздялата само този път, което бавно води до това, че сте през цялото време. И мисля, че това са настроенията, за които ОП пита.   -  person MathematicalOrchid    schedule 17.07.2015
comment
@Sibi Не е толкова много, че мързеливата оценка води до чистота; по-скоро мързеливата оценка ги принуди да поддържат Haskell чист. За информация, на някакъв етап в Подкаст за радио софтуерно инженерство #108, SPJ обяснява защо чистотата ни поддържаше чисти.   -  person jub0bs    schedule 17.07.2015
comment
@Jubobs Съжалявам, трябваше да го формулирам правилно. Бих искал да разбера как мързеливата оценка ги е принудила да поддържат Haskell чист. Ще редактирам въпроса.   -  person Sibi    schedule 17.07.2015


Отговори (4)


Мисля, че отговорът от Jubobs вече го обобщава добре (с добри препратки). Но, по моите собствени думи, това, което според мен имат предвид SPJ и приятели, е следното:

Да се ​​наложи да преминете през този бизнес с „монади“ понякога може да бъде наистина неудобно. Огромният брой въпроси в Stack Overflow, питащи "как просто да премахна това IO нещо?" е доказателство за факта, че понякога наистина наистина искате просто да отпечатате тази една стойност точно тук, обикновено за целите на разгадаването какво всъщност се случва!

В един нетърпелив език би било ужасно изкушаващо просто да започнете да добавяте магически нечисти функции, които ви позволяват да правите нечисти неща директно, както в други езици. Без съмнение бихте започнали с малки неща в началото, но бавно се плъзгате по този хлъзгав склон и преди да се усетите, ефектите са навсякъде.

В мързелив език като Haskell това изкушение все още съществува. Има много случаи, когато би било наистина полезно да можете бързо да промъкнете този малък ефект тук или там. Освен че поради мързела добавянето на ефекти се оказва почти напълно безполезно. Не можете да контролирате кога се случва нещо. Дори само Debug.trace води до крайно неразбираеми резултати.

Накратко, ако проектирате мързелив език, наистина сте принудени да измислите последователна история за това как боравите с ефектите. Не можете просто да кажете „мм, ще се преструваме, че тази функция е просто магия“; без способността да контролирате по-точно ефектите, незабавно ще се окажете в ужасна бъркотия!

TL;DR На нетърпелив език можете да се разминете с измама. На мързелив език, наистина трябва да правите нещата както трябва, или просто не работи.

И ето защо наехме Алекс изчакайте, грешен прозорец...

person MathematicalOrchid    schedule 17.07.2015
comment
От друга гледна точка измамата е много по-ограничена и следователно много по-интересна. Често отнема години, преди програмистът на Haskell да развие прилично усещане за кога и как да мами! - person dfeuer; 23.09.2020

Не е толкова важно, че мързеливата оценка води до чистота; Haskell беше чист като начало. По-скоро мързеливата оценка принуди дизайнерите на езика да запазят езика чист.

Ето подходящ пасаж от статията История на Haskell: да бъдеш мързелив с класа:

След като бяхме ангажирани с мързелив език, чистият беше неизбежен. Обратното не е вярно, но е забележимо, че на практика повечето чисти езици за програмиране също са мързеливи. Защо? Тъй като в езика за извикване по стойност, независимо дали е функционален или не, изкушението да се позволят неограничени странични ефекти вътре във „функция“ е почти неустоимо.

Чистотата е голям залог с всепроникващи последици. Неограничените странични ефекти несъмнено са много удобни. При липсата на странични ефекти входът/изходът на Haskell първоначално беше болезнено тромав, което беше източник на значително смущение. Тъй като необходимостта е майка на изобретението, този срам в крайна сметка доведе до изобретяването на монадичен I/O, който сега считаме за един от основните приноси на Haskell към света, както обсъждаме по-подробно в Раздел 7.

Дали един чист език (с монадични ефекти) в крайна сметка е най-добрият начин за писане на програми все още е отворен въпрос, но със сигурност е радикална и елегантна атака срещу предизвикателството на програмирането и именно тази комбинация от сила и красота мотивира дизайнери. В ретроспекция, следователно, може би най-голямата полза от мързела не е мързелът сам по себе си, а по-скоро това, че мързелът ни запази чисти и по този начин мотивира много продуктивна работа върху монадите и капсулираното състояние.

(мое ударение)

Също така ви каня да слушате 18'30'' от Подкаст за радио софтуерно инженерство #108 за обяснение от самия човек. И ето един по-дълъг, но уместен пасаж от интервюто на SPJ в Кодерите на работа на Peter Seibel :

Сега мисля, че важното нещо за мързела е, че той ни поддържа чисти. [...]

[...] ако имате мързелив оценител, е по-трудно да предвидите точно кога един израз ще бъде оценен. Така че това означава, че ако искате да отпечатате нещо на екрана, всеки език за извикване по стойност, където редът на оценка е напълно ясен, прави това, като има нечиста „функция“ — поставям я в кавички, защото сега изобщо не е функция—с тип нещо като низ към единица. Извиквате тази функция и като страничен ефект тя поставя нещо на екрана. Това се случва в Lisp; случва се и в ML. Случва се по същество във всеки език за извикване по стойност.

Сега на чист език, ако имате функция от низ към единица, никога няма да е необходимо да я извиквате, защото знаете, че тя просто дава единицата за отговор. Това е всичко, което една функция може да направи, е да ви даде отговор. И знаете какъв е отговорът. Но разбира се, ако има странични ефекти, много е важно да го извикате. На мързелив език проблемът е, ако кажете „f приложено към print "hello"“, тогава дали f оценява първия си аргумент не е очевидно за извикващия функцията. Това е нещо общо с вътрешностите на функцията. И ако му подадете два аргумента, f от print "hello" и print "goodbye", тогава можете да отпечатате единия или и двата в който и да е ред или нито един от тях. Така че по някакъв начин, с мързелива оценка, правенето на вход/изход чрез страничен ефект просто не е осъществимо. Не можете да пишете разумни, надеждни, предвидими програми по този начин. Така че трябваше да се примирим с това. Наистина беше малко неудобно, защото всъщност не можехте да правите никакви входни/изходни данни, за които да говорите. Така че дълго време по същество имахме програми, които можеха просто да приемат низ към низ. Това направи цялата програма. Входният низ беше входът, а низът с резултат беше изходът и това е всичко, което програмата наистина можеше да направи.

Може да станете малко по-умни, като накарате изходния низ да кодира някои изходни команди, които са интерпретирани от външен интерпретатор. Така че изходният низ може да каже: „Отпечатайте това на екрана; запишете това на диска. Един преводач всъщност би могъл да направи това. Така че си представяте, че функционалната програма е хубава и чиста и има нещо като този зъл интерпретатор, който интерпретира низ от команди. Но тогава, разбира се, ако прочетете файл, как да върнете входа обратно в програмата? Е, това не е проблем, защото можете да изведете низ от команди, които се интерпретират от злия интерпретатор и използвайки мързелива оценка, той може да изхвърли резултатите обратно във входа на програмата. Така програмата сега приема поток от отговори на поток от заявки. Потокът от молби отива към злия преводач, който прави нещата със света. Всяка заявка генерира отговор, който след това се връща обратно към входа. И тъй като оценката е мързелива, програмата е излъчила отговор точно навреме, за да премине през цикъла и да бъде консумирана като вход. Но беше малко крехко, защото ако консумирате отговора си твърде нетърпеливо, тогава ще получите някакъв вид задънена улица. Защото ще поискате отговор на въпрос, който все още не сте изплюли от задната си част.

Смисълът на това е, че мързелът ни доведе до ъгъла, в който трябваше да измислим начини да заобиколим този I/O проблем. Мисля, че това беше изключително важно. Единственото най-важно нещо за мързела беше, че той ни доведе дотам.

(мое ударение)

person jub0bs    schedule 17.07.2015
comment
Първата връзка вече е прекъсната. - person Max Barraclough; 22.09.2020
comment
@MaxBarraclough Благодаря. Намерих друго копие на сървърите на Microsoft и поправих повредената връзка. - person jub0bs; 23.09.2020

Зависи какво имате предвид под „чист“ в този контекст.

  • Ако за чист имате предвид както в чисто функционален, тогава това, което @MathematicalOrchid е вярно: с мързелива оценка не бихте знаели в коя последователност се изпълняват нечистите действия и следователно не бихте изобщо не можете да пишете смислени програми и сте принудени да бъдете по-чисти (използвайки монадата IO).

    Въпреки това намирам това за неудовлетворително в този случай. Един истински функционален език вече разделя чистия и нечистия код, така че дори стриктният трябва да има някакъв вид IO.

  • Въпреки това е възможно изявлението да е по-широко и да се отнася до чистото във факта, че можете да изразите кода по-лесно, по по-декларативен, композиционен и същевременно ефективен начин.

Разглеждайки този отговор, който прави точно изявлението, което цитирате, се свързва към статията Защо функционалното програмиране има значение от Хюз, който вероятно е този, за който говорите.

Документът показва как функциите от по-висок ред и мързеливата оценка позволяват да се напише по-модулен код. Обърнете внимание, че не казва нищо за чисто функционални и т.н. Смисълът, който прави, е да бъдете по-модулен и по-декларативен, без да губите ефективност.

Статията предоставя някои примери. Например алгоритъмът на Нютон-Рафсън: на строг език трябва да комбинирате заедно кода, който изчислява следващото приближение, и този, който проверява дали сте получили достатъчно добро решение.

С мързелива оценка можете да генерирате безкраен списък от приближения във функция и да го извикате от друга функция, която връща първото достатъчно добро приближение.


В Функционално мислене с Haskell Ричард Бърд отбелязва точно това. Ако разгледаме Глава 2, упражнение D:

Бийвър е нетърпелив оценител, докато Сюзън е мързелива.

[...]

Каква алтернатива може да предпочете Бийвър пред head . filter p . map f?

И отговорът гласи:

[...] Вместо да дефинира first p f = head . filter p . map f, Бобър може да дефинира

first :: (b -> Bool) -> (a -> b) -> [a] -> b
first p xs | null xs = error "Empty list"
           | p x = x
           | otherwise = first p f (tail xs)
           where x = f (head xs)

Въпросът е, че при нетърпелива оценка повечето функции трябва да бъдат дефинирани чрез изрична рекурсия, а не от гледна точка на полезни компонентни функции като map и filter.

Толкова чисто тук означава, че позволява декларативни, композиционни и същевременно ефективни дефиниции, докато при нетърпелива оценка използването на декларативни и композиционни дефиниции може да доведе до ненужно неефективен код.

person Bakuriu    schedule 17.07.2015
comment
Ще информирам Lispers, MLers и Discipline Disciplers, че техните предпочитани езици не са истински функционални езици. Мисля, че просто разбирате погрешно историческата полезност и ролята на функционалната чистота. - person Jonathan Cast; 17.07.2015
comment
@jcast Е, зависи какво разбирате под функционален език. Искам да кажа, че Haskell е проектиран да бъде много функционален и най-голямата стъпка в тази посока е пълното разделение между страничните ефекти и оценката на изразите. Така че, да, определено бих казал, че тези езици са по-малко функционални от Haskell, но не мисля, че Haskell е по-чист само защото се оценява мързеливо. - person Bakuriu; 17.07.2015
comment
@Bakuriu Те са по-малко функционални езици, защото няма езикова поддръжка за писане на чист код. Те все пак позволяват да пишете чисто функционален код; И всъщност това е как се пишат значителни части от кода. - person Cubic; 17.07.2015
comment
@Cubic ‹serious›Indeed.‹/serious› ‹flippant›C също ви позволява да пишете чисто функционален код.‹/flippant›‹admittedly›Няма много чисто функционален C наоколо.‹/admittedly› - person AndrewC; 20.07.2015

Строго погледнато, това твърдение не е вярно, защото Haskell има unsafePerformIO, което е голяма дупка във функционалната чистота на езика. (Той използва дупка във функционалната чистота на GHC Haskell, която в крайна сметка се връща към решението да се приложи неопакована аритметика чрез добавяне на строг фрагмент към езика). unsafePerformIO съществува, защото изкушението да се каже "добре, ще внедря само тази функция, използвайки вътрешни странични ефекти" е неустоимо за повечето програмисти. Но ако погледнете недостатъците на unsafePerformIO[1], ще видите какво точно казват хората:

  • unsafePerformIO a не е гарантирано, че някога ще изпълни a.
  • Нито пък е гарантирано изпълнението на a само веднъж, ако се изпълни.
  • Нито има гаранция за относителното подреждане на I/O, извършвано от a с други части на програмата.

Тези недостатъци държат unsafePerformIO най-вече ограничено до най-безопасните и най-внимателни употреби и защо хората използват IO директно, докато не стане твърде неудобно.

[1] Освен типовата небезопасност; let r :: IORef a; r = unsafePerformIO $ newIORef undefined ви дава полиморфен r :: IORef a, който може да се използва за внедряване на unsafeCast :: a -> b. ML има решение за разпределение на референции, което избягва това и Haskell можеше да го реши по подобен начин, ако чистотата така или иначе не се считаше за желателна (ограничението на мономорфизма така или иначе е почти решение, просто трябва да забраните на хората да работят около него чрез използвайки подпис на типа, както направих по-горе).

person Jonathan Cast    schedule 17.07.2015
comment
Повечето употреби на unsafePerformIO, които съм виждал, дори не са за IO, а за ситуации, в които ST не пасва съвсем (например: едновременни алгоритми, които винаги връщат един и същ резултат при едни и същи аргументи, но които не могат да бъдат доказани за да го направите само с помощта на системата от типове) - person Jeremy List; 20.07.2015