Zulip Chat Archive

Stream: lean4

Topic: How to better compose/pipeline monads


Nicolas Rouquette (Dec 30 2022 at 05:49):

Background:

I am writing a Lean4 DSL for my modeling language, the Ontological Modeling Language, which I would like to define as a subset of the OWL2-DL description logic: https://github.com/opencaesar/oml

Unlike a programming language DSL where it would be sensible to embed the DSL as Lean expressions, this is a different kind of DSL problem in that the semantics of the language is a kind of logic, not executable program. So, what matters here is computing logical consistency, entailments, subsumption, ...

I have made baby steps using a monadic style -- thanks so much for the books/doc in progress; it's been hugely helpful!

My problem:

I have several monads that I wrote to resolve OML syntax into an intermediate representations (logical axioms).
For details, see: https://github.com/NicolasRouquette/oml.lean4

I have these definitions of context (input OML syntax) & state (logical axioms)

abbrev MCore := EStateM Exception State
abbrev M     := ReaderT Context MCore

In https://github.com/NicolasRouquette/oml.lean4/blob/master/src/Oml/Resolver.lean, there are a few toplevel resolvers to analyze the input OML syntax from the context and update the state of logical axioms:

def validateVocabularyDeclarations: M State := do
  ( read).vocabularies.foldlM validateVocabularyDeclaration ( get)

def validateVocabularyStatementDeclarations: M State := do
  ( read).vocabularies.foldlM validateStatementDeclarationsIn ( get)

def validateVocabularySpecializations: M State := do
  ( read).vocabularies.foldlM validateVocabularySpecializationsIn ( get)

I wrote tests to exercise these resolver monads in https://github.com/NicolasRouquette/oml.lean4/blob/master/src/Oml/testResolver.lean

def c1 : Context := { vocabularies := [testSyntax.rdfs, testSyntax.xsd, testSyntax.base, testSyntax.mission ] }
def s0 : State := {}

def r1a : EStateM.Result Exception State State := validateVocabularyDeclarations |>.run c1 |>.run s0

Next, I need to get the result of the above and run validateVocabularyStatementDeclarations.

This is where things get a wee bit ugly:

One way is to extract the result, assuming it is OK and run the next resolver.

def s1 : State := match r1a with | EStateM.Result.ok s _ => s | _ => {}
def r1b : EStateM.Result Exception State State := validateVocabularyStatementDeclarations |>.run c1 |>.run s1

def s2 : State := match r1b with | EStateM.Result.ok s _ => s | _ => {}
def r1c : EStateM.Result Exception State State := validateVocabularySpecializations |>.run c1 |>.run s2

Surely, there must be better way to compose these monads; though it is unclear to me how to do this.

I managed to two monads successfully, e.g.:

def validateVocabularyStatementDeclarations': M State := do
  let c: Context  read
  validateVocabularyDeclarations >>= (fun s => c.vocabularies.foldlM validateStatementDeclarationsIn s)

This allows me to write instead:

def r1b' : EStateM.Result Exception State State := validateVocabularyStatementDeclarations' |>.run c1 |>.run s0

But this does not to work for composing 3 monads:

def validateVocabularySpecializations': M State := do
  let c: Context  read
  validateVocabularyDeclarations
  >>= (fun s => c.vocabularies.foldlM validateStatementDeclarationsIn s)
  >>= (fun s => c.vocabularies.foldlM validateVocabularySpecializationsIn s)

It seems that it is running the 1st and 3rd, skipping the 2nd.

What would be a better way to write this?

Mario Carneiro (Dec 30 2022 at 06:00):

Unlike a programming language DSL where it would be sensible to embed the DSL as Lean expressions, this is a different kind of DSL problem in that the semantics of the language is a kind of logic, not executable program. So, what matters here is computing logical consistency, entailments, subsumption, ...

Just regarding this point: it doesn't really matter what your DSL represents, since it's basically going to facilitate turning expressions into syntax that you can do whatever with. There isn't any special attention paid to syntax that represents executable programs when we are talking about a custom DSL - it all just turns into an AST

Mario Carneiro (Dec 30 2022 at 06:03):

As a terminological point, you aren't composing monads, you are composing monadic operations (using bind). Composing monads is stuff like ReaderT r (StateT s (Except e m))

Mario Carneiro (Dec 30 2022 at 06:05):

The usual way to write your example is:

def validateVocabularySpecializations': M State := do
  let c: Context  read
  let mut s <- validateVocabularyDeclarations
  for x in c.vocabularies do
    s <- validateStatementDeclarationsIn s x
  for x in c.vocabularies do
    s <- validateVocabularySpecializationsIn s x
  pure s

(I think - untested since there is no #mwe)

Mario Carneiro (Dec 30 2022 at 06:06):

But this is a bit fishy, because you have State appearing twice: it is in the monad M (which wraps MCore which wraps a state monad over State) and it is also the return value of the function. This is most likely where the missing state is going

Mario Carneiro (Dec 30 2022 at 06:07):

If you want the state to be part of the monad, then you shouldn't be returning M State, it should be M Unit and you don't need to get s all the time

def validateVocabularySpecializations': M Unit := do
  validateVocabularyDeclarations
  for x in c.vocabularies do
    validateStatementDeclarationsIn x
  for x in c.vocabularies do
    validateVocabularySpecializationsIn x

Nicolas Rouquette (Dec 30 2022 at 20:28):

Thanks for the terminology correction and the suggestions, which worked very well.

def validateVocabularySpecializations': M Unit := do
  let c: Context  read
  validateVocabularyDeclarations
  for v in c.vocabularies do
    validateStatementDeclarationsIn v
  for v in c.vocabularies do
    validateVocabularySpecializationsIn v

Applying your suggestion to other functions, e.g.:

def validateVocabularyDeclarations: M Unit := do
  for x in ( read).vocabularies do
    validateVocabularyDeclaration x

I can further simplify it to just:

def validateVocabularySpecializations'': M Unit := do
  validateVocabularyDeclarations
  validateVocabularyStatementDeclarations
  validateVocabularySpecializations

This is really nice!

For testing purposes, I am wondering what is the proper way to extract the monad's state outside of ado?

With the above, I get nothing:

def r1c'': EStateM.Result Exception State Unit := validateVocabularySpecializations'' |>.run c1 |>.run s0
#eval r1c''

I know that I can write a simple accessor like this so that I can check the resulting state:

def resolverGet: M State := do
  pure ( get)

def validateVocabularySpecializations''Get: M State := do
  validateVocabularySpecializations''
  resolverGet

def r1c2 : EStateM.Result Exception State State := validateVocabularySpecializations''Get |>.run c1 |>.run s0
#eval r1c2

Is there a way to write the equivalent of validateVocabularySpecializations''Getwithout defining resolverGet?

Mario Carneiro (Dec 30 2022 at 20:30):

I'm not sure what you mean by "I get nothing" there

Mario Carneiro (Dec 30 2022 at 20:30):

That result object should contain the final state; you can pattern match on it to get at it

Nicolas Rouquette (Dec 30 2022 at 20:50):

Oh right! I was a bit confused with the output:

def r1c'': EStateM.Result Exception State Unit := validateVocabularySpecializations'' |>.run c1 |>.run s0
#eval r1c''
-- EStateM.Result.ok ()

Indeed, with pattern matching I can extract the state from the result:

def s1c'' : State := match r1c'' with | EStateM.Result.ok _ s => s | _ => {}
#eval s1c''
-- { .... }

Thanks!


Last updated: Dec 20 2023 at 11:08 UTC