01. Basic data and immutable collections
Hara programs start with values. Most values are immutable. An operation returns a new value instead of changing the old value.
This lesson introduces scalar values, collection literals, lookup, and persistent updates.
Learning goals
Section titled “Learning goals”By the end of this lesson, you can:
- Read the main Hara literal forms.
- Distinguish a list form from a vector value.
- Use keywords to label data.
- Read values from vectors and maps.
- Update a persistent collection without mutation.
- Choose a collection from the job it performs.
Scalar values
Section titled “Scalar values”A scalar value is one value that is not a collection.
Nil and booleans
Section titled “Nil and booleans”Hara has one empty value and two boolean values:
niltruefalseOnly nil and false are falsey. Zero, an empty string, and an empty collection are truthy.
(if 0 :yes :no); => :yes
(if [] :yes :no); => :yesDo not use emptiness as a boolean test. Use an explicit predicate such as empty?.
Numbers
Section titled “Numbers”Hara supports integers and floating-point values:
42-73.5It also reads big integers and big decimals:
12345678901234567890N19.95MThe suffix is part of the numeric literal.
Ratios are not an L0 numeric type. Division does not create a ratio value.
(/ 5 2)Check the result in your current runtime before you build numeric rules around integer division.
Strings and characters
Section titled “Strings and characters”A string contains text:
"HAL""line one\nline two"A character is a single character value:
\a\newlineStrings are immutable. String transformation functions return new strings.
Keywords
Section titled “Keywords”A keyword is a stable label:
:name:task/title:status/readyKeywords often label fields in maps. A namespaced keyword makes the domain explicit.
{:task/title "Read input" :task/status :status/ready}Symbols
Section titled “Symbols”A symbol names a Var, local binding, function, namespace, or other program entity:
scoremapfile/readThe evaluator resolves an unquoted symbol. Quote returns the symbol itself:
'file/read; => file/readA keyword usually labels data. A symbol usually names program behavior or a binding.
Collection literals
Section titled “Collection literals”Hara has lists, vectors, maps, and sets.
A list uses parentheses:
(+ 19 23)In source code, the evaluator normally treats a list as a call or special form.
Quote a list when you want list data:
'(north east south west)Use lists for forms and list-shaped data. Do not use a list only because HAL uses parentheses for evaluation forms.
Vectors
Section titled “Vectors”A vector uses square brackets:
["alpha" "beta" "gamma"]A vector preserves order and supports indexed access:
(nth [10 20 30] 1); => 20Indexes start at zero.
Use a vector for ordered records, argument lists, coordinates, and finite sequences that need indexed access.
A map associates keys with values:
{:task/id 1 :task/title "Read input" :task/done false}Use get to read a key:
(get {:task/title "Read input"} :task/title); => "Read input"A keyword can also look itself up in a map:
(:task/title {:task/title "Read input"}); => "Read input"Use a map when fields have names.
A set contains unique values:
#{:read :transform :write}Adding an existing value does not create a duplicate:
(conj #{:read :write} :read); => #{:read :write}Use a set for membership, permissions, tags, and unique categories.
Persistent updates
Section titled “Persistent updates”A persistent collection does not change in place. An update returns another collection.
Add a vector item
Section titled “Add a vector item”(def steps [:read :transform])(def next-steps (conj steps :write))
steps; => [:read :transform]
next-steps; => [:read :transform :write]The old vector remains valid.
Replace a map field
Section titled “Replace a map field”(def task {:task/id 1 :task/title "Read input" :task/done false})
(def completed-task (assoc task :task/done true))assoc returns a map with the selected key associated with the new value.
Remove a map field
Section titled “Remove a map field”(dissoc task :task/title); => {:task/id 1 :task/done false}The original task value still contains the title.
Update nested data
Section titled “Update nested data”Use get-in and assoc-in for nested paths:
(def job {:job/id 7 :job/progress {:current 0 :total 3}})
(get-in job [:job/progress :total]); => 3
(assoc-in job [:job/progress :current] 1)Use update-in when the new value depends on the old value:
(update-in job [:job/progress :current] inc)Each operation returns a new root map.
Collection operations preserve the boundary
Section titled “Collection operations preserve the boundary”Persistent operations return persistent values:
(conj [1 2] 3)(assoc {:a 1} :b 2)(dissoc {:a 1 :b 2} :a)These operations do not silently create mutable arrays or objects.
Later, you will use array and object when mutation is intentional.
Build the course configuration
Section titled “Build the course configuration”Create one immutable configuration value:
(def config {:input/path "data/input.txt" :output/path "data/output.txt" :pipeline/steps [:trim :remove-empty :number] :pipeline/tags #{:text :utf8} :pipeline/options {:skip-empty true :max-lines 1000}})Read one value from each collection level:
(:input/path config)
(nth (:pipeline/steps config) 1)
(get-in config [:pipeline/options :max-lines])Create a changed configuration without changing config:
(def small-config (assoc-in config [:pipeline/options :max-lines] 10))Practice loop
Section titled “Practice loop”Use this sequence for each form:
- Predict the result.
- Run the form.
- Change one literal.
- Explain which value changed.
Try these changes:
- Add another pipeline step.
- Add a tag to the set.
- Change the maximum line count.
- Remove the output path.
- Confirm that the original value remains unchanged.
Common mistakes
Section titled “Common mistakes”Treating parentheses as a container
Section titled “Treating parentheses as a container”(1 2 3)The evaluator tries to call 1. Quote list data or use a vector.
Expecting assoc to mutate
Section titled “Expecting assoc to mutate”(assoc config :input/path "other.txt")configThe second form returns the original value because the first result was not stored or passed onward.
Using truthiness for emptiness
Section titled “Using truthiness for emptiness”(if [] :empty :not-empty); => :emptyThis label is misleading. Empty collections are truthy. Use empty?.
Mixing field names
Section titled “Mixing field names”Do not call the same field :path, :file, and :input in nearby examples. Choose one stable key such as :input/path.
Check yourself
Section titled “Check yourself”You are ready for the next lesson when you can answer these questions:
- Which Hara values are falsey?
- Why does a list often represent code?
- When should you use a vector instead of a map?
- What does
assocreturn? - Why can old and new persistent values coexist?
- When is a set a better fit than a vector?
- What is the difference between a keyword and a symbol?
Continue with 02. Functions and atoms.