dang
dang#
NOTE FROM A HUMAN: this is an AI-assisted draft, for now just establishing the concepts, framing, and facts. Everything here is correct and verifiable, and I do like the brevity, but there are probably better ways to explain things. I'll be improving them gradually and this notice will go away when it's in a state I'm proud of. Sorry for any nonsense. Every paragraph has a 'feedback' button so you can yell at me about it anonymously.
A statically typed scripting language for GraphQL, where the types and functions are loaded directly from the schema.<br>GitHub<br>pkg.go.dev
$ go install github.com/vito/dang/v2/cmd/dang@latest<br>Hello, world!
type Greeter {<br>name: String!<br>greet: String! { `Hello, ${name}!` }
["world", "Dang", "you"].map { who => Greeter(who).greet }<br>=> ["Hello, world!", "Hello, Dang!", "Hello, you!"]
Schema-native types
import Demo # configured in dang.toml below
# imports become globals<br>let u = user("1") # or Demo.user("1") to be explicit
# fields that return scalars query on access<br>print(`${u.name} is ${u.age} (${u.status})`)
# use sub-selections to avoid spamming queries<br>users.{{ name, age, status }}.each {<br>print(`${_.name} (${_.age}): ${_.status}`)<br>→ query Query {user(id:"1"){name}}<br>→ query Query {user(id:"1"){age}}<br>→ query Query {user(id:"1"){status}}<br>John Doe is 30 (ACTIVE)<br>→ query Query {users{name age status}}<br>John Doe (30): ACTIVE<br>Jane Smith (25): PENDING<br>=> [module {age: Int, name: String!, status: Demo.Status!}, module {age: Int, name: String!, status: Demo.Status!}]
Demo is a small schema bundled into this page and resolved in-process.<br>Normally it would be defined with a dang.toml like this:<br>[imports.Demo]<br>schema = "./tests/gqlserver/schema.graphqls"<br>service = ["go", "run", "./tests/gqlserver/service"]
Parallel selection
import Demo
# .{{ }} is parallel selection
# for GraphQL, it queries for all fields at once<br>Demo.posts.{{ title, author.{{ name }} }}.each {<br>print(`${_.title} — ${_.author.name}`)
# for native values, it parallelizes across lists and fields<br>type City {<br>name: String!<br>code: String! {<br># click Run again and the log order might* change<br>print(`${Demo.hello(name)}`)<br>name.toUpper<br>[City("portland"), City("austin")].{{ name, code }}.each {<br>print(`${_.name} → ${_.code}`)
# * Turns out WASM is deterministic, but it might change<br># when switching from server-rendered to client-rendered.<br>→ query Query {posts{title author{name}}}<br>First Post — John Doe<br>Second Post — Jane Smith<br>Third Post — John Doe<br>Fourth Post — John Doe<br>Fifth Post — Jane Smith<br>Sixth Post — John Doe<br>→ query Query {hello(name:"austin")}<br>Hello, austin!<br>→ query Query {hello(name:"portland")}<br>Hello, portland!<br>portland → PORTLAND<br>austin → AUSTIN<br>=> [module {code: String!, name: String!}, module {code: String!, name: String!}]
import GitHub
import GitHub
# the same idea against a real schema: `viewer` is GitHub's<br># authenticated user, and this is one query<br>viewer.{{<br>login<br>name<br>repositories(first: 3).{{ nodes.{{ name, stargazerCount }} }}<br>}}
GitHub's GraphQL API explorer was sadly removed -- so here's something kind of close.<br>To try it, sign in with GitHub and hit Run .<br>NOTE: this will ask for read-only access (read:user). The token only ever exists client-side and expires with the tab.
In a project you'd wire it up in dang.toml:<br>[imports.GitHub]<br>endpoint = "https://api.github.com/graphql"<br>authorization = "Bearer ${GITHUB_TOKEN}"<br>This .envrc might help too:<br>export GITHUB_TOKEN="$(gh auth token)"
Copy-on-write
## shared state changes are copy-on-write
type Counter {<br>n: Int!<br>bump: Counter! {<br>n += 1 # this LOOKS like it mutates, but it doesn't!<br>self # this `self` is actually a clone with n += 1 applied
let c = Counter(0)<br>assert("changes accumulate") { c.bump.bump.n == 2 }<br>assert("original unmodified") { c.n == 0 }
# as a result of this truce between mutable and immutable,<br># we gain trivial syntax for deep structural updates<br>type Tree {<br>node: Node!<br>bump: Tree! {<br># under the hood this is something like:<br># self = clone(self)<br># self.node = clone(self.node)<br># self.node.leaf = clone(self.node.leaf)<br># self.node.leaf.c = self.node.leaf.c + 100<br>self.node.leaf.c += 100<br>self<br>type Node { leaf: Leaf! }<br>type Leaf { c: Int! }<br>let t = Tree(Node(Leaf(42)))<br>[t.bump.bump.node.leaf.c, t.node.leaf.c]<br>=> [242, 42]
See Mutation and copy-on-write for more details.
Block arguments
## &block args are Dang's closures
"""<br>`if` but implemented with blocks.<br>"""<br>when(condition: Boolean!, &body: a): a {<br>if (condition) {<br># body is a zero-arity function, so it gets auto-called<br># like any other field<br>body<br># use &body to grab the function without calling it.<br># a bit like keeping the pin in the grenade.
when(false) { raise "i died" }<br>when(true) { "i lived" }<br>=> "i lived"
See Blocks for more details.
HTML DSL
## a DSL for generating HTML
"""<br>Anything that can be rendered to a string.<br>"""<br>interface Content {<br>render: String!
"""<br>An HTML element.<br>"""<br>type Element implements Content {<br>tag: String!<br>attributes: Map[String!]!...