Fields and functions#

Meta: introduce the field concept first and let the examples carry it — the whole rule is just: let makes a field local, a type makes it public, and a bare name = value reassigns. Then the four "feels weird at first" function points: zero-arity has no parens; the body has no return; positional args can mix with named; &fn is for references. Don't mention pub (it's legacy and formatted away). Object-context behavior lives in Objects (type) — link, don't duplicate.

Fields#

A field is a named, typed thing — a value, a function, or a computed expression — declared in the current scope:

A field is recognized by its shape — a name followed by a type, an argument list, or a block body:

name: String!             # typed field
greet: String! { "hi!" }  # computed field / method
add(x: Int!): Int! { x }  # method with args
y: Int! = 100             # typed field with default
maybe: String = null      # nullable
let secret = "shhh"       # local field (untyped is fine)

Visibility#

A field is public when it declares a type, and local when it starts with let:

name: String!              # public field
greet: String! { "hi!" }   # public method
count: Int! = 0            # public field with a default
let secret = "shhh"        # local field

let introduces a local field. A type-level let is readable only inside that type's own methods and defaults; a block-level let is a fresh local; a top-level let is module-scoped — visible to every .dang file in the directory but not exported, which is how you share helpers across a module (see Modules and imports).

Declaration vs. reassignment#

A bare name = value reassigns an existing field — see Mutation and copy-on-write. To declare a new field instead, give it a type or introduce it with let:

total: Int! = 0   # declares a public field
let total = 0     # declares a local field
total = 5         # reassigns the existing field

Docstrings#

"""
Greets the named user.
"""
greet(name: String!): String! {
  `hi, ${name}`
}

— and on parameters:

greet(
  """name of the person to greet"""
  name: String!
): String! { "hey, ${name}!" }

Functions#

A function is a field with an argument list:

add(a: Int!, b: Int!): Int! { a + b }

Zero-arity and auto-calling#

motd: String! { "hello" }

Arguments#

greet(name: "Alice")   # named
greet("Alice")         # positional
add(10, b: 20)         # mixed

Defaults:

A non-null parameter with a default (name: String! = "world") is nullable on the caller's side but non-null on the receiver's side. Callers may omit it, pass null, or pass a nullable String; every such case falls back to the default. Inside the body the parameter is a plain String!, so no null checks or assertions are needed. This lets an API excise null at the boundary — prefer a non-null-with-default parameter over a nullable one whenever a sensible default (including a sentinel like "") exists, keeping both the caller (who can omit the argument) and the body (which never sees null) happy.

greet(name: String! = "world"): String! { "hi " + name }
greet                      # "hi world"  (omitted)
greet(null)                # "hi world"  (explicit null falls back)
greet(someNullableString)  # falls back to "world" when the value is null

Function references: &fn#

Nested functions#

Meta: link forward to Blocks — block arguments are the more common form of "pass code." Function refs are for the cases where you need a true callable to store or rebind.

Forward references#

tl;dr: they work.

.dang files within a directory share a common scope, like in Go