When working with [Fennel](https://fennel-lang.org/) in [Neovim](https://neovim.io/) through [Conjure](https://github.com/Olical/conjure) you will probably encounter the issue of reloading modules or parts of a module in ways that the rest of your system can see. It's no fun evaluating a function only to realise that the changes won't be picked up by another module until you do some weird Lua hacking or restart your editor. In the past I solved this issue in [Aniseed](https://github.com/Olical/aniseed) using "module macros" which imitated [Clojure](https://clojure.org/) syntax with a handful of tricks to enable interactive reloading of individual values of a module. ```clojure (module foo) (defn bar [] :hello) ``` When evaluating `bar` in Conjure it would correctly reload the value for other modules the depended on it. The move to [nfnl](https://github.com/Olical/nfnl) however required a different way of thinking, I no longer recommend heavy handed macros, instead I use some simple rules of thumb along with some slightly clever inference inside Conjure itself to provide essentially the same experience for near zero cost. A core tenant of nfnl. ## Recipe ```clojure ;; autoload is an optional lazy replacement for require ;; define is an optional helper to make this more succinct (local {: autoload : define} (require :conjure.nfnl.module)) ;; Define our module table M ;; By using define we will either look up the existing module or define a new one using the given second argument. ;; If you don't pass a default value {} is used. (local M ;; For fnl/some/module/name.fnl (define :some.module.name ;; Default value for when we're not loaded yet. {:my-cool-thing 10})) (fn private-fn [] "this is a private function, it's not exported") (fn M.public-fn [] "this is a public function, every time you evaluate it, it gets shared with everyone that depends on this module") (set M.public-value "and this is a public value we might want to share") ;; Remember to return the module table! M ``` That's all there is to it. You make sure you look the existing module table up if there is one, set a default if not then define values on that table using the `fn` or `set` syntax. Finally, you return the module table at the end of your file in order to export it to the world. Each subsequent evaluation through Conjure (provided you perform a full file evaluation first) will reload either the entire module or the individual values as you evaluate them form by form in a way that other modules will see immediately. ## `define` for people who don't want nfnl Here's the source code! You can just paste it into your project if you want the same sort of "reuse the existing module if there is one" behaviour. ```clojure (fn M.define [mod-name base] "Looks up the mod-name in package.loaded, if it's the same type as base it'll use the loaded value. If it's different it'll use base. The returned result should be used as your default value for M like so: (local M (define :my.mod {})) Then return M at the bottom of your file and define functions on M like so: (fn M.my-fn [x] (+ x 1)) This technique helps you have extremely reloadable modules through Conjure. You can reload the entire file or induvidual function definitions and the changes will be reflected in all other modules that depend on this one without having to reload the dependant modules. The base value defaults to {}, an empty table." (let [loaded (. package.loaded mod-name)] (if (= (type loaded) (type base)) loaded (or base {})))) ``` ## Conjure's full file reload magic It wouldn't be Conjure without a _little_ magic. We do two fairly magic things, firstly, we translate file paths into module names. ```clojure (fn M.module-path [path] "Turns a full file path into a dot.delimited.module.path. We can then use this module path to perform live reloads by modifying the currently loaded module. Finds the closest root `fnl` directory and uses that as the root of the module path. TODO: Make this configurable so that non-standard Fennel setups also work. Maybe just read the .nfnl.fnl configuration since that is what we are supposed to be working with." (when path (let [parts (-> path (fs.file-name-root) (fs.split-path)) fnl-and-below (core.drop-while #(not= $1 "fnl") parts)] (when (= "fnl" (core.first fnl-and-below)) (str.join "." (core.rest fnl-and-below)))))) ``` And secondly, when we detect that we're evaluating a buffer or file from disk and when that evaluation results in a table, we _merge_ that result into the existing module table rather than replacing it outright. This means that even if you're not using the `define` technique and you make a new table every time, Conjure will try it's best to merge the new values into the existing table reference, preserving existing references to this module across your project while updating the values. ```clojure ;; When we evaluate a whole file and it ends in a table, we merge that table into the loaded module. ;; This allows you to reload a module with ef or eb. (when (and mod-path (or (= :buf opts.origin) (= :file opts.origin)) (core.table? (core.last results))) (let [mod (core.get package.loaded mod-path)] (tset package.loaded mod-path (core.merge! mod (core.last results))))) ``` If you're interested in the details, please do have a look around the [nfnl Conjure client source](https://github.com/Olical/conjure/blob/main/fnl/conjure/client/fennel/nfnl.fnl) - it's not very complicated (at the time of writing).