Чистота функций, генерирующих ByteString (или любой объект с компонентом ForeignPtr)

Так как a ByteString является конструктором с ForeignPtr:

data ByteString = PS {-# UNPACK #-} !(ForeignPtr Word8) -- payload
                     {-# UNPACK #-} !Int                -- offset
                     {-# UNPACK #-} !Int                -- length

Если у меня есть функция, которая возвращает ByteString, то, учитывая ввод, скажем, константу Word8, функция вернет байтовую строку с неопределенным значением 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 для unsafePerformIOunsafeDupablePerformIO, как я уже упоминал выше), описывает некоторые ошибки, с которыми вы могли столкнуться; аналогично документации для unsafeCoerce# (которую следует использовать через свое портативное имя Unsafe.Coerce.unsafeCoerce).