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


person Sal    schedule 23.12.2011    source источник


Ответы (1)


Невозможно наблюдать значение указателя внутри модуля ForeignPtr снаружи модуля Data.ByteString; его реализация внутренне нечиста, но внешне чиста, потому что она гарантирует, что инварианты, необходимые для чистоты, сохраняются до тех пор, пока вы не видите внутри конструктора ByteString, который вы не может, потому что он не экспортируется.

Это обычная техника в Haskell: реализация чего-то с небезопасными технологиями под капотом, но выставление чистого интерфейса; вы получаете как производительность, так и мощность небезопасных методов, не ставя под угрозу безопасность Haskell. (Конечно, в модулях реализации могут быть ошибки, но вы думаете, что ByteString меньше будет давать утечку своей абстракции, если она будет написана на C? :))

Что касается тонкостей, то, если вы говорите с точки зрения пользователя, не беспокойтесь: вы можете использовать любую функцию, экспортируемую библиотеками ByteString и Vector, не беспокоясь, если они не начинаются с unsafe. Обе они являются очень зрелыми и хорошо протестированными библиотеками, так что вы вообще не должны сталкиваться с какими-либо проблемами чистоты, а если вы сталкиваетесь, это ошибка в библиотеке, и вы должны сообщить об этом.

Что касается написания собственного кода, обеспечивающего внешнюю безопасность с небезопасной внутренней реализацией, правило очень простое: поддерживать ссылочную прозрачность.

Взяв в качестве примера ByteString, функции для создания ByteString используют unsafePerformIO для выделения блоков данных, которые затем изменяются и помещаются в конструктор. Если бы мы экспортировали конструктор, то пользовательский код смог бы получить доступ к ForeignPtr. Это проблематично? Чтобы определить, так ли это, нам нужно найти чистую функцию (т. е. не в IO), которая позволяет нам различать два ForeignPtr, выделенных таким образом. Беглый взгляд на документацию показывает, что есть такая функция: instance Eq (ForeignPtr a) позволит нам их различать. Поэтому мы не должны позволять коду пользователя получать доступ к ForeignPtr. Самый простой способ сделать это — не экспортировать конструктор.

Подводя итог: когда вы используете небезопасный механизм для реализации чего-либо, убедитесь, что вносимая им нечистота не может просочиться за пределы модуля, например. путем проверки значений, которые вы производите с ним.

Что касается проблем с компилятором, вам не о чем беспокоиться; хотя функции небезопасны, они не должны позволять вам делать что-то более опасное, кроме нарушения чистоты, чем то, что вы можете сделать в монаде IO для начала. Как правило, если вы хотите сделать что-то, что может привести к действительно неожиданным результатам, вам придется приложить все усилия, чтобы сделать это: например, вы можете использовать unsafeDupablePerformIO, если вы можете справиться с возможностью двух потоков, оценивающих один и тот же преобразователь формы unsafeDupablePerformIO m одновременно. unsafePerformIO немного медленнее, чем unsafeDupablePerformIO, потому что предотвращает это. (Преобразователи в вашей программе могут оцениваться двумя потоками одновременно во время нормального выполнения с GHC; обычно это не проблема, так как вычисление одного и того же чистого значения дважды не должно иметь неблагоприятных побочных эффектов (по определению), но при написании небезопасного кода, это то, что вы должны принять во внимание.)

документация GHC для unsafePerformIOunsafeDupablePerformIO, как я указал выше) подробно описывает некоторые ловушки, с которыми вы можете столкнуться; аналогично документация для unsafeCoerce# (которое следует использовать через переносимое имя, Unsafe.Coerce.unsafeCoerce).

person ehird    schedule 23.12.2011
comment
Ну, я планирую использовать небезопасные операции. Следовательно, этот вопрос :) Я хотел бы узнать о проблемах, о которых я должен знать, как и авторы библиотеки. Эти идеи будут очень полезны при написании нашего собственного кода, который должен быть быстрым, но при этом внешне чистым, для параллельных и параллельных расширений. - person Sal; 23.12.2011
comment
Ах хорошо; это не было ясно для меня из вопроса. Я попытаюсь включить часть этой информации в свой ответ, хотя это сложно, поскольку основное правило заключается в том, чтобы просто обеспечить ссылочную прозрачность из-за пределов модуля. - person ehird; 23.12.2011
comment
Я расширил его еще немного, надеюсь, что это поможет :) - person ehird; 23.12.2011
comment
большое спасибо, что нашли время, чтобы расширить его. Это очень полезно. Я тоже прочитал документацию GHC о unsafePerformIO и unsafeDupablePerformIO, что заставило меня задуматься, что привело меня к этому вопросу :) - person Sal; 23.12.2011