Skip to content
This repository has been archived by the owner on Dec 8, 2024. It is now read-only.

Commit

Permalink
dustingetz domain: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dustingetz committed Jan 26, 2024
1 parent d2662aa commit 989c05c
Show file tree
Hide file tree
Showing 12 changed files with 447 additions and 0 deletions.
4 changes: 4 additions & 0 deletions deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,8 @@
{:extra-deps {com.pitch/uix.core {:mvn/version "1.0.1"}
com.pitch/uix.dom {:mvn/version "1.0.1"}}}

:dustingetz
{:extra-deps
{com.datomic/peer {:mvn/version "1.0.7075"}
com.hyperfiddle/hfql {:local/root "vendor/hfql"}}}
}}
59 changes: 59 additions & 0 deletions src/dustingetz/electric_y_combinator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Electric Y Combinator β€” Electric Clojure

by Dustin Getz, 2023 July 24

[Electric Clojure](https://github.com/hyperfiddle/electric/) is a new way to write reactive web apps in Clojure/Script which advertises **strong composition across the frontend/backend boundary**, i.e. network transparent composition (see [wikipedia: Network transparency](https://en.wikipedia.org/wiki/Network_transparency)).

As a proof of strong composition, and a demonstration of how this is different from weaker forms of composition like React Server Components, we offer **distributed Y-combinator** and a demo of using it to recursively walk a server filesystem hierarchy and render a browser HTML frontend, in one pass.

If this is your first time seeing Electric, start with the [live tutorial](https://electric.hyperfiddle.net/).

### Figure 1: Fibonacci with Electric Y Combinator

!fiddle-ns[](dustingetz.y-fib/Y-fib)

What's happening
- Recursive fibonacci
- The recursion is traced to the dom by side effect
- There is **no self-recursion**: we use `Y` to inject recursion into `Fib` via higher order fn `Recur` rather than `Fib` calling itself directly by its name.
- As a reminder, Electric functions are called with `new`
- there is lexical closure, e.g. `Gen` and `F` inside `Y` definition

Key ideas
- The **Y Combinator** (wikipedia: [Fixed-point combinator](https://en.wikipedia.org/wiki/Fixed-point_combinator)) captures the essence of recursive iteration in a simple lambda expression. ChatGPT can tell you more about this.
- Y works in Electric Clojure, demonstrating that Electric lambda *is* lambda.
- Note: you don't actually need `Y` for recursion, this is just a demo.

Ok, lets try something harder:

### Figure 2: Recursive walk over server file system, streamed to DOM

!fiddle-ns[](dustingetz.y-dir/Y-dir)

What's happening
- Recursive file system traversal over "./src" directory on the server
- there's a DOM text input that filters the tree, try typing "electric", both client and server refresh live
- **direct frontend/backend composition**: the tree walker, `Dir-tree`, interweaves `e/client` and `e/server` forms arbitrarily. e.g. L26 composes `dom/li` with `file-get-name`, the filename streams across the boundary in mid-flight. The network topology is complex, and irrelevant – Electric takes care of it.
- **automatic incremental/streaming network** (i.e. **streaming lexical scope**), not request/response. No dataloaders.
- **reactive recursion**: the DOM resources are reused across reaction frames, they are not recreated unnecessarily.
- I.e., when we type into the input, the DOM change is minimized
- this minimization is not implemented in electric-dom but rather emergent from the *evaluation model of the language*
- the actual "reactive stack frames" are reused
- the DOM resource lifecycle (mount/unmount) is simply bound to the lifecycle of the reactive frame they exist in!

Key ideas
- **network-transparent composition**: the Electric functions transmit data over the network (as implied by the AST) in a way which is invisible to the application programmer
- **strong composition**: it's actual lambda, which means it scales with complexity. Higher order functions, closures, recursion, are the exact primitive you need to build rigorous abstractions that don't leak.


# Conclusion

Electric Clojure's goal is to raise the abstraction ceiling in web development, by making it so that your application logic can be expressed out of nothing but lambda. And we really mean it. Remember "Functional Core Imperative Shell"? Where is the imperative shell here?

Lambda is the difference between abstraction and boilerplate. I challenge you to take a hard look at your current web project, stare at whatever convoluted machine you have to deal with this week, and ask yourself: Why does this exist? What artificial problem is preventing this from being a 20 line expression?

P.S. You'll notice in the URL that we've essentially URL-encoded an S-expression and routed to it. Why do you suppose we do that? ... *This essay is a function.*

---

To learn more about Electric, check out the [live tutorial](https://electric.hyperfiddle.net/), the [github repo](https://github.com/hyperfiddle/electric/) or [clone the starter app](https://github.com/hyperfiddle/electric-starter-app) and start playing.
27 changes: 27 additions & 0 deletions src/dustingetz/essay.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
(ns dustingetz.essay
(:require clojure.string
[electric-fiddle.fiddle :refer [Fiddle-fn Fiddle-ns]]
[electric-fiddle.fiddle-markdown :refer [Custom-markdown]]
[electric-fiddle.index :refer [Index]]
[hyperfiddle :as hf]
[hyperfiddle.electric :as e]
[hyperfiddle.electric-dom2 :as dom]
[hyperfiddle.history :as history]))

(def essays
{'electric-y-combinator "src-fiddles/dustingetz/electric_y_combinator.md"
'hfql-intro "src-fiddles/dustingetz/hfql_intro.md"
'hfql-teeshirt-orders "src-fiddles/dustingetz/hfql_teeshirt_orders.md"})

(e/def extensions
{'fiddle Fiddle-fn
'fiddle-ns Fiddle-ns})

(e/defn Essay [& [?essay]]
#_(e/client (dom/div #_(dom/props {:class ""}))) ; fix css grid next
(e/client
(let [essay-filename (get essays ?essay)]
(cond
(nil? ?essay) (binding [hf/pages essays] (Index.))
(nil? essay-filename) (dom/h1 (dom/text "Essay not found: " history/route))
() (Custom-markdown. extensions essay-filename)))))
42 changes: 42 additions & 0 deletions src/dustingetz/fiddles.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
(ns dustingetz.fiddles
(:require [hyperfiddle.electric :as e]
[hyperfiddle :as hf]
[dustingetz.scratch.demo-explorer-hfql :refer [DirectoryExplorer-HFQL]]
[dustingetz.hfql-intro :refer [With-HFQL-Bindings
Teeshirt-orders-1
Teeshirt-orders-2
Teeshirt-orders-3
Teeshirt-orders-4
Teeshirt-orders-5]]
dustingetz.scratch
[dustingetz.y-fib :refer [Y-fib]]
[dustingetz.y-dir :refer [Y-dir]]
[dustingetz.essay :refer [Essay]]
[dustingetz.painter :refer [Painter]]

[electric-fiddle.main]
#?(:clj models.teeshirt-orders-datomic)
))

(e/def fiddles
{`Y-fib Y-fib
`Y-dir Y-dir
`Essay (With-HFQL-Bindings. Essay)
`Teeshirt-orders-1 (With-HFQL-Bindings. Teeshirt-orders-1)
`Teeshirt-orders-2 (With-HFQL-Bindings. Teeshirt-orders-2)
`Teeshirt-orders-3 (With-HFQL-Bindings. Teeshirt-orders-3)
`Teeshirt-orders-4 (With-HFQL-Bindings. Teeshirt-orders-4)
`Teeshirt-orders-5 (With-HFQL-Bindings. Teeshirt-orders-5)
`DirectoryExplorer-HFQL (With-HFQL-Bindings. DirectoryExplorer-HFQL)
`Painter Painter
`dustingetz.scratch/Scratch dustingetz.scratch/Scratch})

#?(:clj
(models.teeshirt-orders-datomic/init-datomic))

(e/defn FiddleMain [ring-req]
(e/client
(binding [hf/pages fiddles]
(e/server
(electric-fiddle.main/Main. ring-req)))))

31 changes: 31 additions & 0 deletions src/dustingetz/fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
app = "electric-fiddle"
primary_region = "ewr"
kill_signal = "SIGINT"
kill_timeout = "5s"

[experimental]
auto_rollback = true

[[services]]
protocol = "tcp"
internal_port = 8080
processes = ["app"]

[[services.ports]]
port = 80
handlers = ["http"]
force_https = true

[[services.ports]]
port = 443
handlers = ["tls", "http"]
[services.concurrency]
type = "connections"
hard_limit = 200
soft_limit = 150

[[services.tcp_checks]]
interval = "15s"
timeout = "2s"
grace_period = "1s"
restart_limit = 0
121 changes: 121 additions & 0 deletions src/dustingetz/hfql_intro.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
(ns dustingetz.hfql-intro
(:require [clojure.spec.alpha :as s]
[contrib.electric-codemirror :refer [CodeMirror]]
contrib.str
#?(:clj [datomic.api :as d])
[electric-fiddle.index :refer [Index]]
[hyperfiddle.api :as hf]
[hyperfiddle.electric :as e]
[hyperfiddle.electric-dom2 :as dom]
[hyperfiddle.hfql :refer [hfql]]
[hyperfiddle.hfql-tree-grid :refer [with-gridsheet-renderer]]
[hyperfiddle.rcf :as rcf :refer [tests tap % with]]
#?(:clj [models.teeshirt-orders-datomic :as model
:refer [orders genders shirt-sizes]])))

(e/defn Codemirror-edn [x]
(CodeMirror. {:parent dom/node :readonly true} identity contrib.str/pprint-str
x))

(e/defn Teeshirt-orders-1 []
(Codemirror-edn.
(e/server
(hfql [hf/*$* hf/db]
{(orders "")
[:db/id
:order/email]}))))

(e/defn Teeshirt-orders-2 []
(with-gridsheet-renderer
(e/server
(hfql [hf/*$* hf/db]
{(orders .)
[:db/id
:order/email]}))))

(e/defn Teeshirt-orders-3 []
(with-gridsheet-renderer
(e/server
(hfql [hf/*$* hf/db]
{(orders .)
[:db/id
:order/email
:order/gender
:order/shirt-size]}))))

(e/defn Teeshirt-orders-4 []
#_
(e/client
(dom/table
(e/server
(e/for-by :db/id [record (orders "")]
(let [{:keys [db/id
order/email
order/gender
order/shirt-size]} record]
(e/client
(dom/tr
(dom/td (dom/input id))
(dom/td (dom/input email))
(dom/td (dom/select
:value gender
:options (e/fn [filter]
(e/server (genders filter)))))
(dom/td (dom/select
:value shirt-size
:options (e/fn [filter]
(e/server (shirt-sizes gender filter)))))))))))))

(e/defn Teeshirt-orders-5 []
(with-gridsheet-renderer
(e/server
(hfql [hf/*$* hf/db]
{(orders .)
[:db/id
:order/email
:order/gender
:order/shirt-size]}))))

(e/defn With-HFQL-Bindings [F & args]
(e/client
(e/fn [& args']
(e/server
(binding [hf/db model/*$* ; hfql compiler
hf/*nav!* model/nav! ; hfql compiler
hf/*schema* model/-schema ; hfql gridsheet renderer
]
(e/client
(e/apply F (concat args args'))))))))

(e/defn Scratch [_]
(e/client
(dom/h1 (dom/text "hi"))
(dom/pre (dom/text (pr-str (e/server ((e/partial-dynamic [hf/*$* hf/db] #(orders "")))))))
(dom/pre (dom/text (pr-str (e/server (str hf/db)))))
(dom/pre (dom/text (pr-str (e/server (hfql [hf/*$* hf/db] 42)))))
(dom/pre (dom/text (pr-str (e/server (hfql [hf/*$* hf/db] :db/id 1)))))
(with-gridsheet-renderer
(e/server (hfql [hf/*$* hf/db] :db/id 1)))))

#?(:clj
(tests
(alter-var-root #'hf/*$* (constantly model/*$*))
(some? hf/*$*) := true
(orders "") := [1 2 3]

(with (e/run (tap (binding [hf/db hf/*$*
hf/*nav!* model/nav!]
(hfql [] :db/id 1))))
% := 1)

(with (e/run (tap (binding [hf/db hf/*$*
hf/*nav!* model/nav!]
(hfql []
{(orders "")
[:db/id
:order/email
:order/gender]}))))
% := {`(orders "")
[{:db/id 1, :order/email "[email protected]", :order/gender :order/female}
{:db/id 2, :order/email "[email protected]", :order/gender :order/male}
{:db/id 3, :order/email "[email protected]", :order/gender :order/male}]})))
11 changes: 11 additions & 0 deletions src/dustingetz/hfql_intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# HFQL intro

by Dustin Getz, 2023 July - wip

!fiddle[](dustingetz.hfql-intro/Teeshirt-orders-1)

!fiddle[](dustingetz.hfql-intro/Teeshirt-orders-2)

!fiddle[](dustingetz.hfql-intro/Teeshirt-orders-3)

* Fix router
28 changes: 28 additions & 0 deletions src/dustingetz/hfql_teeshirt_orders.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Teeshirt Orders – HFQL demo

!fiddle-ns[](hfql-demo.hfql-teeshirt-orders/HFQL-teeshirt-orders)


What's happening
* there's a CRUD table, backed by a query
* the table is specified by 4 lines of HFQL + database schema + the clojure.spec for the query
* the filter input labeled `needle` is reflected from the clojure.spec on `orders`, which specifies that the `orders` query accepts a single `string?` parameter named `:needle`
* type "alice" into the input and see the query refresh live

Novel forms
* `hf/hfql`
* `binding`: Electric dynamic scope, it's reactive and used for dependency injection
* `hf/db`
* `hf/*schema*`
* `hf/*nav!*`

Key ideas
* HFQL's mission is to let you model CRUD apps in as few LOC as possible.
* HFQL generalizes graph-pull query notation into a declarative UI specification language.
* spec-driven UI
* macroexpands down to Electric
* network-transparent
* composes with Electric as e/defn
* scope

Dependency injection with Electric bindings
47 changes: 47 additions & 0 deletions src/dustingetz/painter.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
(ns dustingetz.painter
"video: https://gist.github.com/dustingetz/d58a6134be310e05307ca0b586c30947
upstream: https://github.com/formicagreen/electric-clojure-painter"
(:require [hyperfiddle.electric :as e]
[hyperfiddle.electric-dom2 :as dom]))

(def emojis ["πŸ•‰" "🧬" "🧿" "πŸŒ€" "♻️" "🐍" "🐱" "πŸ«₯" "🌰" "🐞" "🐹" "πŸͺ™" "πŸ•Έ" "πŸ“ž"])
#?(:cljs (def mousedown (atom false)))
#?(:cljs (defonce current-emoji (atom "🐱")))
#?(:clj (defonce vertices (atom [])))

(e/defn Painter []
(e/client
(dom/style {:margin "0" :overflow "hidden" :background "lightblue" :user-select "none" :font-size "30px"})
(dom/element "style" (dom/text "@keyframes fadeout { from { opacity: 1; } to { opacity: 0; } }"))
(dom/div
(dom/style {:width "100vw" :height "100vh"})
(dom/on "mousedown" (e/fn [e] (reset! mousedown true)))
(dom/on "mouseup" (e/fn [e] (reset! mousedown false)))

(dom/on "mousemove"
(e/fn [e] (let [x (.-clientX e) y (.-clientY e)
m (e/watch mousedown)
e (e/watch current-emoji)]
(when m
(e/server
(swap! vertices conj [x y e]))))))

(dom/div
(dom/style {:background "#fff5" :backdrop-filter "blur(10px)" :position "fixed"
:top "0" :left "0" :height "100vh" :padding "10px"})
(e/for-by identity [emoji emojis]
(dom/div (dom/text emoji)
(dom/style {:cursor "pointer"})
(dom/on "click" (e/fn [e] (reset! current-emoji emoji)))))
(dom/div (dom/text "πŸ—‘οΈ")
(dom/style {:cursor "pointer" :padding-top "50px"})
(dom/on "click" (e/fn [e] (e/server (reset! vertices []))))))

(dom/div
(e/for-by identity [[x y e] (e/server (e/watch vertices))]
(dom/div (dom/text e)
(dom/style {:width "10px" :height "10px"
:position "absolute"
:left (str x "px")
:top (str y "px")
:user-select "none" :z-index "-1" :pointer-events "none"})))))))
7 changes: 7 additions & 0 deletions src/dustingetz/scratch.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
(ns dustingetz.scratch
(:require [hyperfiddle.electric :as e]
[hyperfiddle.electric-dom2 :as dom]))


(e/defn Scratch []
(e/client (dom/pre (dom/text "yo"))))
Loading

0 comments on commit 989c05c

Please sign in to comment.