lazy.md (4807B)
1 # Lazy Evaluation 2 3 ## The Problem 4 5 `nickel_eval` evaluates the entire expression tree before returning. For a large configuration file with hundreds of fields, this means every field is computed even if you only need one: 6 7 ```julia 8 # Evaluates ALL fields, even though we only need port 9 config = nickel_eval_file("large_config.ncl") 10 config["database"]["port"] # => 5432 11 ``` 12 13 ## The Solution: `nickel_open` 14 15 `nickel_open` evaluates shallowly — it figures out the top-level structure but leaves field values as frozen computations. Values are only computed when you access them: 16 17 ```julia 18 nickel_open("large_config.ncl") do cfg 19 cfg.database.port # only evaluates the path you walk 20 end 21 ``` 22 23 ## Basic Usage 24 25 ### Do-block (preferred) 26 27 The do-block automatically cleans up resources when the block exits: 28 29 ```julia 30 using NickelEval 31 32 result = nickel_open("{ name = \"myapp\", version = \"1.0\" }") do cfg 33 cfg.name # => "myapp" 34 end 35 ``` 36 37 ### Manual mode (REPL exploration) 38 39 For interactive exploration, you can manage the lifecycle yourself: 40 41 ```julia 42 cfg = nickel_open("{ x = 1, y = 2 }") 43 cfg.x # => 1 44 cfg.y # => 2 45 close(cfg) # free resources 46 ``` 47 48 ### File evaluation 49 50 Files ending in `.ncl` are detected automatically. Imports resolve relative to the file: 51 52 ```julia 53 nickel_open("config.ncl") do cfg 54 cfg.database.host # => "localhost" 55 end 56 ``` 57 58 ## Navigation 59 60 Access fields with dot syntax or brackets: 61 62 ```julia 63 nickel_open("{ db = { host = \"localhost\", port = 5432 } }") do cfg 64 cfg.db.host # dot syntax 65 cfg["db"]["host"] # bracket syntax — same result 66 67 cfg.db # returns a NickelValue (still lazy) 68 cfg.db.port # returns Int64(5432) (primitive, resolved) 69 end 70 ``` 71 72 Arrays use 1-based indexing: 73 74 ```julia 75 nickel_open("{ items = [10, 20, 30] }") do cfg 76 cfg.items[1] # => 10 77 cfg.items[3] # => 30 78 end 79 ``` 80 81 ## Inspecting Without Evaluating 82 83 Check the kind and size of a value without evaluating its contents: 84 85 ```julia 86 nickel_open("{ a = 1, b = 2, c = 3 }") do cfg 87 nickel_kind(cfg) # => :record 88 length(cfg) # => 3 89 keys(cfg) # => ["a", "b", "c"] 90 end 91 ``` 92 93 ## Materializing a Subtree 94 95 `collect` recursively evaluates an entire subtree, returning the same types as `nickel_eval`: 96 97 ```julia 98 nickel_open("{ db = { host = \"localhost\", port = 5432 } }") do cfg 99 collect(cfg.db) 100 # => Dict{String, Any}("host" => "localhost", "port" => 5432) 101 102 collect(cfg) 103 # => Dict{String, Any}("db" => Dict("host" => "localhost", "port" => 5432)) 104 end 105 ``` 106 107 ## Iteration 108 109 Iterate over records (yields `key => value` pairs) or arrays: 110 111 ```julia 112 nickel_open("{ a = 1, b = 2 }") do cfg 113 for (k, v) in cfg 114 println("$k = $v") 115 end 116 end 117 118 nickel_open("[10, 20, 30]") do cfg 119 for item in cfg 120 println(item) 121 end 122 end 123 ``` 124 125 ## Benchmark: Lazy vs Eager 126 127 The benefit of lazy evaluation depends on how expensive your fields are to compute. For configs with computationally intensive fields (array operations, complex merges, function calls), the difference is dramatic. 128 129 This benchmark generates configs where each field folds over a 1000-element array. Eager evaluation computes every field; lazy evaluation computes only the one you access: 130 131 ```julia 132 using NickelEval 133 134 function make_expensive_config(n) 135 fields = String[] 136 for i in 1:n 137 push!(fields, 138 "section_$i = std.array.fold_left " * 139 "(fun acc x => acc + x) 0 " * 140 "(std.array.generate (fun x => x + $i) 1000)") 141 end 142 "{ " * join(fields, ", ") * " }" 143 end 144 145 code = make_expensive_config(100) 146 147 # Eager: evaluates all 100 expensive fields (~470 ms) 148 @time result = nickel_eval(code) 149 result["section_50"] 150 151 # Lazy: evaluates only section_50 (~26 ms) 152 @time nickel_open(code) do cfg 153 cfg.section_50 154 end 155 ``` 156 157 Results on Apple M1 (averaged over 3 runs): 158 159 | Fields | Eager | Lazy | Speedup | 160 |--------|-------|------|---------| 161 | 10 | 51 ms | 13 ms | 4x | 162 | 50 | 242 ms | 18 ms | 13x | 163 | 100 | 473 ms | 26 ms | 18x | 164 | 200 | 940 ms | 44 ms | 21x | 165 166 Lazy evaluation time grows slowly (parsing overhead) while eager time scales linearly with the number of fields. For configs with simple static values (no computation), the difference is negligible since parsing dominates. 167 168 ## When to Use Lazy vs Eager 169 170 | Use case | Recommended | 171 |----------|------------| 172 | Small configs (< 50 fields) | `nickel_eval` — simpler, negligible overhead | 173 | Large configs, need all fields | `nickel_eval` — eager is fine if you need everything | 174 | Large configs, need a few fields | `nickel_open` — avoids evaluating unused fields | 175 | Interactive exploration | `nickel_open` (manual mode) — drill in on demand | 176 | Exporting to JSON/YAML/TOML | `nickel_to_json` etc. — these are always eager |