Подтвердить что ты не робот

Доказательство "нет коррупции" в Haskell

Я работаю в критически важной для безопасности отрасли, и наши проекты программного обеспечения обычно предъявляют требования безопасности; вещи, которые мы должны продемонстрировать, что программное обеспечение делает с высокой степенью уверенности. Часто это негативы, такие как "не коррумпировать чаще, чем 1 в". (Я должен добавить, что эти требования вытекают из требований безопасности статистической системы).

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

Во-первых, вот наш критический элемент данных, который не должен быть поврежден.

newtype Critical = Critical String

Теперь я хочу сохранить этот элемент в некоторых других структурах.

data Foo = Foo Integer Critical
data Bar = Bar String Critical

Теперь я хочу написать функцию преобразования из Foo в Bar, которая гарантированно не будет конфликтовать с данными Critical.

goodConvert, badConvert :: Foo -> Bar

goodConvert (Foo n c) = Bar (show n) c

badConvert (Foo n (Critical s)) = Bar (show n) (Critical $ "Bzzt - " ++ s)

Я хочу, чтобы "goodConvert" напечатал проверку, но "badConvert" проверяет тип проверки.

Очевидно, что я могу не импортировать конструктор Critical в модуль, который выполняет преобразование. Но было бы намного лучше, если бы я мог выразить это свойство в типе, потому что тогда я могу составить функции, гарантирующие сохранение этого свойства.

Я попытался добавить типы phantom и "forall" в разных местах, но это не помогает.

Одна вещь, которая могла бы работать, заключалась бы в том, чтобы не экспортировать конструктор Critical, а затем

mkCritical :: String -> IO Critical

Поскольку единственное место, где создаются эти критические элементы данных, находится во входных функциях, это имеет смысл. Но я бы предпочел более элегантное и общее решение.

Edit

В комментариях FUZxxl предложил посмотреть Safe Haskell. Это похоже на лучшее решение. Вместо добавления модификатора "без коррупции" на уровне типа, который я изначально хотел, похоже, что вы можете сделать это на уровне модуля, например:

1: Создайте модуль "Критический", который экспортирует все функции типа Critical, включая его конструктор. Отметьте этот модуль как "небезопасный", поставив "{- # LANGUAGE Unsafe # -}" в заголовок.

2: Создайте модуль "SafeCritical", который повторно экспортирует все, кроме конструктора, и любые другие функции, которые могут быть использованы для повреждения критического значения. Отметьте этот модуль как "заслуживающий доверия".

3: Отметьте все модули, которые необходимы для обработки критических значений без коррупции как "безопасные". Затем используйте это, чтобы продемонстрировать, что любая функция, импортированная как "безопасная", не может вызвать повреждение Критического значения.

Это оставит меньшую часть кода, например код ввода, который анализирует критические значения, требующие дополнительной проверки. Мы не можем устранить этот код, но сокращение суммы, требующей детальной проверки, по-прежнему является значительной победой.

Метод основан на том, что функция не может придумать новое значение, если функция не возвращает его. Если функция получает только одно критическое значение (как в функции "преобразовать" выше), то это единственное, на что он может вернуться.

Более сложная вариация проблемы возникает, когда функция имеет два или более критических значения одного и того же типа; он должен гарантировать, чтобы не смешивать их. Например,

swapFooBar :: (Foo, Bar) -> (Bar, Foo)
swapFooBar (Foo n c1, Bar s c2) = (Bar s c1, Foo n c2)

Однако это может быть выполнено путем предоставления той же обработки содержащим структурам данных.

4b9b3361

Ответ 1

Вы можете использовать параметричность, чтобы получить там часть там

data Foo c = Foo Integer c
data Bar c = Bar String c

goodConvert :: Foo c -> Bar c
goodConvert (Foo n c) = Bar (show n) c

Так как c является переменной без ограничений, вы знаете, что функция goodConvert ничего не знает о c и поэтому не может построить другое значение этого типа. Он должен использовать тот, который указан во входном файле.

Ну, почти. Нижние значения позволяют вам нарушить эту гарантию. Однако вы, по крайней мере, знаете, что если вы попытаетесь использовать "поврежденное" значение, это приведет к исключению (или не прекращению).

badConvert :: Foo c -> Bar c
badConvert (Foo n c) = Bar (show n) undefined

Ответ 2

В то время как решение hammar превосходно, и я обычно предлагаю интеллектуальные конструкторы/не экспортирующие конструктор, сегодня я решил попробовать это в помощнике Coq и извлечение в Haskell.

Обратите внимание! Я не очень хорошо разбираюсь в Coq/извлечении. Некоторые люди сделали хорошую работу с проверкой и извлечением кода Haskell, поэтому посмотрите на них для качественных примеров - я просто играю!

Сначала мы хотим определить ваши типы данных. В Coq это выглядит так же, как Haskell GADT:

Require Import String.
Require Import ZArith.

Inductive Critical :=
  Crit : string -> Critical.

Inductive FooT :=
  Foo : Z -> Critical -> FooT.

Inductive BarT :=
  Bar : string -> Critical -> BarT.

Подумайте об этих Inductive строках, таких как Inductive FooT := Foo : ... ., как объявления типов данных: data FooT = Foo Integer Critical

Для простоты использования вы можете получить некоторые дополнительные функции:

Definition critF f := match f with Foo _ c => c end.
Definition critB b := match b with Bar _ c => c end.

Так как Coq не определяет много функций стиля "show", я буду использовать заполнитель для отображения целых чисел.

Definition ascii_of_Z (z : Z) : string := EmptyString. (* FIXME *)

Теперь у нас есть основы, давайте определим функцию goodConvert!

Definition goodConvert (foo : FooT) : BarT :=
  match foo with
    Foo n c => Bar (ascii_of_Z n) c
  end.

Что все довольно очевидно - это ваша функция преобразования, но в Coq и использующая инструкцию case, а не соответствие шаблону верхнего уровня. Но как мы знаем, что эта функция действительно будет поддерживать инвариант? Мы это докажем!

Lemma convertIsGood : forall (f : FooT) (b : BarT),
  goodConvert f = b -> critF f = critB b.
Proof.
  intros. 
  destruct f. destruct b.
  unfold goodConvert in H. simpl.
  inversion H. reflexivity.
Qed.

Это говорит о том, что если преобразование f приводит к b, тогда критическое поле f должно быть таким же, как критическое поле b (предполагая некоторые незначительные вещи, например, вы не испортили поле аксессуаров).

Теперь откройте это Haskell!

Extraction Language Haskell.
Extract Constant ascii_of_Z => "Prelude.show".  (* obviously, all sorts of unsafe and incorrect behavior can be introduced by your extraction *)
Extract Inductive string => "Prelude.String" ["[]" ":"]. Print positive.
Extract Inductive positive => "Prelude.Integer" ["`Data.Bits.shiftL` 1 + 1" "`Data.Bits.shiftL` 1" "1"].
Extract Inductive Z => "Prelude.Integer" ["0" "" ""].

Extraction "so.hs" goodConvert critF critB.

Производство:

module So where

import qualified Prelude

data Bool =
   True
 | False

data Ascii0 =
   Ascii Bool Bool Bool Bool Bool Bool Bool Bool

type Critical =
  Prelude.String
  -- singleton inductive, whose constructor was crit

data FooT =
   Foo Prelude.Integer Critical

data BarT =
   Bar Prelude.String Critical

critF :: FooT -> Critical
critF f =
  case f of {
   Foo z c -> c}

critB :: BarT -> Critical
critB b =
  case b of {
   Bar s c -> c}

ascii_of_Z :: Prelude.Integer -> Prelude.String
ascii_of_Z z =
  []

goodConvert :: FooT -> BarT
goodConvert foo =
  case foo of {
   Foo n c -> Bar (ascii_of_Z n) c}

Можем ли мы запустить его? Это работает?

> critB $ goodConvert (Foo 32 "hi")
"hi"

Отлично! Если у кого-то есть предложения для меня, даже если это "ответ", я все уши. Я не уверен, как удалить мертвый код таких вещей, как Ascii0 или Bool, не говоря уже о том, чтобы сделать хорошие экземпляры шоу. Если кому-то интересно, я думаю, что имена полей могут быть выполнены автоматически, если я использовал Record вместо Inductive, но это могло бы сделать этот пост синтаксически уродливым.

Ответ 3

Я думаю, что решение скрытых конструкторов является идиоматическим. Вы можете экспортировать две функции:

mkCritical :: String -> D Critical
extract :: Critical -> String

где D - тривиальная монада или любая другая. Любая функция, которая создает объекты типа Critical в какой-либо точке, помечена D. Функция без этого D может извлекать данные из объектов Critical, но не создавать новые.

В качестве альтернативы:

data C a = C a Critical
modify :: (a -> String -> b) -> C a -> C b
modify f (C x (Critical y)) = C (f x y) (Critical y)

Если вы не экспортируете конструктор C, только modify, вы можете написать:

goodConvert :: C Int -> C String
goodConvert = modify (\(a, _) -> show a)

но badConvert невозможно записать.