NickelEval.jl

Julia FFI bindings for Nickel configuration language
Log | Files | Refs | README | LICENSE

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 |