Zulip Chat Archive
Stream: lean4
Topic: Idiomatic way of proving things about Applicatives
Brendan Seamas Murphy (Nov 11 2023 at 00:51):
The definition of an Applicative
in Lean is different from the one in Haskell, specifically the operator <*>
is different. The type signature of Seq.seq
is different than the type signature of <*>
, in that it takes a thunk as its second argument. My understanding is that this is so that you don't waste time on big computations that won't be evaluated anyway (e.g. if you're in the Maybe monad) don't have time wasted on them. Additionally <*>
is a more complicated sort of macro than a normal infix operator (this makes propositions involving <*>
, e.g. the traversable laws, display with <*>
expanded). These design choices make practical sense, but logically (in the precense of function extensionality) we have (Unit → α) ≃ α
and so none of this should really matter when reasoning about Applicative
s internal to Lean. Is therething I'm missing, or some way to avoid the pain point of <*>
printing weirdly?
Brendan Seamas Murphy (Nov 11 2023 at 19:57):
I guess also it's not clear to me why Seq
takes one thunk and one non-thunk argument, instead of having Seq.seq
be derived from some more symmetric primitive Seq.seq' : {α β : Type u} → (Unit → f (α → β)) → (Unit → f α) → f β
?
Brendan Seamas Murphy (Nov 12 2023 at 21:07):
Also, somewhat related, it seems like if you define a monad by just giving pure and bind and then want to prove that it's a lawful monad then you'll get some junk proof burdens about the applicative data that's just been derived from the given pure/bind. I might be missing something, but isn't requiring the user to prove seqLeft_eq
totally redundant here?
Kyle Miller (Nov 12 2023 at 21:35):
Yes, Lean uses eager evaluation and Haskell uses lazy evaluation, which explains the practical difference in their types.
Brendan Seamas Murphy said:
I guess also it's not clear to me why
Seq
takes one thunk and one non-thunk argument, instead of havingSeq.seq
be derived from some more symmetric primitiveSeq.seq' : {α β : Type u} → (Unit → f (α → β)) → (Unit → f α) → f β
?
Seq
always evaluates the first argument and it might evaluate the second, so there's no need for it to be symmetric. This operator is for writing programs, and making the first argument be Unit -> ...
as well would just be giving the compiler more work to do to eliminate the function application.
Kyle Miller (Nov 12 2023 at 21:37):
Brendan Seamas Murphy said:
I might be missing something, but isn't requiring the user to prove
seqLeft_eq
totally redundant here?
I don't know what can be derived from what, but if you're able to prove it in general, you could add the proof as a default value for seqLeft_eq
in the LawfulMonad
definition, just like how it's done for map_pure
etc.
Brendan Seamas Murphy (Nov 12 2023 at 21:38):
Seq
does always evaluate the first argument, but that's exactly the design choice I'm asking about. Applicative
s are completely symmetric, you can always reverse their internal evaluation order, so why assume in the type of Seq.seq
that evaluation is occuring left to right?
Kyle Miller (Nov 12 2023 at 21:41):
Maybe for consistency with other operators like &&
which also evaluate left-to-right? It's also conceivable that they wanted to make sure the compiler didn't have to unfold Seq.seq
and eliminate function applications before seeing how it sequences, similar to what I already suggested.
Kyle Miller (Nov 12 2023 at 21:42):
Re pretty printing of Seq.seq
, that just seems to be an oversight.
Kyle Miller (Nov 12 2023 at 21:44):
To do it right takes a bit of work that hasn't been done yet. (You can write an app_unexpander
that's right almost all the time, but it'd be good to make sure it really is always right.)
Brendan Seamas Murphy (Nov 12 2023 at 21:46):
&&
was a good point of comparison, I didn't know about @[macro_inline]
. Maybe I'll toy around with it and see if I can somehow modify <*>
to be a @[macro_inline]
operator which calls Seq.seq
on a thunk without breaking anything. Thanks!
Kyle Miller (Nov 12 2023 at 21:54):
@Brendan Seamas Murphy If all you're wanting is nice pretty printing, then here's what's missing:
import Lean
section Delab
open Lean PrettyPrinter.Delaborator SubExpr
@[delab app.Seq.seq]
def delabSeq : Delab := whenPPOption Lean.getPPNotation do
let #[_, _, _, _, _, thunk] := (← getExpr).getAppArgs | failure
guard <| thunk.isLambda && !thunk.bindingBody!.hasLooseBVar 0
let a ← withAppFn <| withAppArg delab
let b ← withAppArg <| withBindingBody `_ delab
`($a <*> $b)
@[delab app.SeqLeft.seqLeft]
def delabSeqLeft : Delab := whenPPOption Lean.getPPNotation do
let #[_, _, _, _, _, thunk] := (← getExpr).getAppArgs | failure
guard <| thunk.isLambda && !thunk.bindingBody!.hasLooseBVar 0
let a ← withAppFn <| withAppArg delab
let b ← withAppArg <| withBindingBody `_ delab
`($a <* $b)
@[delab app.SeqRight.seqRight]
def delabSeqRight : Delab := whenPPOption Lean.getPPNotation do
let #[_, _, _, _, _, thunk] := (← getExpr).getAppArgs | failure
guard <| thunk.isLambda && !thunk.bindingBody!.hasLooseBVar 0
let a ← withAppFn <| withAppArg delab
let b ← withAppArg <| withBindingBody `_ delab
`($a *> $b)
end Delab
-- Example
#print LawfulApplicative
/-
LawfulApplicative.mk : ∀ {f : Type u → Type v} [inst : Applicative f] [toLawfulFunctor : LawfulFunctor f],
(∀ {α β : Type u} (x : f α) (y : f β), x <* y = Function.const β <$> x <*> y) →
(∀ {α β : Type u} (x : f α) (y : f β), x *> y = Function.const α id <$> x <*> y) →
(∀ {α β : Type u} (g : α → β) (x : f α), pure g <*> x = g <$> x) →
(∀ {α β : Type u} (g : α → β) (x : α), g <$> pure x = pure (g x)) →
(∀ {α β : Type u} (g : f (α → β)) (x : α), g <*> pure x = (fun h ↦ h x) <$> g) →
(∀ {α β γ : Type u} (x : f α) (g : f (α → β)) (h : f (β → γ)),
h <*> (g <*> x) = Function.comp <$> h <*> g <*> x) →
LawfulApplicative f
-/
Kyle Miller (Nov 12 2023 at 21:55):
I just had it print LawfulApplicative
as a test because it had all the operators
Brendan Seamas Murphy (Nov 12 2023 at 21:55):
Thanks, that's awesome! Should be much easier to read now
Brendan Seamas Murphy (Nov 12 2023 at 21:57):
Kyle Miller said:
Brendan Seamas Murphy said:
I might be missing something, but isn't requiring the user to prove
seqLeft_eq
totally redundant here?I don't know what can be derived from what, but if you're able to prove it in general, you could add the proof as a default value for
seqLeft_eq
in theLawfulMonad
definition, just like how it's done formap_pure
etc.
I think the issue is that map_pure
can be proven without knowing the actual definition of the Applicative
structure, using bind_pure_comp
and pure_bind
, while the implementation I'm suggesting requires you to know that the Applicative
structure was constructed using the defaults in the declaration of Monad
. Maybe it could be an alternate constructor for LawfulMonad
? I'm not clear enough on how the default implementation sugar actually works
Eric Wieser (Nov 12 2023 at 22:36):
Brendan Seamas Murphy said:
Also, somewhat related, it seems like if you define a monad by just giving pure and bind and then want to prove that it's a lawful monad then you'll get some junk proof burdens about the applicative data that's just been derived from the given pure/bind.
I think this is what docs#LawfulMonad.mk' is for
Brendan Seamas Murphy (Nov 12 2023 at 22:36):
!!! Tysm
Last updated: Dec 20 2023 at 11:08 UTC