Чистота функций, генерирующих ByteString (или любой объект с компонентом ForeignPtr)
Так как ByteString
это конструктор с ForeignPtr
:
data ByteString = PS {-# UNPACK #-} !(ForeignPtr Word8) -- payload
{-# UNPACK #-} !Int -- offset
{-# UNPACK #-} !Int -- length
Если у меня есть функция, которая возвращает ByteString
затем, учитывая вход, скажем, константа Word8
функция возвратит ByteString с недетерминированным значением ForeignPtr - то, каким будет это значение, определяется диспетчером памяти.
Значит ли это, что функция, которая возвращает ByteString, не является чистой? Очевидно, что это не так, если вы использовали библиотеки ByteString и Vector. Конечно, это было бы широко обсуждаться, если бы это было так (и, надеюсь, появится в верхней части поиска Google). Как достигается эта чистота?
Причина, по которой я задаю этот вопрос, заключается в том, что мне любопытно, какие тонкости связаны с использованием объектов ByteString и Vector с точки зрения компилятора GHC, учитывая наличие члена ForeignPtr в их конструкторе.
1 ответ
Нет возможности наблюдать значение указателя внутри ForeignPtr
снаружи Data.ByteString
модуль; его реализация внутренне нечиста, но внешне чиста, потому что она гарантирует, что инварианты, требуемые быть чистыми, сохраняются до тех пор, пока вы не можете видеть внутри ByteString
конструктор - который вы не можете, потому что он не экспортируется.
Это обычная техника в Haskell: реализация чего-то с небезопасными техниками под капотом, но раскрытие чистого интерфейса; Вы получаете как производительность, так и небезопасную технику, не ставя под угрозу безопасность Haskell. (Конечно, у модулей реализации могут быть ошибки, но вы думаете, ByteString
было бы менее вероятно, утечка его абстракции, если бы он был написан на C?:))
Что касается тонких моментов, если вы говорите с точки зрения пользователя, не беспокойтесь: вы можете использовать любую функцию, которую экспортируют библиотеки ByteString и Vector, не беспокоясь, если они не начинаются с unsafe
, Они оба являются очень зрелыми и хорошо протестированными библиотеками, поэтому вам не следует сталкиваться с какими-либо проблемами с чистотой, и если вы это сделаете, то это ошибка в библиотеке, и вы должны сообщить об этом.
Что касается написания собственного кода, который обеспечивает внешнюю безопасность с небезопасной внутренней реализацией, правило очень простое: поддерживать ссылочную прозрачность.
Взяв ByteString в качестве примера, функции для построения ByteStrings используют unsafePerformIO
выделить блоки данных, которые они затем мутируют и помещают в конструктор. Если мы экспортируем конструктор, то пользовательский код сможет получить ForeignPtr
, Это проблематично? Чтобы определить, так ли это, нам нужно найти чистую функцию (т.е. не в IO
), что позволяет нам различать два ForeignPtr, выделенных таким образом. Беглый взгляд на документацию показывает, что есть такая функция: instance Eq (ForeignPtr a)
позволил бы нам различать это. Поэтому мы не должны позволять пользовательскому коду получать доступ к ForeignPtr
, Самый простой способ сделать это - не экспортировать конструктор.
Итак, когда вы используете небезопасный механизм для реализации чего-либо, убедитесь, что вводимая им примесь не может просочиться за пределы модуля, например, путем проверки значений, которые вы производите с его помощью.
Что касается проблем с компилятором, вам не нужно беспокоиться о них; хотя функции небезопасны, они не должны позволять вам делать что-то более опасное, кроме нарушения чистоты, чем вы можете сделать в IO
Монада для начала. Как правило, если вы хотите сделать что-то, что может привести к действительно неожиданным результатам, вам придется сделать все возможное, чтобы сделать это: например, вы можете использовать unsafeDupablePerformIO
если вы можете иметь дело с возможностью двух потоков, оценивающих один и тот же бланк формы unsafeDupablePerformIO m
одновременно. unsafePerformIO
немного медленнее, чем unsafeDupablePerformIO
потому что это предотвращает это. (Thunks в вашей программе могут оцениваться двумя потоками одновременно во время обычного выполнения с GHC; обычно это не проблема, так как оценка одного и того же чистого значения дважды не должна иметь побочных эффектов (по определению), но при написании небезопасного кода, это то, что вы должны принять во внимание.)
Документация GHC для unsafePerformIO
(а также unsafeDupablePerformIO
как я уже упоминал выше) подробно описывает некоторые подводные камни, с которыми вы можете столкнуться; аналогично документация для unsafeCoerce#
(который должен использоваться через его переносимое имя, Unsafe.Coerce.unsafeCoerce).