Как использовать опровержение, чтобы направить проверку типов в Haskell? - PullRequest
5 голосов
/ 29 октября 2019

Требуется ли для заполнения отверстия в следующей программе неконструктивные средства? Если да, то все еще имеет место, если x :~: y разрешимо?

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

(я знаю, что могу обойтисьпроблема, определяя Choose как GADT, я спрашиваю специально для семейства типов)

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

module PropositionalDisequality where

import Data.Type.Equality
import Data.Void

type family Choose x y where
  Choose x x = 1
  Choose x _ = 2

lem :: (x :~: y -> Void) -> Choose x y :~: 2
lem refutation = _

1 Ответ

3 голосов
/ 30 октября 2019

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

Интересно, почему этот аргумент не приводится чаще.

Абсолютно неточное резюме:

  • Акт I: поиск корректуры очень прост.
  • Акт II: также зависимые типы.
  • Акт III: Haskell все еще хорош для написания программ с зависимой типизацией.

Я. Игра поиска в поиске

Сначала мы определим пространство поиска.

Мы можем сократить любое определение Haskell до одной из форм

lem = (exp)

для некоторого выражения (exp). Теперь нам нужно найти только одно выражение.

Посмотрите на все возможные способы создания выражения в Haskell: https://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-220003 (это не учитывает расширения, упражнение для читателя).

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

  • лямбда-выражений, \x -> ...
  • сопоставление с образцом, case ... of ...
  • приложение-функция, f x
  • конструкторы, C (включая целочисленные литералы)
  • константы, c (для примитивов, которые нельзя записать в терминахконструкции выше, так что различные встроенные модули (seq) и, возможно, FFI, если это считается)
  • переменных (связанных лямбда-выражениями и падежами)

Мы можем исключить каждую константу наоснование, что я думаю, что вопрос действительно о чистом лямбда-исчислении (или читатель может перечислить константы, чтобы исключить константы черной магии, такие как undefined, unsafeCoerce, unsafePerformIO, которые заставляют все разрушаться (любой тип обитаем идля некоторых из них система типов несостоятельна, и ее следует оставить с константами белой магии, на которые можно обобщить настоящий теоретический аргумент с помощью хорошо финансируемого тезиса).

Мы также можемразумно предположить, что мы хотим получить решение без рекурсии (чтобы избавиться от шума, такого как lem = lem и fix, если вы чувствовали, что раньше не могли с ним расстаться), и которое на самом деле имеет нормальную форму или предпочтительноканоническая форма относительно βη-эквивалентности. Другими словами, мы уточняем и исследуем множество возможных решений следующим образом.

  • lem :: _ -> _ имеет тип функции, поэтому мы можем предположить WLOG , что его определениеначинается с лямбды:

    -- Any solution
    lem = (exp)
    
    -- is η-equivalent to a lambda
    lem = \refutation -> (exp) refutation
    
    -- so let's assume we do have a lambda
    lem = \refutation -> _hole
    

Теперь перечислим, что может быть под лямбдой.

  • Это может быть конструктор, который затем имеетбыть Refl, но нет никаких доказательств того, что Choose x y ~ 2 в контексте (здесь мы могли бы формализовать и перечислить равенства типов, о которых проверяет типограф и может вывести, или сделать синтаксис приведений (доказательств равенств) явным и сохранитьиграть в эту игру с поиском доказательства с ними), так что это не проверка типа:

    lem = \refutation -> Refl
    

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

  • Это может быть некоторое применение конструктора C x1 x2 ... или переменной refutation (применено или нет);но нет никакого способа, который был бы хорошо напечатан, он должен каким-то образом произвести (:~:), и Refl действительно единственный путь.

  • Или это может быть case,WLOG, нет ни вложенного case слева, ни какого-либо конструктора, потому что выражение может быть упрощено в обоих случаях:

    -- Any left-nested case expression
    case (case (e) of { C x1 x2 -> (f) }) { D y1 y2 -> (g) }
    
    -- is equivalent to a right-nested case
    case (e) of { C x1 x2 -> case (f) of { D y1 y2 -> (g) } }
    
    
    
    -- Any case expression with a nested constructor
    case (C u v) of { C x1 x2 -> f x1 x2 }
    
    -- reduces to
    f u v
    

    Таким образом, последний подкласс является регистром переменной:

    lem = \refutation -> case refutation (_hole :: x :~: y) of {}
    

    и мы должны построить x :~: y. Перечислим способы заполнения _hole еще раз. Это либо Refl, но доказательств нет, либо (пропуская несколько шагов) case refutation (_anotherHole :: x :~: y) of {},d у нас на руках бесконечный спуск, что тоже абсурдно. Другой возможный аргумент здесь заключается в том, что мы можем извлечь case из приложения, чтобы исключить этот случай из рассмотрения WLOG.

    -- Any application to a case
    f (case e of C x1 x2 -> g x1 x2)
    
    -- is equivalent to a case with the application inside
    case e of C x1 x2 -> f (g x1 x2)
    

Больше нет случаев. Поиск завершен, и мы не нашли реализацию (x :~: y -> Void) -> Choose x y :~: 2. КЭД.

Чтобы прочитать больше по этой теме, я думаю, что курс / книга о лямбда-исчислении до тех пор, пока доказательство нормализации простого типа лямбда-исчисления не даст вам базовые инструменты для начала. Следующий тезис содержит введение по этому вопросу в первой части, но, по общему признанию, я плохо разбираюсь в сложности такого материала: Какие типы имеют уникального обитателя? Сосредоточив внимание на чистой программной эквивалентности , Габриэль Шерер.

Не стесняйтесь предлагать более адекватные ресурсы и литературу.


II. Исправление предложения и доказательство его зависимыми типами

Ваша первоначальная интуиция о том, что это должно кодировать действительное предложение, определенно верна. Как мы можем исправить это, чтобы сделать его доказуемым?

Технически, тип, на который мы смотрим, количественно определяется с помощью forall:

forall x y. (x :~: y -> Void) -> Choose x y :~: 2

Важной особенностью forall является то, что он не имеет значения квантификатор. Переменные, которые он вводит , не могут использоваться "напрямую" в терминах этого типа. Хотя этот аспект становится более заметным в присутствии зависимых типов, он все еще пронизывает Haskell сегодня , предоставляя другую интуицию, почему это (и многие другие примеры) не являются «доказуемыми» в Haskell: если вы думаете о том, почемувы думаете, что предложение является действительным, вы, естественно, начнете с разделения случая на предмет того, равно ли x равен y, но даже для такого разделения случая вам нужен способ решить с какой стороны вына который, конечно, придется взглянуть на x и y, поэтому они не могут быть неактуальными. forall в Хаскеле совсем не похоже на то, что большинство людей имеют в виду «для всех».

Некоторое обсуждение вопроса актуальности можно найти в тезисе Зависимые типы в Хаскеле ,Ричардом Айзенбергом (в частности, раздел 3.1.1.5 для начального примера, раздел 4.3 для релевантности в зависимом Haskell и раздел 8.7 для сравнения с другими языками с зависимыми типами).

Для зависимого Haskell потребуется релевантный квантификатор для дополнения forall, и который приблизил бы нас к доказательству этого:

foreach x y. (x :~: y -> Void) -> Choose x y :~: 2

Тогда мы, вероятно, могли бы написать это:

lem :: foreach x y. (x :~: y -> Void) -> Choose x y :~: 2
lem x y p = case x ==? u of
  Left r -> absurd (p r)  -- x ~ y, that's absurd!
  Right Irrefl -> Refl    -- x /~ y, so Choose x y = 2

Это также предполагаетпервоклассное понятие неравенства /~, дополняющее ~, чтобы помочь Choose уменьшить, когда оно находится в контексте и функции принятия решения (==?) :: foreach x y. Either (x :~: y) (x :/~: y). На самом деле, этот механизм не является необходимым, это просто приводит к более короткому ответу.

На данный момент я придумываю вещи, потому что Dependent Haskell еще не существует, но это легко выполнимо в связанных языках с зависимой типизацией(Coq, Agda, Idris, Lean), по модулю адекватная замена семейства типов Choose (семейства типов в некотором смысле слишком сильны, чтобы их можно было переводить как простые функции, поэтому это может быть обманом, но я отвлекся).

Вот сопоставимая программа в Coq, показывающая также, что lem применяется к 1 и 2, и подходящее доказательство сводится к доказательству с помощью рефлексивности choose 1 2 = 2.

https://gist.github.com/Lysxia/5a9b6996a3aae741688e7bf83903887b


III. Без зависимых типов

Критическим источником трудностей здесь является то, что Choose является семейством закрытых типов с перекрывающимися экземплярами. Это проблематично, потому что не существует надлежащего способа выразить тот факт, что x и y не равны в Haskell, знать, что первое предложение Choose x x не применимо.

Более плодотворный путьесли вы в псевдо-зависимом Haskell, используйте булево равенство типов:

-- In the base library
import Data.Type.Bool (If)
import Data.Type.Equality (type (==))

type Choose x y = If (x == y) 1 2

Альтернативная кодировка ограничений равенства становится полезной для этого стиля:

type x ~~ y = ((x == y) ~ 'True)
type x /~ y = ((x == y) ~ 'False)

с этим,мы можем получить другую версию предложения типа выше,выражается в текущем Haskell (где SBool - тип синглтона Bool), что, по сути, может быть прочитано как добавление предположения о том, что равенство x и y разрешимо. Это не противоречит предыдущему утверждению о «неактуальности» forall, функция проверяет логическое значение (или, скорее, SBool), которое откладывает проверку x и y для любого, кто вызывает lem.

lem :: forall x y. SBool (x == y) -> ((x ~~ y) => Void) -> Choose x y :~: 2
lem decideEq p = case decideEq of
  STrue -> absurd p
  SFalse -> Refl
...