#haskell #exception #ghc
#haskell #исключение #ghc
Вопрос:
Рассмотрим эту (довольно урезанную) функцию для обнаружения сбоя сопоставления с образцом.
module Handle where
import Control.Exception
import System.IO.Unsafe
wrap :: (a -> b) -> a -> b
wrap f x =
unsafePerformIO (catch (return $! (f x)) handler)
where
handler :: PatternMatchFail -> b
handler _ = error "caught"
Я поместил его в отдельный модуль, чтобы он не был встроен в последующие примеры. Вот пример использования оболочки.
module Main where
import Handle
f [x] = x
g [x] y = x y
h [x] = y -> x y
main = do putStrLn (show (wrap h [] 4))
Давайте попробуем это с GHCi. Как и ожидалось, в простых примерах wrap
либо возвращает значение f x
, либо, если шаблон завершается с ошибкой, он улавливает ошибку:
mike@spinnaker:~$ ghci example.hs
GHCi, version 8.4.4: http://www.haskell.org/ghc/ :? for help
[1 of 2] Compiling Handle ( Handle.hs, interpreted )
[2 of 2] Compiling Main ( example.hs, interpreted )
Ok, two modules loaded.
*Main> wrap f [3]
3
*Main> wrap f []
*** Exception: caught
Но теперь рассмотрим функции g
и h
, которые должны быть эквивалентны друг другу. wrap
Функция не работает g
, оставляя сбой на верхнем уровне, но он работает h
.
*Main> wrap g [3] 4
7
*Main> wrap g [] 4
*** Exception: example.hs:6:1-15: Non-exhaustive patterns in function g
*Main> wrap h [] 4
*** Exception: caught
Теперь давайте попробуем сам GHC. В результате получается, что при отключенной оптимизации ошибка from h
перехвачена; но при -O
использовании оптимизатора, по-видимому, превращается h
в g
, и ошибка больше не перехвачена.
mike@spinnaker:~$ ghc -O --make -o example example.hs
mike@spinnaker:~$ example
example: example.hs:8:1-20: Non-exhaustive patterns in function h
Я вполне могу себе представить, что оптимизатор превращает вложенную функцию h
в функцию с двумя аргументами одновременно, например g
, и что применение g
к одному аргументу дает объект, который считается нормальной формой заголовка, пока он не получит свой второй аргумент, поэтому сбой сопоставления с шаблоном откладывается до тех пор, пока мы не окажемся за пределами сфера применения wrap
. Мои вопросы таковы: является ли это объяснение в целом правильным? И если мы хотим согласованного поведения, как мы можем этого добиться?
(Пример извлечен из интерпретатора для языка программирования более высокого порядка, где частичные функции устанавливаются как примитивы, и нам нужен единый метод устранения сбоев примитивов, независимо от того, выдают они результат более высокого порядка или нет: это зависит от базовой монады.)
Комментарии:
1. Интересно, действительно ли
g
иh
должно быть эквивалентным. Один похож наx -> case x of {[_] -> y -> ...}
другойx y -> case x of [_] -> ....
. Первый может возвращать значение bottom, если применяется к одному аргументу, другой — нет. Чтобы этогоseq
было достаточно, нам не нужны небезопасные функции. Тем не менее, я не совсем уверен в том, что здесь происходит.2. Теперь я считаю, что
-fno-do-lambda-eta-expansion
это может быть правильным флагом, чтобы избежать оптимизации, которая предотвращает преждевременное генерирование ошибки. По крайней мере, так кажется, глядя на суть базового теста, который я пробовал.3. Да, абсолютно:
g
иh
являются функционально разными функциями в Haskell, и это фактически означаетx y ->
, что это не аббревиатура дляx -> y ->
. Проблема возникает потомуh
, что это то, что мы хотим здесь, но компилятор превращает это вg
. Я ответил на свой собственный вопрос с помощью обходного пути.4. Я не думаю, что между и есть заметные различия
x -> y ->
x y ->
, за исключением производительности, может быть. Когда вы начинаете сопоставлять шаблоны между лямбдами, все начинает отличаться, AFAICS.5. Я согласен: небрежно, я позволял
x
использовать произвольный шаблон. Разве вы не видели, что это было написано другим шрифтом?
Ответ №1:
Вот работоспособная схема. В приведенном примере аргумент [x]
представляет список аргументов примитива в интерпретаторе и y
является артефактом используемой монады (возможно M a = Mem -> (a, Mem)
). [Я использую монады, но не заключаю их в классы типов. Каждому свое!]
Во-первых, версия wrap
этого печатает имя функции и список оскорбительных аргументов. Обратите внимание, что res
параметр является результатом, который должен быть возвращен — CBN означает, что нет необходимости формировать приложение f args
внутри wrap
, когда оно может быть передано извне.
module Handle where
import Control.Exception
import System.IO.Unsafe
wrap :: Show a => String -> a -> b -> b
wrap f args res =
unsafePerformIO (catch (return $! res) handler)
where
handler :: PatternMatchFail -> b
handler _ = error ("caught " f " " show args)
Теперь пример с переносом как функций с одним аргументом, так и функций с двумя аргументами. Вспомогательная функция wrap2
имеет одно определение для каждой версии интерпретатора, тогда f
как , g
и h
являются членами большого семейства примитивов.
module Main where
import Handle
f [x] = x
ff arg = wrap "f" arg (f arg)
wrap2 :: Show a => String -> (a -> b -> c) -> a -> b -> c
wrap2 name fun arg y = wrap name arg (fun arg y)
g [x] y = x y
gg = wrap2 "g" g
h [x] = y -> x y
hh = wrap2 "h" h
main = do putStrLn (show (hh [] 4))
Цель состоит в том, чтобы иметь возможность определять частичные примитивы, подобные этим, с однострочным определением для каждого примитива.
Комментарии:
1. Фанаты Haskell могут использовать гибкие и перекрывающиеся экземпляры, чтобы система классов типов выбирала способ упаковки каждого примитива в соответствии с его типом. Аккуратно или отвратительно, в зависимости от вашей точки зрения.
Ответ №2:
Преобразование перемещения лямбда-выражения из выражения case и связанного с ним выражения, влияющего на монаду ввода-вывода, может быть отключено путем присвоения GHC флагов -fpedantic-bottoms
и -fno-state-hack
. Это кажется лучшим решением этой проблемы, вместо того, чтобы пытаться угадать, какая функция-оболочка необходима в каждом конкретном случае.
Комментарии:
1. Два ответа на мой собственный вопрос! Извините за это. Но оба кажутся полезными — один, чтобы подробнее объяснить, что происходит, другой, чтобы дать простой способ избежать проблемы. Я оставляю их обоих здесь на случай, если они будут полезны другим.