Які правила існують щодо функції a -> (), що оцінюється в Haskell?


12

Як заголовок: як є гарантія оцінювання блоку повернення функції Haskell? Можна подумати, що немає необхідності проводити будь-яку оцінку в такому випадку, компілятор міг би замінити всі такі виклики на негайне ()значення, якщо немає явних запитів на суворість, і в цьому випадку код може вирішити, чи слід повернення ()або дно.
Я експериментував з цим у GHCi, і, схоже, відбувається навпаки, тобто така функція, схоже, оцінюється. Дуже примітивний приклад був би

f :: a -> ()
f _ = undefined

Оцінювання f 1кидає помилку через наявність undefined, тому певна оцінка точно відбувається. Не ясно, наскільки глибоко йде оцінка; іноді, схоже, йдеться настільки глибоко, як це необхідно для оцінки всіх дзвінків до функцій, що повертаються (). Приклад:

g :: [a] -> ()
g [] = ()
g (_:xs) = g xs

Цей код замикається на невизначений термін, якщо він представлений g (let x = 1:x in x). Але потім

f :: a -> ()
f _ = undefined
h :: a -> ()
h _ = ()

може бути використаний для показу, що h (f 1)повертається (), тому в цьому випадку оцінюються не всі одиничні вираження під вираженнями. Яке тут загальне правило?

ЕТА: звичайно, я знаю про лінь. Я запитую, що заважає авторам-компіляторам зробити цей конкретний випадок ще лінивішим, ніж це можливо.

ETA2: підсумок прикладів: GHC, як видається, трактує ()точно так само, як і будь-який інший тип, тобто як би виникає питання про те, яке регулярне значення, що населяє тип, має бути повернуто з функції. Те, що існує лише одне таке значення, схоже, не використовується (ab), що використовується алгоритмами оптимізації.

ETA3: коли я кажу Haskell, я маю на увазі Haskell як визначено у звіті, а не Haskell-H-in-GHC. Здається, це припущення, яке не поділяється так широко, як я уявляв (це було «на 100% читачів»), або я, певно, міг би сформулювати більш чітке питання. Незважаючи на це, я шкодую, що змінив назву питання, оскільки спочатку запитував, які існують гарантії для такої функції.

ETA4: Здавалося б, це питання пробігло свій шлях, і я вважаю його без відповіді. (Я шукав функцію "закритого запитання", але знайшов лише "відповідь на власне запитання", і оскільки на нього не можна відповісти, я не пішов по цьому маршруту.) Зі Звіту ніхто не підніс нічого, що вирішило б це так чи інакше , яку я спокусив трактувати як категоричну, але не певну відповідь "немає гарантії для мови як такої". Все, що ми знаємо, - це те, що поточна реалізація GHC не пропустить оцінку такої функції.

Я зіткнувся з фактичною проблемою під час перенесення програми OCaml в Haskell. Початковий додаток мав взаємно рекурсивну структуру багатьох типів, і код оголошував ряд функцій, викликаних assert_structureN_is_correctN в 1..6 або 7, кожна з яких повертала одиницю, якщо структура справді була правильною, і кинула виняток, якщо її не було . Крім того, ці функції називали один одного, коли вони розкладали умови правильності. У Haskell це краще вирішувати, використовуючи Either Stringмонаду, тому я її так переписав, але питання як теоретичне питання залишилося. Дякую за всі матеріали та відповіді.


1
Це лінь на роботі. Якщо не вимагається результат функції (наприклад, за зразком відповідності конструктору), тіло функції не оцінюється. Щоб помітити різницю, спробуйте порівняти h1::()->() ; h1 () = ()та h2::()->() ; h2 _ = (). Запустіть h1 (f 1)і те h2 (f 1), і подивіться, що вимагає лише перший (f 1).
чі

1
"Лінь, здається, диктує, що його заміняють на (), без будь-якої оцінки." Що це означає? f 1"замінено" на undefinedвсі випадки.
oisdk

3
Функція ... -> ()може 1) закінчуватись і повертатись (), 2) закінчуватися з помилкою виключення / часу виконання і нічого не повертати, або 3) розходитися (нескінченна рекурсія). GHC не оптимізує код, припускаючи, що може статися лише 1): якщо f 1вимагається, він не пропускає свою оцінку та повертається (). Семантика Haskell полягає в тому, щоб оцінити її і побачити, що відбувається серед 1,2,3.
чі

2
У ()цьому питанні насправді немає нічого особливого (ні типу, ні значення). Все ті ж спостереження трапляються, якщо замінити () :: ()їх, скажімо, 0 :: Intскрізь. Це все просто нудні старі наслідки лінивої оцінки.
Даніель Вагнер

2
ні, "уникання" тощо - це не семантика Хаскелла. і є два можливі значення ()типу, ()і undefined.
Буде Несс

Відповіді:


10

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

Це не так, як працює Haskell. Кожен тип має ще одне значення в Haskell, а саме немає значення, помилки або так званого "дна", кодується undefined. Таким чином, фактично відбувається оцінка:

main = print (f 1)

еквівалентно мові основної

main = _Case (f 1) _Of x -> print x   -- pseudocode illustration

або навіть (*)

main = _Case (f 1) _Of x -> putStr "()"

і сердечника _Caseє примус :

"Оцінка %case[виразу] змушує оцінювати вираз, що тестується (" scrutinee "). Значення scrutinee прив'язується до змінної після %ofключового слова, ...".

Величина змушена слабкою головою нормальної форми. Це частина визначення мови.

Haskell не є декларативною мовою програмування.


(*) print x = putStr (show x) і show () = "()", тому showвиклик може бути складений повністю.

Значення дійсно відомо заздалегідь як (), і навіть значення show ()відомо заздалегідь як "()". Тим НЕ менше прийняті Haskell семантика вимагає , щоб значення (f 1)змушений слабкою головний нормальній формі , перш ніж приступити до друку , що відомо заздалегідь рядки "()".


редагувати: Розглянути concat (repeat []). Це повинно бути [], чи це має бути нескінченна петля?

На це, мабуть, відповідь "декларативної мови" []. Відповідь Хаскелла - нескінченна петля .

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

edit2 : print $ h (f 1) == _Case (h (f 1)) _Of () -> print ()і лише hвимушений, ні f; і для отримання своєї відповіді hне потрібно нічого змушувати, відповідно до її визначення h _ = ().

зауваження на розлуку: Лінь може мати причину, але це не її визначення. Лінь - це те, що воно є. Він визначається як спочатку всі величини, які спонукаються до WHNF відповідно до вимог, що випливають main. Якщо це допомагає уникнути дна в певному конкретному випадку відповідно до його конкретних обставин, то це робить. Якщо ні, ні. Це все.

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


Ідучи до звіту , ми маємо

f :: a -> ()
f = \_ -> (undefined :: ())

тоді

print (f 1)
 = print ((\ _ ->  undefined :: ()) 1)
 = print          (undefined :: ())
 = putStrLn (show (undefined :: ()))

і с

instance Show () where
    show :: () -> String
    show x = case x of () -> "()"

це триває

 = putStrLn (case (undefined :: ()) of () -> "()")

Тепер у розділі 3.17.3 йдеться про формальну семантику відповідності зразків звіту

Семантика caseвиразів [наведена] на рисунках 3.1–3.3. Будь-яка реалізація повинна вести себе так, щоб ці ідентичності мали [...].

і випадок (r)на рисунку 3.2 станів

(r)     case  of { K x1  xn -> e; _ -> e } =  
        where K is a data constructor of arity n 

() - це конструктор даних arity 0, тому він такий самий, як

(r)     case  of { () -> e; _ -> e } =  

і таким чином загальний результат оцінки .


2
Мені подобається ваше пояснення. Це зрозуміло і просто.
стрілка

@DanielWagner Я мав на увазі caseвід Core, насправді, і ігнорував зияючий отвір. :) Я відредагував згадку про Core.
Буде Несс

1
Чи не буде чи форсування бути showвикликано print? Щось на кшталтshow x = case x of () -> "()"
user253751

1
Я маю на увазі caseв Core, а не в самому Haskell. Haskell перекладається на Core, який має форсинг case, AFAIK. Ви правильні, що caseв Haskell не змушує себе. Я можу щось написати в схему чи ML (якщо я можу написати ML, що є) або псевдокод.
Буде Несс

1
Авторитетна відповідь на все це, мабуть, десь у Звіті. Все, що я знаю - тут не відбувається "оптимізація", і "регулярне значення" в цьому контексті не є значущим терміном. Що б не примушували, примушували. printсили, скільки потрібно для друку. він не дивиться на тип, типи зникли, стираються, до моменту запуску програми правильна підпрограма друку вже обрана та складена у відповідності з типом під час компіляції; ця підпрограма все ще буде примушувати своє вхідне значення до WHNF під час виконання, і якщо воно не було визначене, воно спричинить помилку.
Буде Несс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.