Нет способа наблюдать значение указателя внутри ForeignPtr
снаружи модуля Data.ByteString
; его реализация внутренне нечистая, но внешне чистая, потому что она гарантирует, что требуемые чистые инварианты поддерживаются, пока вы не можете видеть внутри конструктора ByteString
- который вы не может, потому что не экспортируется.
Это распространенная техника в Haskell: реализация чего-то с небезопасными техниками под капотом, но раскрытие чистого интерфейса; Вы получаете как производительность, так и небезопасную технику, не ставя под угрозу безопасность Haskell. (Конечно, у модулей реализации могут быть ошибки, но вы думаете, что ByteString
будет меньше с вероятностью утечки его абстракции, если он будет написан на C?:))
Что касается тонких моментов, если вы говорите с точки зрения пользователя, не беспокойтесь: вы можете использовать любую функцию, которую экспортируют библиотеки ByteString и Vector, не беспокоясь, если они не начинаются с unsafe
. Они оба являются очень зрелыми и хорошо протестированными библиотеками, поэтому вам вообще не нужно сталкиваться с проблемами чистоты, и если вы делаете , это ошибка в библиотеке, и вы должны сообщить об этом.
Что касается написания собственного кода, который обеспечивает внешнюю безопасность с небезопасной внутренней реализацией, правило очень простое: поддерживает прозрачность ссылок .
Взяв в качестве примера ByteString, функции для создания ByteString используют unsafePerformIO
для выделения блоков данных, которые они затем мутируют и помещают в конструктор. Если мы экспортируем конструктор, то пользовательский код сможет получить значение ForeignPtr
. Это проблематично? Чтобы определить, так ли это, нам нужно найти функцию pure (т.е. не в IO
), которая позволяет нам различать два ForeignPtr, выделенных таким образом. Быстрый взгляд на документацию показывает, что есть такая функция: instance Eq (ForeignPtr a)
позволит нам их различить. Поэтому мы не должны позволять коду пользователя обращаться к ForeignPtr
. Самый простой способ сделать это - не экспортировать конструктор.
В итоге: Когда вы используете небезопасный механизм для реализации чего-либо, убедитесь, что примеси, которые он вводит, не могут просочиться за пределы модуля, например, проверяя значения, которые вы производите с его помощью.
Что касается проблем с компилятором, вам не нужно беспокоиться о них; хотя функции небезопасны , они не должны позволять вам делать что-то более опасное, кроме нарушения чистоты, чем вы можете сделать в монаде IO
для начала. Как правило, если вы хотите сделать что-то, что может привести к действительно неожиданным результатам, вам придется сделать все возможное, чтобы сделать это: например, вы можете использовать unsafeDupablePerformIO
если вы можете иметь дело с возможностью того, что два потока одновременно оценивают один и тот же бланк формы unsafeDupablePerformIO m
. unsafePerformIO
немного медленнее, чем unsafeDupablePerformIO
, потому что это предотвращает это. (Thunks в вашей программе могут быть оценены двумя потоками одновременно во время обычного выполнения с GHC; обычно это не проблема, так как оценка одного и того же чистого значения дважды не должна иметь побочных эффектов (по определению), но при написании небезопасного кода, это то, что вы должны принять во внимание.)
Документация GHC для unsafePerformIO
(и unsafeDupablePerformIO
, как я упоминал выше) подробно описывает некоторые подводные камни, с которыми вы можете столкнуться; аналогично, документация для unsafeCoerce#
(которая должна использоваться через его переносимое имя, Unsafe.Coerce.unsafeCoerce ).