Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,27 @@ jobs:
environment: deploy

steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0

- uses: DeLaGuardo/setup-clojure@13.4
- uses: actions/setup-java@v4
with:
cli: '1.12.3.1577'
distribution: 'temurin'
java-version: '21'

- uses: actions/cache@v4.3.0
- uses: DeLaGuardo/setup-clojure@13.5
with:
cli: '1.12.4.1582'

- uses: actions/cache@v5.0.2
with:
path: ~/.m2
key: default-build

- run: clojure -T:build ci

- uses: codecov/codecov-action@v5.5.1
- uses: codecov/codecov-action@v5.5.2

- name: deploy
if: github.event.release
Expand Down
3 changes: 2 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ Some of the most frequently used APIs are:
<2> by `assoc` key-value pairs to a fun-map, you turn Datomic API into a map.

== Further read
- xref:doc/rational.adoc[Rational]
- xref:doc/rationale.adoc[Rationale]
- xref:doc/change_log.adoc[Changes]
- xref:doc/concepts.adoc[Concepts]
- xref:doc/improvements.adoc[Planned Improvements]

== Development

Expand Down
75 changes: 75 additions & 0 deletions WARP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# WARP.md

This file provides guidance to WARP (warp.dev) when working with code in this repository.

## Project Overview

fun-map is a Clojure/ClojureScript library that provides a map data structure where values are automatically unwrapped when accessed. It supports lazy evaluation, dependency injection between map values, and lifecycle management.

## Build & Test Commands

```bash
# Run tests in watch mode (auto-reload on changes)
clj -M:dev:test --watch

# Run all tests once (CLJ + CLJS)
clj -T:build tests

# Run Clojure tests only
clj -M:dev:test

# Run ClojureScript tests only
clj -M:dev:cljs-test

# Build JAR
clj -T:build ci

# Deploy to Clojars
clj -T:build deploy

# Copy clj-kondo config to local dev
clj -T:build copy-clj-kondo-config
```

## Architecture

### Core Concepts

1. **Value Wrappers** (`wrapper.cljc`): Protocol `ValueWrapper` defines how values are unwrapped. Implementations include:
- `FunctionWrapper` - wraps a function that receives the map and key
- `CachedWrapper` - caches results with optional focus function for invalidation
- `TracedWrapper` - adds tracing/logging capability

2. **DelegatedMap** (`core.cljc`): The main map implementation that:
- Delegates to an underlying map
- Applies `fn-entry` transformation on access to unwrap values
- Implements full Clojure map interfaces for both CLJ and CLJS

3. **Macros** (`fun_map.cljc`):
- `fw` - Creates function wrappers with full destructuring and options (`:focus`, `:trace`, `:par?`, `:wrappers`)
- `fnk` - Shortcut for `fw` that auto-focuses on declared keys

### Key Files

- `src/robertluo/fun_map.cljc` - Public API: `fun-map`, `fw`, `fnk`, `life-cycle-map`, `closeable`, `halt!`
- `src/robertluo/fun_map/core.cljc` - `DelegatedMap` type with CLJ/CLJS implementations
- `src/robertluo/fun_map/wrapper.cljc` - `ValueWrapper` protocol and wrapper types
- `src/robertluo/fun_map/helper.cljc` - Macro helpers for `fw`/`fnk` expansion

### Testing

Tests use kaocha with cloverage. Test files mirror source structure in `test/`.

## Known Issues & Improvements

See `doc/improvements.adoc` for a tracked list of potential improvements.

### Key Gotchas

1. **~~Missing keys produce cryptic errors~~** - FIXED: now throws helpful `ex-info` with context
2. **~~Circular deps cause StackOverflowError~~** - FIXED: now detects cycles and reports the path
3. **Plain maps inside fun-maps don't unwrap** - use nested fun-maps if you need nested unwrapping
4. **Iteration realizes all values** - `keys`, `vals`, `seq` trigger all computations
5. **`select-keys`/`into` return plain maps** - wrappers are lost
6. **`:keep-ref true` + `fnk`** - `fnk` receives atoms, not values; use `fw` with explicit `@` in focus
7. **`update` on computed key** - replaces `fnk` with the updated value
6 changes: 3 additions & 3 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{:paths ["src"]
:aliases {:dev ;for development
{:extra-paths ["test"]
:extra-deps {manifold/manifold {:mvn/version "0.4.3"}
org.clojure/clojurescript {:mvn/version "1.11.121"}}}
:extra-deps {manifold/manifold {:mvn/version "0.5.0"}
org.clojure/clojurescript {:mvn/version "1.12.134"}}}
:test ;run tests under console. e.g. clj -M:dev:test
{:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}
lambdaisland/kaocha-cloverage {:mvn/version "1.1.89"}}
Expand All @@ -14,4 +14,4 @@
:build ;customized building process running. e.g. clj -T:build ci
{:deps {io.github.robertluo/build-clj {:git/sha "5d45f58cc20747c136bb320c9b13d65d2bf4cf58"}}
:ns-default build}
:clj-kondo {:extra-deps {clj-kondo/clj-kondo {:mvn/version "2025.09.22"}}}}}
:clj-kondo {:extra-deps {clj-kondo/clj-kondo {:mvn/version "2026.01.12"}}}}}
105 changes: 105 additions & 0 deletions doc/concepts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,108 @@ With manifold's excellent `let-flow` macro and its `future` function, if you hav
(time (:c m)) ;=> 600 in approx. 3000msec instead of 6000
----

## Gotchas and Common Pitfalls

This section covers behaviors that may surprise new users.

### Plain maps inside fun-maps don't unwrap

Only fun-maps unwrap their values. If you nest a plain map inside a fun-map, values in that plain map are not unwrapped:

[source,clojure]
----
;; Plain map nested inside fun-map - delays NOT unwrapped
(def m (fun-map {:a {:b (delay 42)}})) ; {:b ...} is a plain map
(:b (:a m)) ;=> #object[clojure.lang.Delay ...]

;; Nested fun-map - delays ARE unwrapped
(def m (fun-map {:a (fun-map {:b (delay 42)})})) ; inner is also a fun-map
(:b (:a m)) ;=> 42
(get-in m [:a :b]) ;=> 42 ; get-in works too
----

### Iteration realizes all values

Iterating a fun-map (`keys`, `vals`, `seq`, `reduce-kv`, etc.) triggers computation of all values:

[source,clojure]
----
(def m (fun-map {:a 1 :b (fnk [a] (println "computing b") (* a 2))}))
(keys m) ; prints "computing b" even though you only asked for keys!
----

This is because iteration must produce key-value pairs, which requires unwrapping.

### `select-keys` and `into` return plain maps

Standard Clojure functions return plain maps, losing fun-map semantics:

[source,clojure]
----
(def m (fun-map {:a 1 :b (fnk [a] (* a 2))}))
(type (select-keys m [:a :b])) ;=> clojure.lang.PersistentArrayMap
(type (into {} m)) ;=> clojure.lang.PersistentArrayMap
----

Values are realized during iteration, so the result contains computed values, not wrappers.

### `update` on computed keys replaces the fnk

Using `update` on a key with an `fnk` value realizes the value and replaces the wrapper:

[source,clojure]
----
(def m (fun-map {:a 1 :b (fnk [a] (* a 2))}))
(def m2 (update m :b inc))
(:b m2) ;=> 3 (not a function of :a anymore!)

;; The fnk was replaced with the literal value 3
(def m3 (assoc m2 :a 100))
(:b m3) ;=> 3 (unchanged, no longer depends on :a)
----

### `dissoc` silently breaks dependencies

Removing a key that other `fnk`s depend on doesn't fail until access:

[source,clojure]
----
(def m (fun-map {:a 1 :b (fnk [a] (* a 2))}))
(def m2 (dissoc m :a))
m2 ;=> looks fine: {:b <<unrealized>>}
(:b m2) ;=> Error! :a is missing
----

### `:keep-ref` + `fnk` requires careful handling

With `:keep-ref true`, `fnk` receives the ref itself, not its value:

[source,clojure]
----
(def state (atom [1 2 3]))
(def m (fun-map {:nums state
:count (fnk [nums] (count nums))} ; nums is the atom!
:keep-ref true))
(:count m) ;=> Error: count not supported on Atom

;; Correct: use fw with explicit deref
(def m (fun-map {:nums state
:count (fw {:keys [nums] :focus @nums}
(count @nums))}
:keep-ref true))
----

### Printing shows wrapper state

Printing a fun-map shows raw wrapper state, not computed values:

[source,clojure]
----
(def m (fun-map {:a 1 :b (fnk [a] (* a 2))}))
(println m) ;=> {:a 1, :b <<unrealized>>}
(:b m) ;=> 2
(println m) ;=> {:a 1, :b <<2>>}
----

The `<<...>>` notation indicates wrapper values. `<<unrealized>>` means not yet computed.

Loading