Чистота функций, генерирующих 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).

Другие вопросы по тегам