Fields: pub and let
Meta: this is also the right place to introduce the field concept, since pub and let declare fields whether they hold a value or a function. The object-context behavior is covered in Objects (type) — link there, don't duplicate.
The pub and let keywords declare fields in the current scope:
- Top-level fields, declared across
.dangfiles in the same directory - Type-level fields declared within an Objects (
type) or Interfaces and unions - Block-level fields declared within the nearest enclosing
{and}
These keywords distinguish the expression from Mutation and copy-on-write, which updates an already-declared field.
Two visibilities
pub name = value— exported; visible to importers and outside the typelet name = value— unexported. A top-levelletis module-scoped: since a directory is one module, it is visible from every.dangfile in that directory (just not exported). A type-levelletis readable only inside that type's own methods/defaults. (So a top-levelletin its own file is the way to share private helpers across a directory module — see Modules and imports.)
What a field is
- a field is a named, typed thing — value, function, or computed expression
- fields can carry an explicit type annotation
- fields without a value (
pub name: Type) act as required constructor parameters (in objects) or unresolved declarations - a
let(private) required field with no default still becomes a required constructor parameter; aletfield with a default does not - private fields with defaults are preferred over outer-scope bindings of the same name inside the type
Forms
pub x = 42 # inferred Int!
pub y: Int! = 100 # explicit type
pub maybe: String = null # nullable
let secret = "shhh"
Forward references
tl;dr: they work.
.dang files within a directory share a common scope, like in Go
- field declarations may forward-reference fields later in the same file
- field declarations may cross-reference fields in sibling files
- types may forward-reference types declared later in the file
- forward reads hidden behind function calls / computed defaults resolve via lazy module slots
- a direct initializer cycle (
pub a = b,pub b = a) is rejected statically:circular module variable initializer: a -> b -> a - a cycle hidden behind an auto-called function or constructor default is caught at runtime when the variable is forced:
initialization cycle while evaluating variable "..."
Docstrings
- a
"""..."""literal immediately before a declaration attaches as documentation - works on modules, types, fields, functions, function parameters, directives, directive args
"""
Greets the named user.
"""
pub greet(name: String!): String! {
`hi, ${name}`
}
Reassignment
name = newValuemutates an existing field (or local/arg of that name)+=for compound update (Int add, String/List concatenation)- type must remain assignable to the field's declared type
- assigning a function-valued field a bare function name calls it; use
&nameto assign the function itself — see Functions - nested-path mutation is copy-on-write: copying an object then writing
m.a.b.c = 2leaves the original unchanged - inside a
type, barename = ...resolves to the field when nothing shadows it; if a parameter (or local) shadows the field name, field mutation requiresself.name = ...— see Mutation and copy-on-write