Objects (type)
Meta: lead with "a type declares both a type and its prototype constructor." That's the unusual thing. Cross-link to Mutation and copy-on-write — don't try to explain CoW here.
Declaration
type Person {
pub name: String!
pub age: Int! = 0
pub greet: String! {
"hi, I'm " + name
}
}
- declares a type
Personand a constructor functionPerson - members are fields or methods, indistinguishable in syntax (
pub/let+name+ optional: Type,= default, or{ body })
Public vs. private members
pub— readable from outside the type; default visibility for atypelet— readable only inside the type's own methods/defaults (private)- whether a member is a constructor parameter depends on having NO default, NOT on visibility:
pub x: T!(no default) → required positional parampub x: T! = d/pub x = d→ optional param (defaultd)let x: T!(no default) → required positional param too, e.g.Foo("public_value", "private_value")let x: T! = d/let x = d→ NOT a param; the default is used- method/computed members (have a
{ body }) are never constructor params
Implicit constructor
- one positional parameter per non-default field, in declaration order (NOT required-first-then-defaults — a defaulted field may precede a required one; positional args still line up by declaration order)
- e.g.
Mixed { pub publicWithDefault = "default"; let privateRequired: Int! }constructs asMixed("default", 42)
- e.g.
- can also be called with named arguments (
Counter(value: 42)) - field defaults are evaluated with
selfbound, so a default may reference earlier/sibling fields, e.g.combined = prefix + "_" + suffix
Person("Alice", age: 30)
Person(name: "Alice")
Zero-arg auto-construction
- a type whose constructor needs nothing (all fields have defaults / no required params) constructs on bare reference:
let p = Person≡let p = Person() - exception: a constructor that requires a block argument is NOT auto-called by a bare reference (
pub loop: Loop! = Loopis an error)
Explicit constructor: new
type Greeter {
pub greeting: String!
new(name: String!) {
self.greeting = "hello, " + name
self
}
}
- declared as
new(args) { body }ornew { body }(no parens when no args) - no
puband no return-type annotation — both are errors ("'new' is a constructor, not a method") newis only valid inside atypebody- overrides the implicit constructor (the type's fields no longer auto-become params;
new's arg list defines the signature) - constructor args are local bindings — distinct from fields, even when same-named:
- mutable:
foo = foo + 10/foo += foorebinds the arg, does NOT touchself.foo - shadow same-named fields and outer scope; bare name = arg, field write needs
self.field = - NOT visible in method bodies — only in
new
- mutable:
- must return the constructed type (
self, or a method chain that returns it): body's last expression must beFoo!- returning another type errors:
new() must return Wrong!, got String! - returning
nullerrors
- returning another type errors:
- may end by chaining other methods, which propagate their forked
self, e.g.self.withSuffix("!") - self-field mutation inside a loop accumulates into one fork
- can accept block args:
new(&condition: Boolean!) { ... }; a block param uses&name
self
- bound during constructor and method execution (and during field-default / computed-field evaluation)
- bare names inside a method resolve against the current receiver first: bare
namereadsself.name, bareincrBy(1)callsself.incrBy(1) - field reads never need
self.; for assignment, both forms work:- bare
a += 1/value = von a field forksselfand sets the field self.field = ...is the explicit form; required only to disambiguate from a same-named local/arg- bare assignment binds a local/arg when the name is one in scope (shadowing); otherwise targets the field
- bare
- assigning a field forks the receiver (see Mutation and copy-on-write)
selfis the value returned by chainable methods (the finalselfreturns the accumulated fork)
Computed fields
- a member with a type and a body but no arg list is a computed field — a zero-arg function evaluated on
selfeach access:
pub fullName: String! { firstName + " " + lastName }
- accessed like a plain field (
obj.fullName, no call parens); recomputes against the current receiver - a defaulted-value member (
pub computedField = config.name + "_computed") is computed once at construction; a{ body }computed field is re-evaluated per access
Implements
type Person implements Named & Identifiable { ... }
Forward references
- methods can reference types and fields defined later in the same file
- references also resolve across files in a directory module, order-independently
- defaults can reference sibling fields (evaluated with
selfbound)
Meta: user-defined types and imported GraphQL input types share the same Type(args) construction syntax. Schema input types are constructed exactly like local types: CreateUserInput(name: "Alice", email: "...") passed as Mutation.createUser(input: ...). See GraphQL interop.