Lesson 01 — Anatomy of a fn-def
Goal: by the end of this lesson you can write a fn-def by hand and explain each part of it.
Concepts introduced: fn-def, :name, :parent, :args,
base function, fns.edn.
The smallest meaningful fn-def
{:name :hello-handler
:parent :const
:args {:value {:status 200 :body "Hello from Graphden!"}}}
That's a complete fn-def. Three keys:
:name— what you'll call this fn elsewhere. Names are unique per namespace (like vars in a Clojure ns): two namespaces may each define:get-user, and a bare reference resolves to your own namespace's fn first; referencing a same-named fn elsewhere uses the qualified form (:other.ns/get-user).:parent— the fn this one inherits from. Here:constis a base function (a built-in primitive) that returns its:valueargument unchanged when executed.:args— values that fill in the parent's slots.:constexposes one slot called:value; we bind it to a map.
Executing :hello-handler returns the map {:status 200 :body "Hello from Graphden!"}.
Where fn-defs live
Two places:
resources/packages/<pkg>/<module>/fns.edn— a vector of fn-def maps, loaded at startup. This is the source-of-truth form. See PACKAGES.md.- Inside the running editor — when you click
+in a namespace, the editor creates a fn entity directly viaPOST /api/entities/fn. Same shape, different entry point.
For this lesson assume you're typing into the editor. The
fns.edn form is what you'd write for code review.
Two kinds of fn
Look at the :parent field. There are two kinds of fn you can
parent to:
| Kind | What it is | Example | How to spot one |
|---|---|---|---|
| Base function | A small Clojure impl wrapping one library call | :const, :add, :render-hiccup, :pg-query |
Has a :return-type-fn-id, no :parent-ids |
| fn-def | A pure composition — no Clojure, just bindings | :hello-handler, :web-server, :editor-page |
Has at least one :parent-ids |
You can parent a new fn-def to either kind. Inheritance works the same way for both.
A fn-def that uses a fn-def
Now let's compose two:
{:name :hello-handler
:parent :const
:args {:value {:status 200 :body "Hello!"}}}
{:name :hello-route
:parent :assoc
:args {:map {} :key "handler" :value :hello-handler}}
:hello-route parents to :assoc — a base function that returns
{key value} merged into map. The interesting part: :value is
bound to :hello-handler (the keyword form of its name). When
:hello-route runs:
- The executor sees the ref
:hello-handlerin slot:value. - It looks at
:assoc's slot type for:value— not:fn-typed. - So it executes
:hello-handler(gets the response map) and uses that as the value of:value.
The same syntax in a :fn-typed slot would behave differently —
the fn-id would be passed unchanged for the parent to invoke. We'll
cover that in lesson 06 when we hit higher-order functions.
Try it
Prefer to be shown? This lesson exists as a guided in-editor tour: open the demo with the tour running (no sign-up), or pick “Interactive tutorial” in the editor's account menu.
In the running editor, in a namespace of your choice (or create
tutorial/01-fn-defs):
- Click
+to add a new fn. Name ithello-handler. - Set its parent to
:const. The editor will show one free arg:value. - Click
:valueand bind a literal map. The editor's value form takes JSON:{"status": 200, "body": "Hello!"}(infns.ednyou'd write the same map as EDN). When the slot's type is already known, the form renders one field per key instead — the{} rawbutton in its corner switches back to a raw editor that accepts any JSON, a bare number, or plain text. - Open the row's
⋯actions popover and click ▶ Run — you should see the map come back.
What we glossed over
- What "slot" actually is as a database entity — Lesson 03.
- What "inheritance" really means in the data layer — Lesson 02.
:fn-typed slots and higher-order functions — Lesson 06.- Why
:parentis singular here but the docs mention "multiple parents" (MI) — Lesson 02.
Each of these is built on the same fn-def shape; we just keep
peeling layers.
Next
Lesson 02 — Parents and inheritance (already written)