This repository has been archived by the owner on Dec 8, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d2662aa
commit 989c05c
Showing
12 changed files
with
447 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))))) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}]}))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"}))))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")))) |
Oops, something went wrong.