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: new IChoiceMap interface, implementations (2/n) #52

Merged
merged 1 commit into from
Nov 18, 2023

Conversation

sritchie
Copy link
Collaborator

@sritchie sritchie commented Nov 14, 2023

This PR contains an implementation of the choicemap interface and data types found in Gen.jl. I stuck very closely to their behavior, sparsely implementing clojure.core methods where they were necessary or appropriate.

This PR follows #51 .

Also, a note for reviewers... the design here of using a "Choice" or "ValueChoiceMap" type follows GenJAX, as well as a design suggested by @georgematheos in these two PRs:

We actually did the latter in #23, but get-choices wasn't actually returning an IChoiceMap in all cases. This change fixes that, as George recognized.

This change from Gen.jl is blessed by @alexlew, Vikash and @femtomc.

@sritchie sritchie requested a review from zane November 14, 2023 19:57
@sritchie sritchie changed the title feat: new choicemap interface feat: new IChoiceMap interface, implementations Nov 14, 2023
Copy link

codecov bot commented Nov 14, 2023

Codecov Report

Attention: 46 lines in your changes are missing coverage. Please review.

Comparison is base (55aa92f) 68.58% compared to head (9a75b78) 71.88%.

Files Patch % Lines
src/gen/choicemap.cljc 81.45% 38 Missing and 8 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #52      +/-   ##
==========================================
+ Coverage   68.58%   71.88%   +3.30%     
==========================================
  Files          19       20       +1     
  Lines         748      996     +248     
  Branches       17       24       +7     
==========================================
+ Hits          513      716     +203     
- Misses        218      256      +38     
- Partials       17       24       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Base automatically changed from sritchie/array to main November 14, 2023 21:08
@sritchie sritchie changed the title feat: new IChoiceMap interface, implementations feat: new IChoiceMap interface, implementations (2/n) Nov 14, 2023
Copy link

@georgematheos georgematheos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sritchie this looks great!

I left a number of small comments, but the overall structure looks great to me.

I would not fully trust my review on the parts of this code which provide implementations of Clojure/Clojurescript interfaces for choicemaps, since I am not familiar with those interfaces.

What is going to happen to dynamic/choice_map.cljc after this file is added?

([m] (-get-value m))
([m k] (-get-value (get-submap m k))))

;; ## Choice

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice - I like the rebrand of ValueChoiceMap to Choice.

arr/IArray
(to-array [_] [v])
(-from-array [_ xs idx]
[1 (Choice. (nth xs idx))])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am probably misunderstanding the syntax here - but is this creating a vector [1, Choice(...)]?
If so, why is 1 the first element?
(If this is a syntax misunderstanding, pointers to a search term where I can understand this syntax? Thanks!)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep questions like this coming! also we should get a REPL up and pair soon.

This function wants a return value of a 2-element vector with

  • how many items you took off of the input array xs
  • the new value you created with those elements

As described here https://github.com/InferenceQL/gen.clj/blob/main/src/gen/array.cljc#L10

So for Choice, we take 1 thing off from xs at start-idx, and create the Choice instance in the second slot.

(And yes you're right that it's creating the vector [1, Choice(,,,,)]

(-from-array [_ xs idx]
[1 (Choice. (nth xs idx))])

#?@(:clj

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI - from here to the defn of Empty is beyond my current Clojure knowledge to understand (so I have not reviewed this chunk).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is surprisingly annoying in Clojure to extend the base interfaces!! these are flexed in the tests, and I tried to match the implementations in Gen.jl for things like getindex. I'll comment below on what these do


(declare kv->choicemap)

(deftype EmptyChoiceMap [m]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sritchie why does this accept an argument? (My guess: is it impossible to define types with no data in side in Clojure?)


(declare equiv choicemap)

(deftype DynamicChoiceMap [m]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder about the name DynamicChoiceMap here.

I think of the name DynamicChoiceMap in Gen.jl as stemming from the distinction with the StaticChoiceMap. Due to the way staging works in Gen.jl, the address shape of a StaticChoiceMap is known at compile time. Thus, methods for updating traces can be specialized to different StaticChoiceMap shapes at compile time. While the shape of a StaticChoiceMap is known "statically" (at compile time), the shape of a DynamicChoiceMap is "dynamic" (it can be modified at runtime). Thus, update methods which accept a DynamicChoiceMap cannot be specialized at compile-time to the shape of the update choicemap.

Is there going to be a similar distinction between dynamic choicemaps and static choicemaps in gen.clj?

Not totally sure what a better alternative name would be if not, but here are a few ideas... Maybe HierarchicalChoiceMap or DictChoiceMap or GeneralChoiceMap?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another possibility is to rename
ChoiceMap -> Choices
DynamicChoiceMap -> ChoiceMap
VectorChoiceMap -> ChoiceVector
and keep Choice.
This sort of implies VectorChoiceMaps don't have hierarchical structure, though.

So maybe the best bet is just to change DynamicChoiceMap -> GeneralChoiceMap to denote that (unlike a VectorChoiceMap) there are no restrictions on the key type.

It also seems reasonable to me to use DynamicChoiceMap for now even if it isn't super well motivated, just so things read similarly to Gen.jl for now. We could then possibly do a pass reconsidering names like this across all the Gen variants after GenJAX and gen.clj are more feature complete.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like these thoughts — I would recommend that we stay with the current name to match Gen.jl for now, and that if we make a shift we do it with Gen.jl too.

BUT there is one subtlety — there is both a DynamicChoiceMap (which is mutable, yeehaw) AND a DynamicDSLChoiceMap in Gen.jl. The latter is not mutable once it's returned by get_choices, but is a wrapper around the trie stored in the dynamic trace.

Dynamic is doing a lot of work in Gen :)

[v]
(if (choicemap? v)
(and (not (has-value? v))
(core/empty? v))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

am I right to understand - based on my quick googling - that this recursive call to core/empty? calls count on v? (or does it call the empty function you defined on each of the types above?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think empty? actually calls seq and sees if it returns nil. The empty function WITHOUT the question mark returns an empty instance of the supplied type:

gen.choicemap-test> (empty {1 2 3 4})
{}
gen.choicemap-test> (empty [1 2 3])
[]
gen.choicemap-test> (empty '(1 2 3))
()
gen.choicemap-test> (empty (choicemap/choicemap [1 2 3]))
#gen/choicemap {}

"Tests for the [[gen.choicemap]] namespace and the various base implementations
that live there."
(:require [clojure.test :refer [deftest is testing]]
[clojure.test.check.generators :as gen]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean gen throughout the tests refers to clojure.test.check.generators (not Gen the PPL)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, correct, confusing here I guess but I think that's been their convention... that's not going to work in other namespaces though so I should change it!

(gen/map k-gen v-gen))))

(defn gen-vector-choicemap
"Returns a generator that produces [[gen.choicemap/Choice]] instances."

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring typo

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thank you!

(checking "not-found arity" 100
[m (generators/gen-dynamic-choicemap)]
(is (= ::not-found
(m ::not-present ::not-found))))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any pointers to where I can learn what ::not-found and ::not-present mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those are "namespaced keywords": https://blog.jeaye.com/2017/10/31/clojure-keywords/

So ::not-found expands out to :gen.choicemap-test/not-found. I use it here because the generative testing library is going to generate many random keywords and I want to make sure that my not-found "sentinel" doesn't clash with any of those.

(is (choicemap/empty? []))
(is (choicemap/empty? {})))

(testing "merge"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to add a test for merging hierarchical choicemaps?
E.g. {:k 1} with {:a {:b 1}}, and {:k 1} with {:k {:a 1}}. (Is the second case supposed to be an error?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added, and yes!

Copy link
Collaborator Author

@sritchie sritchie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great comments, hopefully these responses help!

arr/IArray
(to-array [_] [v])
(-from-array [_ xs idx]
[1 (Choice. (nth xs idx))])
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep questions like this coming! also we should get a REPL up and pair soon.

This function wants a return value of a 2-element vector with

  • how many items you took off of the input array xs
  • the new value you created with those elements

As described here https://github.com/InferenceQL/gen.clj/blob/main/src/gen/array.cljc#L10

So for Choice, we take 1 thing off from xs at start-idx, and create the Choice instance in the second slot.

(And yes you're right that it's creating the vector [1, Choice(,,,,)]

(-from-array [_ xs idx]
[1 (Choice. (nth xs idx))])

#?@(:clj
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is surprisingly annoying in Clojure to extend the base interfaces!! these are flexed in the tests, and I tried to match the implementations in Gen.jl for things like getindex. I'll comment below on what these do


#?@(:clj
[Object
(toString [this] (pr-str this))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basic equality and stringification

(equiv [this other] (-equiv this other))

IPrintWithWriter
(-pr-writer [_ writer opts]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClojureScript is more modular with its interfaces; this is how ClojureScript prints, and the interface below is more generic equality than the js equality above.

(and (instance? Choice o)
(= v (.-v ^Choice o))))]))

#?(:cljs
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the next two methods use our print implementation with clojure's default printer and pretty printer.

[v]
(if (choicemap? v)
(and (not (has-value? v))
(core/empty? v))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think empty? actually calls seq and sees if it returns nil. The empty function WITHOUT the question mark returns an empty instance of the supplied type:

gen.choicemap-test> (empty {1 2 3 4})
{}
gen.choicemap-test> (empty [1 2 3])
[]
gen.choicemap-test> (empty '(1 2 3))
()
gen.choicemap-test> (empty (choicemap/choicemap [1 2 3]))
#gen/choicemap {}

"Tests for the [[gen.choicemap]] namespace and the various base implementations
that live there."
(:require [clojure.test :refer [deftest is testing]]
[clojure.test.check.generators :as gen]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, correct, confusing here I guess but I think that's been their convention... that's not going to work in other namespaces though so I should change it!

(checking "not-found arity" 100
[m (generators/gen-dynamic-choicemap)]
(is (= ::not-found
(m ::not-present ::not-found))))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those are "namespaced keywords": https://blog.jeaye.com/2017/10/31/clojure-keywords/

So ::not-found expands out to :gen.choicemap-test/not-found. I use it here because the generative testing library is going to generate many random keywords and I want to make sure that my not-found "sentinel" doesn't clash with any of those.

(is (choicemap/empty? []))
(is (choicemap/empty? {})))

(testing "merge"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added, and yes!

(gen/map k-gen v-gen))))

(defn gen-vector-choicemap
"Returns a generator that produces [[gen.choicemap/Choice]] instances."
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thank you!

@sritchie
Copy link
Collaborator Author

@georgematheos good Q... I am going to delete gen/choice_map.cljc in favor of this, after a final PR that converts the dynamic language implementation pieces over to this new stuff + #54 . This is the future!

@georgematheos
Copy link

Thanks for the detailed responses -- looks great!

@sritchie sritchie merged commit 868346a into main Nov 18, 2023
6 checks passed
@sritchie sritchie deleted the sritchie/choicemap branch November 18, 2023 01:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants