Чистота функций, генерирующих ByteString (или любой объект с компонентом ForeignPtr) - PullRequest
14 голосов
/ 23 декабря 2011

Поскольку 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 Ответ

18 голосов
/ 23 декабря 2011

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

...