|
356 | 356 |
|
357 | 357 | ;; ### 📖 The Solution Works: Standard Merge Composes |
358 | 358 |
|
359 | | -;; Show this actually solves the problem: |
| 359 | +;; Standard `merge` preserves all properties - nothing lost: |
360 | 360 |
|
361 | 361 | (merge {:=x :bill-length-mm :=color :species} |
362 | 362 | {:=y :bill-depth-mm :=alpha 0.5}) |
363 | | -;; => {:=x :bill-length-mm |
364 | | -;; :=y :bill-depth-mm |
365 | | -;; :=color :species |
366 | | -;; :=alpha 0.5} |
367 | 363 |
|
368 | | -;; Nothing lost! All properties preserved. |
369 | 364 | ;; This is why `=*` (our composition operator) can use standard `merge` internally. |
370 | 365 |
|
371 | 366 | ;; # Design Overview |
@@ -3888,107 +3883,58 @@ iris |
3888 | 3883 |
|
3889 | 3884 | ;; # Design Discussion |
3890 | 3885 | ;; |
3891 | | -;; ## 📖 Plot-Level Properties in Layer Vectors |
| 3886 | +;; ## 📖 Map-Based IR: Separating Layers from Plot Configuration |
3892 | 3887 | ;; |
3893 | | -;; ### The Tension |
3894 | | -;; |
3895 | | -;; Our current design uses a vector of layer maps as the intermediate representation: |
3896 | | -;; |
3897 | | -;; ```clojure |
3898 | | -;; (-> penguins |
3899 | | -;; (mapping :x :y) |
3900 | | -;; (scatter) |
3901 | | -;; (target :vl) |
3902 | | -;; (size 800 600)) |
3903 | | -;; ;; => [{:=data ... :=x :x :=y :y :=plottype :scatter |
3904 | | -;; ;; :=target :vl :=width 800 :=height 600}] |
3905 | | -;; ``` |
3906 | | -;; |
3907 | | -;; Notice that `:=target`, `:=width`, and `:=height` appear in every layer, |
3908 | | -;; even though their meaning applies to **all layers together**, not individual layers. |
3909 | | -;; These are plot-level properties, not layer-level properties. |
3910 | | -;; |
3911 | | -;; This creates a conceptual impurity: properties about "how to render the whole plot" |
3912 | | -;; are stored alongside properties about "what data to show and how to transform it." |
3913 | | -;; |
3914 | | -;; ### Why This Happens |
3915 | | -;; |
3916 | | -;; It's a consequence of our choice to use **vectors of maps** as the IR: |
3917 | | -;; |
3918 | | -;; - The `*` operator merges maps: `(merge layer-a layer-b)` |
3919 | | -;; - When we do `(=* layers (target :vl))`, the target gets merged into all layers |
3920 | | -;; - Same for `(=* layers (size 800 600))` - dimensions merge into every layer |
3921 | | -;; |
3922 | | -;; The current workaround: `plot-impl` extracts the first occurrence: |
3923 | | -;; ```clojure |
3924 | | -;; (some :=target layers-vec) ;; Get first non-nil target |
3925 | | -;; (some :=width layers-vec) ;; Get first non-nil width |
3926 | | -;; ``` |
3927 | | -;; |
3928 | | -;; ### Alternative Approaches Considered |
3929 | | -;; |
3930 | | -;; **1. Metadata on the Vector** |
3931 | | -;; |
3932 | | -;; ```clojure |
3933 | | -;; (defn target [target-kw] |
3934 | | -;; (with-meta [] {:=target target-kw})) |
3935 | | -;; |
3936 | | -;; ;; Result: |
3937 | | -;; ^{:=target :vl :=width 800 :=height 600} |
3938 | | -;; [{:=data ... :=x :x :=plottype :scatter}] |
3939 | | -;; ``` |
3940 | | -;; |
3941 | | -;; **Pros**: Conceptually clean separation - layers are grammar, metadata is rendering config |
3942 | | -;; |
3943 | | -;; **Cons**: Metadata easily lost, less inspectable, threading needs special handling |
3944 | | -;; |
3945 | | -;; **2. Wrapper Map** |
3946 | | -;; |
3947 | | -;; ```clojure |
3948 | | -;; {:layers [{:=data ... :=x :x} |
3949 | | -;; {:=transformation :linear}] |
3950 | | -;; :config {:target :vl :width 800 :height 600}} |
3951 | | -;; ``` |
3952 | | -;; |
3953 | | -;; **Pros**: Explicit separation, no duplication, easy to inspect |
3954 | | -;; |
3955 | | -;; **Cons**: Breaks the algebra (`*` and `+` work on vectors), operators become complex |
3956 | | -;; |
3957 | | -;; **3. Special Marker Layer** |
| 3888 | +;; ### The Design Choice |
3958 | 3889 | ;; |
| 3890 | +;; The internal representation uses a map structure with explicit separation: |
| 3891 | + |
| 3892 | +(-> penguins |
| 3893 | + (mapping :bill-length-mm :bill-depth-mm) |
| 3894 | + (scatter) |
| 3895 | + (target :vl) |
| 3896 | + (size 800 600)) |
| 3897 | + |
| 3898 | +;; This produces a map with `:=layers` and plot-level properties separated: |
| 3899 | + |
3959 | 3900 | ;; ```clojure |
3960 | | -;; [{:=data ... :=x :x :=plottype :scatter} |
3961 | | -;; {:=plot-config true |
3962 | | -;; :=target :vl :=width 800 :=height 600}] |
| 3901 | +;; {:=layers [{:=data penguins |
| 3902 | +;; :=x :bill-length-mm |
| 3903 | +;; :=y :bill-depth-mm |
| 3904 | +;; :=plottype :scatter}] |
| 3905 | +;; :=target :vl |
| 3906 | +;; :=width 800 |
| 3907 | +;; :=height 600} |
3963 | 3908 | ;; ``` |
3964 | 3909 | ;; |
3965 | | -;; **Pros**: Keeps vector structure, filterable |
3966 | | -;; |
3967 | | -;; **Cons**: Still mixed concerns, just specially marked |
3968 | | -;; |
3969 | | -;; **4. Accept the Duplication (Current)** |
| 3910 | +;; ### Why This Structure? |
3970 | 3911 | ;; |
3971 | | -;; Keep plot-level properties duplicated in every layer. |
| 3912 | +;; **Clean separation of concerns:** |
| 3913 | +;; - **Layer properties** (`:=data`, `:=x`, `:=y`, `:=plottype`) describe what to visualize |
| 3914 | +;; - **Plot properties** (`:=target`, `:=width`, `:=height`, scales) describe how to render |
3972 | 3915 | ;; |
3973 | | -;; **Pros**: Simple, works with current algebra, extraction via `some` is straightforward |
3974 | | -;; |
3975 | | -;; **Cons**: Conceptually impure, wasteful (though negligible), could confuse on inspection |
| 3916 | +;; **Benefits:** |
| 3917 | +;; 1. **Inspectable** - `kind/pprint` shows clear structure |
| 3918 | +;; 2. **No duplication** - Plot config appears once, not in every layer |
| 3919 | +;; 3. **Composable** - `=*` and `=+` can merge both levels correctly |
| 3920 | +;; 4. **Extensible** - Easy to add new plot-level properties (themes, titles, etc.) |
3976 | 3921 | ;; |
3977 | | -;; ### Current Decision |
| 3922 | +;; ### How Operators Handle It |
3978 | 3923 | ;; |
3979 | | -;; We've chosen **Alternative 4** (accept duplication) for now because: |
| 3924 | +;; **`=*` (merge):** |
| 3925 | +;; - Performs cross-product on `:=layers` vectors |
| 3926 | +;; - Merges plot-level properties (right side wins) |
3980 | 3927 | ;; |
3981 | | -;; 1. **Simplicity**: No special cases in `*` and `+` operators |
3982 | | -;; 2. **Works**: The `some` extraction pattern is fast and reliable |
3983 | | -;; 3. **Practical**: The duplication overhead is negligible in practice |
3984 | | -;; 4. **Revisable**: If this becomes problematic, we can migrate to metadata later |
| 3928 | +;; **`=+` (overlay):** |
| 3929 | +;; - Concatenates `:=layers` vectors |
| 3930 | +;; - Merges plot-level properties (right side wins) |
3985 | 3931 | ;; |
3986 | | -;; The key insight: this is a **limitation of using vectors of maps as IR**, not a |
3987 | | -;; fundamental flaw. If plot-level configuration grows significantly, the metadata |
3988 | | -;; approach (Alternative 1) would be the natural evolution, as it mirrors Clojure's |
3989 | | -;; philosophy of metadata for "information about the thing" vs "the thing itself." |
| 3932 | +;; **Constructors:** |
| 3933 | +;; - Layer constructors (`data`, `mapping`, `scatter`) return `{:=layers [...]}` |
| 3934 | +;; - Plot constructors (`target`, `size`, `scale`) return `{:=property value}` |
| 3935 | +;; - Both compose naturally via `=*` |
3990 | 3936 | ;; |
3991 | | -;; For now, the simplicity trade-off is worth it. |
| 3937 | +;; This design balances compositional elegance with practical clarity. |
3992 | 3938 |
|
3993 | 3939 | ;; # Summary |
3994 | 3940 | ;; |
|
0 commit comments