Skip to content

First Contact

This is a short working session, not a survey of the whole language. On the main Hara site, the six examples share one named lesson session, so definitions remain available as you move down the page. Each step is also self-contained and can be rerun safely.

Use the same rhythm throughout:

  1. Predict the result.
  2. Run the form.
  3. Change one value.
  4. Explain the new result in one sentence.

Most Hara expressions use one visible shape: the operation comes first, followed by its inputs.

(+ 19 23)

Change either number and run it again. Nothing else is required to begin.

What to notice: the source form and the returned value stay close together.

Vectors keep order. Maps give facts names. Keywords make those names stable and readable.

(let [player {:name "Nova"
              :score 10
              :items ["lamp" "cable"]}]
  {:name (:name player)
   :first-item (nth (:items player) 0)})

Replace the name, add another item, or retrieve a different field.

What to notice: the same value can be read directly, inspected, tested, or passed to another function.

A function receives values and returns a value. It does not need to know where those values came from or where the result will go.

(do
  (defn add-score [player amount]
    (update player :score + amount))

  (add-score {:name "Nova" :score 10} 25))

Change the amount, then add another field to the player map. The function still has one clear job.

What to notice: a useful name turns a general operation into a domain rule.

Persistent data lets an old value and a changed value coexist.

(let [original {:name "Nova" :score 10}
      changed (assoc original :score 35)]
  {:original original
   :changed changed})

Change another field and inspect both maps again.

What to notice: history, comparison, testing, rollback, and explanation all become easier when updates return values rather than silently rewriting them.

count works with several kinds of value because those values participate in a shared contract.

{:vector (count [10 20 30])
 :map (count {:left 10 :right 20})
 :set (count #{:read :build :test})}

The call does not need a different spelling for every collection type.

What to notice: the important idea is not the concrete type. It is the ability the value provides.

ICount is Hara’s countability protocol. Its canonical identity lives at std.protocol.icount/ICount. A new value can implement that contract and work with the ordinary count function.

(do
  (defstruct Inventory [items])

  (extend-type Inventory ICount
    (count [inventory]
      (count (field inventory :items))))

  (count (Inventory ["lamp" "cable" "radio"])))

Add or remove an item. Then rename Inventory; the calling convention does not change.

What to notice: existing code gains a new implementation without being rewritten.

You have already used Hara’s central progression:

value → transformation → persistent result → shared protocol

Hara is not simple because it can do less. It stays simple because a small vocabulary can be extended.

Continue to Protocols for Builders →{ .md-button .md-button—primary } Build the first game →{ .md-button }