Время исключения соответствия шаблону в Haskell

#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. Два ответа на мой собственный вопрос! Извините за это. Но оба кажутся полезными — один, чтобы подробнее объяснить, что происходит, другой, чтобы дать простой способ избежать проблемы. Я оставляю их обоих здесь на случай, если они будут полезны другим.