Zulip Chat Archive

Stream: lean4

Topic: HashMap extension


Patrick Massot (Aug 22 2022 at 20:41):

Playing with environment extensions, I realized I kept using extensions whose states were lists of pairs and searching in the list elements with a given first component. So I feel like I want extensions where the state is a hash map. But my understanding of environment extensions is very shallow. The following code seems to be doing what I want, but I'm not confident at all that I'm not doing something very stupid.

import Lean
import Std.Data.HashMap

open Lean Std

def HashMapExtension (α β : Type) [BEq α] [Hashable α] := SimplePersistentEnvExtension (α × β) (HashMap α β)

instance (α β : Type) [BEq α] [Hashable α] : Inhabited (HashMapExtension α β) :=
  inferInstanceAs (Inhabited (SimplePersistentEnvExtension (α × β) (HashMap α β)))

def mkHashMapExtension (name : Name) (α β : Type) [BEq α] [Hashable α]  : IO (HashMapExtension α β) :=
  registerSimplePersistentEnvExtension {
    name          := name,
    addImportedFn := fun as => HashMap.ofList (as.concatMap id).toList,
    addEntryFn    := fun s n => s.insert n.1 n.2 ,
    toArrayFn     := fun es => es.toArray
  }

-- Now I'll write some tests

initialize testExt : HashMapExtension Nat String  mkHashMapExtension `my_test Nat String

elab "#add_test" name:num content:str : command =>
  modifyEnv (testExt.addEntry · (name.getNat, content.getString))

elab "#list_tests" : command => do
  let s := testExt.getState ( getEnv)
  dbg_trace s.toList

elab "#get_test" n:num : command => do
  let s := testExt.getState ( getEnv)
  dbg_trace s.find? n.getNat

Patrick Massot (Aug 22 2022 at 21:21):

@Sebastian Ullrich while you're here, do you have any comment about this thread?

Sebastian Ullrich (Aug 22 2022 at 21:27):

Looks good I think. You can use mkStateFromImportedEntries to simplify and optimize your addImportedFn.

Patrick Massot (Aug 22 2022 at 21:29):

I was especially worried about that addImportedFn indeed. Now I need to decipher what mkStateFromImportedEntries does...

Patrick Massot (Aug 22 2022 at 21:34):

Do you mean

import Lean
import Std.Data.HashMap

open Lean Std

def HashMapExtension (α β : Type) [BEq α] [Hashable α] := SimplePersistentEnvExtension (α × β) (HashMap α β)

instance (α β : Type) [BEq α] [Hashable α] : Inhabited (HashMapExtension α β) :=
  inferInstanceAs (Inhabited (SimplePersistentEnvExtension (α × β) (HashMap α β)))

def HashMapExtension.addEntryFn {α β : Type} [BEq α] [Hashable α] (s : HashMap α β) (n : α × β) : HashMap α β :=
s.insert n.1 n.2

def mkHashMapExtension (name : Name) (α β : Type) [BEq α] [Hashable α]  : IO (HashMapExtension α β) :=
  registerSimplePersistentEnvExtension {
    name          := name,
    addImportedFn := mkStateFromImportedEntries HashMapExtension.addEntryFn {},
    addEntryFn    := HashMapExtension.addEntryFn,
    toArrayFn     := fun es => es.toArray
  }

Sebastian Ullrich (Aug 22 2022 at 21:55):

Yes, though I would probably have inlined the new function as a lambda :)

Patrick Massot (Aug 22 2022 at 22:12):

Thanks!

Patrick Massot (Sep 26 2022 at 12:12):

I'm returning to this old thread. Now that I try to actually use this HashMap extension idea I see that it doesn't work. I have a file HashMapExt.lean with

import Lean

open Lean Std

def HashMapExtension (α β : Type) [BEq α] [Hashable α] := SimplePersistentEnvExtension (α × β) (HashMap α β)

instance (α β : Type) [BEq α] [Hashable α] : Inhabited (HashMapExtension α β) :=
  inferInstanceAs (Inhabited (SimplePersistentEnvExtension (α × β) (HashMap α β)))

def mkHashMapExtension (name : Name) (α β : Type) [BEq α] [Hashable α]  : IO (HashMapExtension α β) :=
  registerSimplePersistentEnvExtension {
    name          := name,
    addImportedFn := mkStateFromImportedEntries (λ s n => s.insert n.1 n.2) {},
    addEntryFn    := (λ s n => s.insert n.1 n.2),
    toArrayFn     := fun es => es.toArray
  }

namespace HashMapExtension

variable {α β : Type} [BEq α] [Hashable α] {m: Type  Type} [Monad m] [MonadEnv m]

def find? (ext : HashMapExtension α β) (a : α) : m $ Option β := do
  return (ext.getState ( getEnv)).find? a

def insert (ext : HashMapExtension α β) (a : α) (b : β) : m Unit :=
  modifyEnv (ext.modifyState · (·.insert a b))

def update (ext : HashMapExtension α β) (a : α) (b : β) : m Unit :=
  modifyEnv (ext.modifyState · (λ hm => (hm.erase a).insert a b))

end HashMapExtension

and then a second file test0.lean with

import HashMapExt

initialize testExt : HashMapExtension Nat String  mkHashMapExtension `test Nat String

elab "Test" n:num s:str : command => do
  testExt.insert n.getNat s.getString

open Lean

elab "PrintTests" : command => do
  logInfo $ repr $ (testExt.getState ( getEnv)).toList

and test1.lean

import test0

Test 3 "Foo"

PrintTests

so far everything seems to work. But when I create test2.lean with

import test1

Test 4 "Bar"

PrintTests

I see only the entry created in test2, not two entries as I expected.

Patrick Massot (Sep 26 2022 at 12:21):

Sebastian, do you see what I'm doing wrong?

Sebastian Ullrich (Sep 26 2022 at 12:27):

Not directly, I'd have to take a closer look

Patrick Massot (Sep 26 2022 at 13:18):

Should I create an issue then?

Patrick Massot (Sep 26 2022 at 13:31):

For people who want to investigate this, issue.zip contains a folder issue with a Lean project showing my problem in the Issue lib.

Patrick Massot (Sep 26 2022 at 15:38):

(deleted)

Gabriel Ebner (Sep 26 2022 at 18:49):

Your issue is with the HashMapExtension.insert and update functions.

Gabriel Ebner (Sep 26 2022 at 18:49):

The SimplePersistentEnvExtension.modifyState should never be used because the state is not persisted.

Gabriel Ebner (Sep 26 2022 at 18:50):

(It's obvious that it can't be persisted: what is being written to the olean file is the entries, while modifyState applies some opaque function to the state. It doesn't know which entries to store to recreate the state modification.)

Gabriel Ebner (Sep 26 2022 at 18:51):

This should fix your issues:

def insert (ext : HashMapExtension α β) (a : α) (b : β) : m Unit :=
  modifyEnv (ext.addEntry · (a, b))

def update (ext : HashMapExtension α β) (a : α) (b : β) : m Unit :=
  modifyEnv (ext.addEntry · (a, b))

Patrick Massot (Sep 26 2022 at 18:59):

Indeed it works much better!

Patrick Massot (Sep 26 2022 at 18:59):

Thanks a lot Gabriel!

Patrick Massot (Sep 26 2022 at 19:01):

SimplePersistentEnvExtension.modifyState is indeed rather misleading. Should I PR a docstring with a warning or do you think it should be completely removed?

Gabriel Ebner (Sep 26 2022 at 19:03):

Yes. At least a docstring would be good.

Patrick Massot (Sep 26 2022 at 19:04):

It has nothing to do with "Simple". Already PersistentExtension.modifyState is implemented as

def modifyState {α β σ : Type} (ext : PersistentEnvExtension α β σ) (env : Environment) (f : σ  σ) : Environment :=
  ext.toEnvExtension.modifyState env fun ps => { ps with state := f (ps.state) }

where ext.toEnvExtension very much sounds like to first step is to forget about persistance...

Gabriel Ebner (Sep 26 2022 at 19:04):

Yeah, that function should get a docstring as well.

Gabriel Ebner (Sep 26 2022 at 19:05):

I believe the only use of these dangerous functions is to implement ScopedEnvExtension .

Patrick Massot (Sep 26 2022 at 19:05):

Actually the same projection occurs in addEntry so it doesn't explain why your version works

Gabriel Ebner (Sep 26 2022 at 19:05):

Maybe it's best to just remove both sets of them, and then have ScopedEnvExtension break the abstraction barrier explicitly.

Gabriel Ebner (Sep 26 2022 at 19:07):

Patrick Massot said:

Actually the same projection occurs in addEntry so it doesn't explain why your version works

If you look at the addEntry function, it calls the addEntryFn field. And the addEntryFn is set by registerSimplePersistentEnvExtension to a function that updates both the state as well as the entries.

Patrick Massot (Sep 26 2022 at 19:25):

I opened https://github.com/leanprover/lean4/pull/1649


Last updated: Dec 20 2023 at 11:08 UTC