Description
In zig, modules are 'just structs' but the namespace of structs is partitioned into fields and defs. Field values belong to the struct and are eagerly evaluated in order when the struct is instantiated at runtime. Def values belong to the struct type, are lazily evaluated at compile time and may be mutually recursive. (The comptime interpreter throws a compile error if it forces a cyclic dependency).
In 1ML modules really are 'just structs' which happen to have been evaluated at compile time. But they move quickly past recursion so it's not clear to me exactly how they handle this. My hunch is that let rec
blocks are evaluated eagerly (at compile-time for modules/types and at runtime for recursive closures).
Is let rec
style recursion an acceptable price to pay for unifying the namespaces? Can we recover laziness without dual namespaces? Would the lack of laziness break nice zig features (eg make platform-specific meta-programming onerous because we have to carefully avoid eagerly-evaluating anything that would not work on the target platform even if we don't use it) (eg not being able to run a test because some value that the test doesn't even depend on does not compile).
Additional restrictions that are important in zig:
- Comptime-evaluated code cannot cause side-effects
- Comptime-evaluated functions which return types may be memoized.
A basic 1ML -like solution would be a module
syntax which allows mutually recursive bindings, eagerly evalutes them and returns a struct.
odd_even = module {
even = (n) if (n == 0) 'true' else odd(n - 1)
odd = (n) if (n == 0) 'false' else even(n - 1)
}
odd_even.even(3)
=>
'false'
module [
a = b + 1
b = 1
]
=>
[a: 2, b: 1]
module [
a = b + 1
b = a + 1
]
=>
compile error
This allows for mutual recursion, but not for eg cyclic imports. How annoying is this?
We add a lazy
type and extend the module syntax to return lazy values.
module [
a = b + 1
b = a + 1
]
// equivalent to
[
a: lazy(() b + 1)
b: lazy(() a + 1)
]
I don't like this because it:
- Introduces reference cycles, which makes these values hard to print/serialize
- Introduces invisible side-effects when accessing these values for the first time
- Requires either that we have some cycle-detection mechanism at runtime, or that some language features are only available at compile-time (which in turn implies no multi-stage programming).