Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sugar for SatisfiesM #1029

Merged
merged 13 commits into from
Nov 13, 2024
Merged

feat: sugar for SatisfiesM #1029

merged 13 commits into from
Nov 13, 2024

Conversation

kim-em
Copy link
Collaborator

@kim-em kim-em commented Nov 8, 2024

Enables:

example (xs : Array Expr) : MetaM { ts : Array Expr // ts.size = xs.size } := do
  let r ← satisfying (xs.size_mapM inferType)
  return r

by showing that for the basic monads and monad transformers, one can lift the SatisfiesM predicate to a monadic value in Subtype.

leanprover-community-mathlib4-bot added a commit to leanprover-community/mathlib4 that referenced this pull request Nov 8, 2024
@leanprover-community-bot
Copy link
Collaborator

leanprover-community-bot commented Nov 8, 2024

Mathlib CI status (docs):

@digama0
Copy link
Member

digama0 commented Nov 8, 2024

Another thing I was considering regarding this class is that if Satisfying took LawfulMonad as a parameter or extended it (such that you can't even say satisfying unless the monad is lawful), then it should be legal to implement satisfying using an unsafeCast (i.e. using implemented_by on the typeclass projection function) since the constraints on it already tell you that it has to be the identity function. (You need lawfulness because otherwise the map in satisfies.2 could be doing something nontrivial to the monadic value.)

@fgdorais fgdorais added the awaiting-author Waiting for PR author to address issues label Nov 9, 2024
leanprover-community-mathlib4-bot added a commit to leanprover-community/mathlib4 that referenced this pull request Nov 10, 2024
leanprover-community-mathlib4-bot added a commit to leanprover-community/mathlib4 that referenced this pull request Nov 11, 2024
@kim-em
Copy link
Collaborator Author

kim-em commented Nov 11, 2024

Another thing I was considering regarding this class is that if Satisfying took LawfulMonad as a parameter or extended it (such that you can't even say satisfying unless the monad is lawful), then it should be legal to implement satisfying using an unsafeCast (i.e. using implemented_by on the typeclass projection function) since the constraints on it already tell you that it has to be the identity function. (You need lawfulness because otherwise the map in satisfies.2 could be doing something nontrivial to the monadic value.)

@digama0, how do you feel about:

set_option linter.unusedVariables false in
/--
Since we assume `m` is lawful,
we can replace the implementation of `satisfying` at runtime using the identity function
and an unsafe "proof" via `lcProof`.
-/
unsafe def unsafeSatisfying {m : Type u → Type v} [Functor m] [LawfulFunctor m] [MonadSatisfying m]
    {α} {p : α → Prop} {x : m α} (h : SatisfiesM (m := m) p x) : m {a // p a} :=
  (⟨·, lcProof⟩) <$> x

/-- `unsafeSatisfying` is equivalent to `satisfying`. -/
@[csimp] unsafe def satisfying_eq_unsafeSatisfying :
    @satisfying = @unsafeSatisfying := by
  funext m _ _ _ _ p x h 
  unfold unsafeSatisfying
  conv => rhs; simp +singlePass [← MonadSatisfying.val_eq h]
  simp

instead of the unsafeCast / implemented_by?

Pros: It "documents" the reason why it is safe to do the replacement. (Although of course given that Lean would accept lcProof inside the @[csimp] means that there is no real improvement in safety).
Cons: It's a bit strange! Maybe you have in mind some performance difference between unsafeCast and lcProof here?

Happy to switch it out for @[implemented_by].

leanprover-community-mathlib4-bot added a commit to leanprover-community/mathlib4 that referenced this pull request Nov 11, 2024
leanprover-community-mathlib4-bot added a commit to leanprover-community/mathlib4 that referenced this pull request Nov 11, 2024
@digama0
Copy link
Member

digama0 commented Nov 11, 2024

I'm actually more concerned about the performance impact of the <$> rather than the unsafeCast. Also, proving theorems about unsafe definitions is a bit scuffed.

@kim-em
Copy link
Collaborator Author

kim-em commented Nov 11, 2024

I'm actually more concerned about the performance impact of the <$> rather than the unsafeCast. Also, proving theorems about unsafe definitions is a bit scuffed.

But no concrete change here, right? I don't see any way to avoid the <$>. Happy to remove the theorem, as I agree it is a bit misleading, but will leave as is unless you say otherwise!

@digama0
Copy link
Member

digama0 commented Nov 11, 2024

Well, with unsafeCast you can avoid the <$>. I don't think there is any way to prove that this cast makes sense using lean's logic though - you would need to have facts about how subtypes are represented in the ABI and somehow translate them to equalities, but they are definitely not equalities in the logic...

@kim-em
Copy link
Collaborator Author

kim-em commented Nov 11, 2024

Oh, yikes, you just want to unsafeCast the monadic value itself? That seems scarier... How about I remove the csimp here, and we can think about that in a follow-up PR?

leanprover-community-mathlib4-bot added a commit to leanprover-community/mathlib4 that referenced this pull request Nov 11, 2024
@digama0
Copy link
Member

digama0 commented Nov 11, 2024

It's actually somewhat safer, because the use of alpha in a nested context ensures that it will be boxed (so there will not be any difference in representation even if alpha is a primitive type).

Agreed that this is not critical and can be deferred.

leanprover-community-mathlib4-bot added a commit to leanprover-community/mathlib4 that referenced this pull request Nov 11, 2024
@Ericson2314
Copy link

Ericson2314 commented Nov 12, 2024

Very happy this exists right around when I wrote #1032 ! Crazy coincidence.

Thinking a bit further, I think the class can be simplified by inlining SatisfiesM and currying the existential into a universal (provided this doesn't fuck up the prop modality). I would write out what I mean exactly but I'm in my phone.

(I am not sure what Lean/Batteries had to truncate a Type to a Prop. But that must be used to curry the existential.)

@kim-em
Copy link
Collaborator Author

kim-em commented Nov 12, 2024

Very happy this exists right around when I wrote #1032 ! Crazy coincidence.

Thinking a bit further, I think the class can be simplified by inlining SatisfiesM and currying the existential into a universal (provided this doesn't fuck up the prop modality). I would write out what I mean exactly but I'm in my phone.

(I am not sure what Lean/Batteries had to truncate a Type to a Prop. But that must be used to curry the existential.)

Sorry, not following you here.

leanprover-community-mathlib4-bot added a commit to leanprover-community/mathlib4 that referenced this pull request Nov 12, 2024
@Ericson2314
Copy link

class MonadSatisfying (m : Type u → Type v) [Functor m] [LawfulFunctor m] where
  satisfying {p : α → Prop} {x : m α} (h : SatisfiesM (m := m) p x) : m {a // p a}
  val_eq {p : α → Prop} {x : m α} (h : SatisfiesM (m := m) p x) : Subtype.val <$> satisfying h = x

Now let's inline SatisfiesM:

class MonadSatisfying (m : Type u → Type v) [Functor m] [LawfulFunctor m] where
  satisfying {p : α → Prop} {x : m α} (h : ∃ x' : m {a // p a}, Subtype.val <$> x' = x) : m {a // p a}
  val_eq {p : α → Prop} {x : m α} (h : ∃ x' : m {a // p a}, Subtype.val <$> x' = x) : Subtype.val <$> satisfying h = x

now let's curry:

class MonadSatisfying (m : Type u → Type v) [Functor m] [LawfulFunctor m] where
  satisfying {p : α → Prop} {x : m α} [x'' : NonEmpty (m {a // p a})] (ext: Nonempty.elim x'' (fun x' => Subtype.val <$> x' = x) : m {a // p a}
  val_eq {p : α → Prop} {x : m α} [x'' : NonEmpty (m {a // p a})] (ext: Nonempty.elim x'' (fun x' => Subtype.val <$> x' = x) : Subtype.val <$> satisfying x'' ext = x

now let's simplify val_eq:

class MonadSatisfying (m : Type u → Type v) [Functor m] [LawfulFunctor m] where
  satisfying {p : α → Prop} {x : m α} [x'' : NonEmpty (m {a // p a})] (ext: Nonempty.elim x'' (fun x' => Subtype.val <$> x' = x) : m {a // p a}
  val_eq {p : α → Prop} {x : m α} [x'' : NonEmpty (m {a // p a})] (ext: Nonempty.elim x'' (fun x' => Subtype.val <$> x' = x) : Nonempty.elim x'' (fun x' => satisfying x'' ext = x')

This avoids the second use of <$>, and shows that satisfying constructs the monadic value whose mere existence is provided as an argument.

A non-class version of NonEmpty would be more appropriate for this.

@kim-em kim-em removed the awaiting-author Waiting for PR author to address issues label Nov 13, 2024
@kim-em
Copy link
Collaborator Author

kim-em commented Nov 13, 2024

@Ericson2314, I might let you pursue that in a separate PR if you're interested.

@digama0
Copy link
Member

digama0 commented Nov 13, 2024

@Ericson2314 the "let's curry" step is not correct. The assumption ext: Nonempty.elim x'' (fun x' => Subtype.val <$> x' = x) is not type correct because Nonempty.elim can only produce elements of propositions and Subtype.val <$> x' = x is not an element of a proposition (it is a proposition itself). You would have to use choice to do this elimination, but that's also not correct because that's saying that an arbitrary other element of the type m {a // p a} is equivalent to the monadic value x, which generally will not be the case unless m {a // p a} is a subsingleton.

The usage of mere existence in SatisfiesM is quite deliberate. You could have a data carrying version of SatisfiesM, this would be equivalent in strength to the conjunction of SatisfiesM and MonadSatisfying, but I wanted to avoid that because that would require recursively constructing this value over the structure of the type, while MonadSatisfying allows you to skip the whole process and use an identity function.

@digama0 digama0 added this pull request to the merge queue Nov 13, 2024
Merged via the queue into main with commit 11da075 Nov 13, 2024
3 checks passed
@Ericson2314
Copy link

can only produce elements of propositions and Subtype.val <$> x' = x is not an element of a proposition (it is a proposition itself).

True; my mistake.

I agree mere existence is well motivated. Can the function taking it be curried in some other way?

Alternatively, can we do this?

class MonadSatisfying (m : Type u → Type v) [Functor m] [LawfulFunctor m] where
  satisfying {p : α → Prop} {x : m α} (h : SatisfiesM (m := m) p x) : m {a // p a}
  val_eq {p : α → Prop} {x : m α} (h : SatisfiesM (m := m) p x) : let ⟨x', _⟩ := h ; satisfying h = x'

@digama0
Copy link
Member

digama0 commented Nov 14, 2024

I agree mere existence is well motivated. Can the function taking it be curried in some other way?

No, currying the function would make it equivalent to the data carrying version and hence make this function useless (an identity function).

Alternatively, can we do this?

No, the pattern match in the let is hiding a call to Exists.recOn and is also not type correct for the same reason as your previous version. Existential statements aren't actually storing any value x' which can meaningfully be used in such an equality. Once its in the existential package we completely forget which value x' we were talking about, and because this existential is not uniquely inhabited you can't recover the x' unless you have a function like satisfying to do it.

@Ericson2314
Copy link

Ericson2314 commented Nov 14, 2024

No, currying the function would make it equivalent to the data carrying version and hence make this function useless (an identity function).

There's no way to get a proof-irrelevant universal quantification for this? That seems like an odd deficiency. I would think NonEmpty ought to work, since class vs inductive is more or less sugar, right? One should be able to dependently eliminate it into _ : Prop, no?

Existential statements aren't actually storing any value x'

understood.

which can meaningfully be used in such an equality.

I don't get this because the equality is a prop, no? We shouldn't need proof relevance here any more than we need proof relevance on the RHS of SatisfiesM.

Once its in the existential package we completely forget which value x' we were talking about

Yes we cannot case on it, but we do have the propositional equality from the RHS to reason about it nevertheless.

because this existential is not uniquely inhabited you can't recover the x' unless you have a function like satisfying to do it.

But val_eq doesn't need to recover it! It is just prooving the proposition that satisfying recovered it.

Also, if we have

∃ x' : m {a // p a}, Subtype.val <$> x' = x
Subtype.val <$> satisfying h = x -- via old `val_eq`

then it should be possible to prove

Subtype.val <$> satisfying h = Subtype.val <$> x' 
satisfying h = x'

(with a sufficiently lawful functor)?

So even if my new version doesn't make any proofs shorter (since the strategy should use the old version as a lemma anyways) it should still work?


Consider also this one-method version

class MonadSatisfying (m : Type u → Type v) [Functor m] [LawfulFunctor m] where
  satisfying' 
    {p : α → Prop} 
    {x : m α}
    (h : SatisfiesM (m := m) p x)
    : {x'' : m {a // p a} // let ⟨x', _⟩ := h ; x'' = x' }

This is saying "give me an action, and merely prove the existence of an augmented action whose interior values are suitably refined, and I will construct that very same action".

The let, being to the right of the //, should safely be in Prop land, no?

@digama0
Copy link
Member

digama0 commented Nov 14, 2024

Let's take this to zulip

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants