Proof-Search Friendly Non-Equality Proofs

This commit is contained in:
Nathan McCarty 2025-07-05 23:53:41 -04:00
parent 99b7ca322e
commit 106d46cc3c
4 changed files with 85 additions and 1 deletions

16
db/posts/8.json Normal file
View file

@ -0,0 +1,16 @@
{
"edited-at": [
],
"hidden": false,
"idris": true,
"ipkg": "/home/nathan/Projects/Blog/projects/Idris/Idris.ipkg",
"posted-at": "2025-07-05T22:48:03.372522-04:00",
"slugs": [
],
"source": "/home/nathan/Projects/Blog/projects/Idris/src/TipsAndTricks/ProofSearchAndNonEquality.md",
"special": false,
"tags": [
"idris",
"tip"
]
}

View file

@ -1,7 +1,8 @@
{
"desc": "Introductions to the building blocks of dependently typed programs.</p><p>The posts in this series are 'living posts', they will be referenced as library modules by future posts, and will be updated as needs change.",
"post-ids": [
7
7,
8
],
"title": "Dependent Nuggets"
}

View file

@ -20,6 +20,7 @@ modules = Idris
, Posts.HelloWorld
, LessMacrosMoreTypes.Printf
, DependentNuggets.FreshLists
, TipsAndTricks.ProofSearchAndNonEquality
-- main file (i.e. file to load at REPL)
-- main =

View file

@ -0,0 +1,66 @@
# Proof-Search Friendly Non-Equality Proofs
```idris hide
module TipsAndTricks.ProofSearchAndNonEquality
import Data.List.Elem
import Data.So
import Decidable.Decidable
import Decidable.Equality
```
## The Problem
When designing interfaces, you may occasionally find a need to thread a proof of non-equality through API, and for ergonomics reasons, you would like that proof to be an `auto` implicit so the user doesn't have to generate the proof manually. Often, these proofs seem like they really should be trivial for the compiler to generate.
I recently came across an instance of this implementing the _Brown Benchmark for Table Types_[^1], where I needed to encode a proof that a given value was non-equal to every value currently in a list (much like a _FreshList_[^2], but with propositional equality). In many cases, this list is going to be know statically at compile time, so I would like the compiler to just generate the structure when possible to keep the API easy to use.
It may seem natural to use a value of type `Not (x = y)`, but this has a bit of an issue, the compiler will refuse to generate even seemingly trivial values of this type:
```idris
failing
oneIsNotTwo : Not (1 = 2)
oneIsNotTwo = %search
```
This is because `Not x` is an alias for `x -> Void`, a function type. Proof search won't try to produces values of function types, it only searches for value types. More generally, it is a lot easier to find positive proof of a statement than negative proof of a statement.
## The Solution
We can sneak around the compiler's limitations here by generating a positive proof of non-equality instead. One cheeky way that comes to mind is to use our old friend `Data.So`, instead asking for a `So`bearing witness that the result of calling `decEq x y` satisfies the `isNo` function.
Lets take a look at it in action, here's an example of a type that serves as proof that some value `x` is unique compared to every value in some list:
```idris
data IsUnique : (value : t) -> List t -> Type where
NotInTheEmptyList : IsUnique value []
NotHere : DecEq t => {x : t} ->
(prf : So (isNo (decEq value x))) -> IsUnique value xs
-> IsUnique value (x :: xs)
```
This sneaks our proof of non-equality into a value type, as the argument to `So` is a `Bool`, not a function, allowing proof search to generate these values by actively calling the `decEq` and `isNo` functions.
This `So (isNo (decEq x y))` type is equivalent to `Not (x = y)`. Don't believe me? Here's proof:
```idris
soNoIsNot : DecEq t => {x, y : t} -> So (isNo (decEq x y)) -> Not (x = y)
soNoIsNot {x} {y} soPrf with (decEq x y)
soNoIsNot {x} {y} soPrf | Yes prf = absurd soPrf
soNoIsNot {x} {y} soPrf | No contra = contra
```
We can therefore use this `IsUnique v xs` proof to, for example, produce a proof of `Not (Elem v xs)`:
```idris
isUniqueMeansNotElem : {v : t} -> IsUnique v xs -> Not (Elem v xs)
isUniqueMeansNotElem (NotHere prf x) Here =
let notPrf = soNoIsNot {x = v} {y = v} prf
in notPrf Refl
isUniqueMeansNotElem (NotHere prf x) (There y) = isUniqueMeansNotElem x y
```
[^1]: <https://github.com/brownplt/B2T2>
[^2]: [FreshLists: Containers That Only Accept "Fresh" Elements](/posts/by-slug/freshlists-containers-that-only-accept-fresh.html)