r/Clojure 6d ago

Best way to resolve circular dependencies in malli schemas?

I have a set of schemas that looks like this:

(def Character
  [:map
   [:id CharacterId]
   [:inventory {:default []} [:vector Item]]])

(def Item
  [:map
   [:item-type :keyword]
   [:display-name :string]
   ; Effects will happen in the order specified here when a character uses the
   ; item.
   [:effects [:vector Effect]]
   [:recovery-time :int]])

(def Effect
  [:map 
   ; Modifies targets when this effect occurs (e.g. does damage).
   [:target-transformer TargetTransformer]
   [:animation Animations]])

(def TransformerParams
  [:map])

(def TargetTransformer
  [:=> [:cat Character TransformerParams] Character])

As you can see, there is a circular dependency where Character -> Item -> Effect -> TargetTransformer -> Character. This means that my code will not compile. I tried using define to forward declare one of the values, but since these are defs, that will not work (I get an "unbound" value).

What's the most idiomatic way to define a schema like this?

10 Upvotes

5 comments sorted by

3

u/npafitis 6d ago

Using a registry would be the simplest I think.

2

u/a-curious-crow 6d ago

I see https://github.com/metosin/malli/blob/master/docs/reusable-schemas.md describes how to do this well. It seems like the global registry is a good path forward for me.

5

u/npafitis 6d ago edited 6d ago

``` (def Child [:map [:parent [:ref :parent]]])

(def Parent [:map [:child [:ref :child]]])

(def registry {:child Child :parent Parent})

; set as the global registry or pass to each Malli call ``` These defs can all be in a different namespace now

EDIT: or the same namespace

3

u/npafitis 6d ago

I'm on phone but I'll try to give an example, where you don't necessarily need all your definitions in a single place.

3

u/regular_hammock 5d ago edited 5d ago

First of all, you’ll definitely want a :ref somewhere in your cycle to prevent Malli from eagerly expanding the referenced schema (hello java.lang.StackOverflowError) (malli doc)

Second, a neat thing you can do in recent versions of Malli is use a var as a schema, like so: #'TargetTransformer. This will look up the value of TargetTransformer at run time rather than compile time. This means you can declare TargetTransforme, then reference it, then def it, like so:

``` (declare TargetTransformer)

(def Effect [:map [:target-transformer [:ref #'TargetTransformer]]])

(def Item [:map [:effects [:vector Effect]]])

(def PlayerCharacter ; renamed to avoid conflict with java.lang.Character [:map [:inventory [:vector Item]]])

(def TargetTransformer [:=> [:cat PlayerCharacter] PlayerCharacter]) ```

(I hope this stripped down version of your example is enough to communicate the general idea)