This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-02-20
Channels
- # aleph (78)
- # announcements (13)
- # architecture (8)
- # aws (3)
- # babashka (110)
- # beginners (38)
- # calva (2)
- # clerk (1)
- # clojure (118)
- # clojure-austin (3)
- # clojure-dev (8)
- # clojure-europe (50)
- # clojure-france (2)
- # clojure-nl (1)
- # clojure-portugal (1)
- # clojure-uk (3)
- # clojurescript (101)
- # clr (10)
- # data-science (15)
- # datascript (5)
- # datomic (3)
- # events (1)
- # fulcro (22)
- # graalvm (2)
- # gratitude (3)
- # guix (1)
- # honeysql (1)
- # hyperfiddle (72)
- # jobs (3)
- # lsp (23)
- # malli (18)
- # membrane (29)
- # obb (1)
- # releases (1)
- # ring-swagger (2)
- # shadow-cljs (8)
- # squint (113)
- # xtdb (9)
I have an question of "the absolute beginner", which knows nothing about Electric Clojure.
But this is maybe a key piece of documentation to be added first. For the rest we can look at the existing example code. Some words of explanation about the relationship between !x
x
and why / how and where we use them either as
(swap! !x not)
(if (e/server x)
This is key, because doing this wrong fails in (for a beginner) unpredictable ways, and not in obvious "compile errors".
(ns user.demo-2-toggle
#?(:cljs (:require-macros user.demo-2-toggle))
(:require
[hyperfiddle.electric :as e]
[hyperfiddle.electric-dom2 :as dom]
[hyperfiddle.electric-ui4 :as ui]))
; A stateful app with a server-side counter
; Electric functions are reactive and incrementally maintained,
; which means that when you make a small change to the state,
; the functions will recompute and you'll get a small adjustment to the DOM
(defonce !x #?(:clj (atom true) :cljs nil)) ; server state
(e/def x (e/server (e/watch !x))) ; reactive signal derived from reference
(e/defn App []
(e/client
(dom/h1 (dom/text "Toggle"))
(ui/button (e/fn [] (e/server (swap! !x not)))
(dom/text "toggle client/server"))
(dom/p
(dom/text "Number type is: "
(if (e/server x)
(e/client (pr-str (type 1))) ; javascript number type
(e/server (pr-str (type 1)))))))) ; java number type```
By understanding this basics (and to know "which data types" are allowed in x
), us beginners can do quite some experiments, to start to understand Electric Clojure and see its potential.Concretely asked:
When am I supposed to read/change the "server state" !x
and when am I supposed to read/change the "reactive signal" x
and why do I need this 2 ?
And why do I never need to "deference" the atom in the usual way @!x
in this example app ?
β’ We mark atoms with a leading exclamation mark, so !x
holds a clojure atom which is a mutable data type, i.e. you can write a new value into it (`reset!` or swap!
)
β’ (e/watch !x)
returns a reactive read-only view of the atom's value
β’ foo
in (foo (e/watch !x))
will re-run whenever the value of !x
changes
β’ foo
in (foo @!x)
will only run once
Ok, thanks, just one more question: Inside both expressions
(e/server .... and (e/client
x
can be accessed, as "in global scope`
Do I use them in exactly the same way across Clojure and Clojurescript and are they guarantied to contain the same data ?
(and what happens with data of different types in this case , maybe even incompatible between Javascript and JVM or Clojurescript / Clojure)Because it "seems" that x
/ !x
is a common variable across client/server, to which Clojurescript can "write into" and Clojure can "read from".
Is this a correct interpretation ?
(Ok, the demo code above does no show the access of access to`x` from Clojurescript, but I have tried it and it seems to work)
This variant of the toggle demo shows this magic very nicely:
(e/defn App []
(e/client
(dom/h1 (dom/text "Toggle"))
(ui/button (e/fn [] (e/server (swap! !x not)))
(dom/text (str "toggle client/server : current = "
(case x
true "Client"
false "Server"
))))
(dom/p
(dom/text "Number type is: "
(if (e/server x)
(e/client (pr-str (type 1))) ; javascript number type
(e/server (pr-str (type 1)))))))) ; java number type
One of the value propositions of electric is you shouldn't have to care where is the value defined to read it. Writing will happen on one side only though, you cannot reset!
a server atom on the client (that would also be dangerous).
> Do I use them in exactly the same way across Clojure and Clojurescript and are they guarantied to contain the same data ?
yes
> what happens with data of different types in this case , maybe even incompatible between Javascript and JVM
currently we use transit to send data over the wire, so if you send something it cannot serialize you'll see an "Unserializable reference transfer" error in your console
Wow, cannot overstate how cool this project is Have been playing around with it for a while, wondering if you have any suggestions for deploying? Tried putting the starter example in a docker image but if I try to deploy on something like http://Fly.io or Railway it runs out of memory before the server can start
we will work on an optimized dockerfile that runs the Electric compiler at build time. itβs the compiler that uses too much memory
Ok, I finally got to the example I was slightly worried about. It appears that e/defn
calls create unbound vars outside the context of an electric fn. How would I go about making a dynamic data path, where parts of the reactive structure can be swapped out? Would that necessitate dropping down to missionary land? Does dropping down to missionary then prevent using the e/server
and e/client
because the compiler no longer is informed about context switches?
the e/defn macro, today, just creates an ordinary def and saves the quoted Electric code as metadata on the clojure var, the whole program is compiled all at once at the application entrypoint
IIUC, for "dynamic data path" you have if
statements which switch between pieces of DAG (imagine train track switch), as well as e/fn
Electric lambdas which are higher order DAG values
Does this answer your question? I'll provide an example after I am certain I understand the question
Sorta. It makes sense that this would work instead
(e/defn PerfOp
[{::keys [count op]}]
(println "PerfOp" count op (pr-str (get ops op)))
(new (case op
:times Times
:inc Inc) count))
but that's somewhat limiting. The use case I envision is using pathom to declaratively define the data model, and for components to declaratively define what data they depend on. Then a library would do the wiring up of a function call tree that provides the data required by the UI.We do not today support dynamic linking of Electric Clojure compilation units, so the "hyper-dag" (spanning set of all possible DAGs) must be known at compile time
hm... is this on the roadmap or are there technical limitations on doing something like this?
This summer maybe
I'm probably going to fiddle with it, but is it possible to write a subset of the dag in missionary and hook it into the electric compiler?
Yes you can join dynamic missionary DAGs but missionary DAGs do not have managed network, so no client/server transfer
> The use case I envision is using pathom to declaratively define the data model, and for components to declaratively define what data they depend on. Then a library would do the wiring up of a function call tree that provides the data required by the UI. iiuc i see no reason this can't be done at compile time through a macro
unrelated - is it possible to orchestrate multiple clients together? e.g. server, plugin, and client?
N-sites is on the roadmap
> iiuc i see no reason this can't be done at compile time through a macro Technically yes, it's possible to run that at compile time. However a step further on top of this idea is user-defined UI. electric takes care of a massive amount of UI wiring, pathom can take care of data path planning, so suddenly with only a bit more work you can get user defined UI. e.g. they could change a number input into a slider. Or, for a detail viewer show not only the name of a person but all user details.
Rereading your example - to be clear, we have not just e/defn
but also e/fn
(lambda)
so in addition to
(e/defn PerfOp
[{::keys [count op]}]
(println "PerfOp" count op (pr-str (get ops op)))
(new (case op
:times Times
:inc Inc) count))
we also have
(e/defn PerfOp
[{::keys [count op]}]
(println "PerfOp" count op (pr-str (get ops op)))
(let [Times (e/fn [x] ...)
Inc (e/fn [x] ...)
Choice (if (= 1 (rand-int 2))
(e/fn [x] (* 10 x))
(e/fn [x] (inc x)))])
(new Choice count))
it's really lambda, you can do quite a lot with this, the thing we don't have today is (1) eval
and (2) dynamic linking
β’ Here is a test similar to your example, with https://github.com/hyperfiddle/electric/blob/4a5e43a630f542663ab561f110b5706d99a42d77/test/hyperfiddle/electric_test.cljc#L120-L128 β’ here's another good one - https://github.com/hyperfiddle/electric/blob/4a5e43a630f542663ab561f110b5706d99a42d77/test/hyperfiddle/electric_test.cljc#L1704-L1711 (Electric signals form Circuits actually not DAGs but we say DAG anyway for marketing reasons)
e/defn doesn't seem to play well with wrapping macros - I guess this is expected?
(defmacro wrap-defn
[name args & body]
`(e/defn ~name ~args ~@body))
(wrap-defn Thing
[m]
(dom/div (dom/text "Hello")))
;; macroexpand-1
#_(hyperfiddle.electric/defn Thing [m] (dom/div (dom/text "Hello")))
(e/defn App
[]
(e/client
(Thing. {})))
what error do you see?
i cant think of a reason that this wont work
[:dev] Compiling ...
[:dev] Build failure:
------ ERROR -------------------------------------------------------------------
File: /Users/jarrett/code/oss/electric/src-dev/user.cljs:7:20
--------------------------------------------------------------------------------
4 | hyperfiddle.rcf
5 | user-main))
6 |
7 | (def electric-main (hyperfiddle.electric/boot (user-main/Main.)))
--------------------------^-----------------------------------------------------
Encountered error when macroexpanding hyperfiddle.electric/boot.
Unable to resolve symbol: Thing
{:file "user/pathom_fulcroish.cljc", :line 85, :column 4, :in [(Thing. {}) (do (Thing. {})) (:hyperfiddle.electric.impl.compiler/client (do (Thing. {})) {:line 84, :column 3, :hyperfiddle.electric.debug/type :transfer, :hyperfiddle.electric.debug/name :hyperfiddle.electric/client}) (e/client (Thing. {})) (let* [] (e/client (Thing. {}))) (clojure.core/let [] (e/client (Thing. {}))) (:hyperfiddle.electric.impl.compiler/closure (clojure.core/let [] (e/client (Thing. {})))) (clojure.core/binding [hyperfiddle.electric.impl.compiler/rec (:hyperfiddle.electric.impl.compiler/closure (clojure.core/let [] (e/client (Thing. {}))))] (new hyperfiddle.electric.impl.compiler/rec)) (do (hyperfiddle.electric/-check-fn-arity! (quote App) 0 hyperfiddle.electric.impl.compiler/%arity) (clojure.core/binding [hyperfiddle.electric.impl.compiler/rec (:hyperfiddle.electric.impl.compiler/closure (clojure.core/let [] (e/client (Thing. {}))))] (new hyperfiddle.electric.impl.compiler/rec))) (:hyperfiddle.electric.impl.compiler/closure (do (hyperfiddle.electric/-check-fn-arity! (quote App) 0 hyperfiddle.electric.impl.compiler/%arity) (clojure.core/binding [hyperfiddle.electric.impl.compiler/rec (:hyperfiddle.electric.impl.compiler/closure (clojure.core/let [] (e/client (Thing. {}))))] (new hyperfiddle.electric.impl.compiler/rec))) {:hyperfiddle.electric.debug/name App, :hyperfiddle.electric.debug/args [], :hyperfiddle.electric.debug/type :reactive-defn, :hyperfiddle.electric.debug/meta {:line 82}})]}
ExceptionInfo: Unable to resolve symbol: Thing
hyperfiddle.electric.impl.compiler/analyze-form (compiler.clj:781)
hyperfiddle.electric.impl.compiler/analyze-form (compiler.clj:767)
hyperfiddle.electric.impl.compiler/eval31638/analyze--31644/fn--31649 (compiler.clj:826)
hyperfiddle.electric.impl.compiler/eval31638/analyze--31644 (compiler.clj:822)
hyperfiddle.electric/boot (electric.cljc:58)
hyperfiddle.electric/boot (electric.cljc:53)
clojure.core/apply (core.clj:671)
clojure.core/apply (core.clj:662)
cljs.analyzer/macroexpand-1*/fn--3704 (analyzer.cljc:4025)
cljs.analyzer/macroexpand-1* (analyzer.cljc:4023)
cljs.analyzer/macroexpand-1* (analyzer.cljc:4010)
cljs.analyzer/macroexpand-1 (analyzer.cljc:4074)
cljs.analyzer/macroexpand-1 (analyzer.cljc:4070)
cljs.analyzer/analyze-seq (analyzer.cljc:4107)
cljs.analyzer/analyze-seq (analyzer.cljc:4087)
cljs.analyzer/analyze-form (analyzer.cljc:4296)
cljs.analyzer/analyze-form (analyzer.cljc:4293)
cljs.analyzer/analyze* (analyzer.cljc:4349)
cljs.analyzer/analyze* (analyzer.cljc:4341)
cljs.analyzer/analyze (analyzer.cljc:4369)
cljs.analyzer/analyze (analyzer.cljc:4352)
cljs.analyzer/analyze (analyzer.cljc:4362)
cljs.analyzer/analyze (analyzer.cljc:4352)
cljs.analyzer/fn--2861/fn--2864 (analyzer.cljc:2010)
cljs.analyzer/fn--2861 (analyzer.cljc:2008)
cljs.analyzer/fn--2861 (analyzer.cljc:1931)
clojure.lang.MultiFn.invoke (MultiFn.java:252)
cljs.analyzer/analyze-seq* (analyzer.cljc:4080)
cljs.analyzer/analyze-seq* (analyzer.cljc:4078)
cljs.analyzer/analyze-seq*-wrap (analyzer.cljc:4085)
cljs.analyzer/analyze-seq*-wrap (analyzer.cljc:4083)
cljs.analyzer/analyze-seq (analyzer.cljc:4109)
cljs.analyzer/analyze-seq (analyzer.cljc:4087)
cljs.analyzer/analyze-form (analyzer.cljc:4296)
cljs.analyzer/analyze-form (analyzer.cljc:4293)
cljs.analyzer/analyze* (analyzer.cljc:4349)
cljs.analyzer/analyze* (analyzer.cljc:4341)
shadow.build.compiler/analyze/fn--18440 (compiler.clj:264)
shadow.build.compiler/analyze (compiler.clj:252)
shadow.build.compiler/analyze (compiler.clj:211)
shadow.build.compiler/analyze (compiler.clj:213)
shadow.build.compiler/analyze (compiler.clj:211)
shadow.build.compiler/default-analyze-cljs (compiler.clj:411)
shadow.build.compiler/default-analyze-cljs (compiler.clj:400)
clojure.core/partial/fn--5908 (core.clj:2642)
shadow.build.compiler/do-analyze-cljs-string (compiler.clj:321)
shadow.build.compiler/do-analyze-cljs-string (compiler.clj:278)
shadow.build.compiler/analyze-cljs-string/fn--18534 (compiler.clj:514)
shadow.build.compiler/analyze-cljs-string (compiler.clj:513)
shadow.build.compiler/analyze-cljs-string (compiler.clj:511)
shadow.build.compiler/do-compile-cljs-resource/fn--18562 (compiler.clj:629)
shadow.build.compiler/do-compile-cljs-resource (compiler.clj:610)
shadow.build.compiler/do-compile-cljs-resource (compiler.clj:568)
shadow.build.compiler/maybe-compile-cljs/fn--18665 (compiler.clj:961)
shadow.build.compiler/maybe-compile-cljs (compiler.clj:960)
shadow.build.compiler/maybe-compile-cljs (compiler.clj:936)
shadow.build.compiler/par-compile-one (compiler.clj:1081)
shadow.build.compiler/par-compile-one (compiler.clj:1036)
shadow.build.compiler/par-compile-cljs-sources/fn--18705/iter--18727--18731/fn--18732/fn--18733/fn--18734 (compiler.clj:1154)
clojure.core/apply (core.clj:667)
clojure.core/with-bindings* (core.clj:1990)
clojure.core/with-bindings* (core.clj:1990)
clojure.core/apply (core.clj:671)
clojure.core/bound-fn*/fn--5818 (core.clj:2020)
java.util.concurrent.FutureTask.run (FutureTask.java:264)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1128)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:628)
java.lang.Thread.run (Thread.java:829)
--------------------------------------------------------------------------------
8 | (defonce reactor nil)
9 |
10 | (defn ^:dev/after-load ^:export start! []
11 | (set! reactor (electric-main
--------------------------------------------------------------------------------
nice
try
#?(:cljs (:require-macros [user.demo-1-hello-world :refer [wrap-defn]]))
[compiler] Still upset. Uncommenting the macroexpand-1
form makes it work again though.
(defmacro wrap-defn
[name args & body]
`(e/defn ~name ~args ~@body))
(wrap-defn Thing
[m]
(dom/div (dom/text "Hello")))
#_
(macroexpand-1 '(wrap-defn Thing
[m]
(dom/div (dom/text "Hello"))))
#_
(hyperfiddle.electric/defn Thing [m] (dom/div (dom/text "Hello")))
(e/defn App
[]
(e/client
(new Thing {})))
we have an unknown shadow cljs issue likely related to macros and hot code reloading that we dont understand yet
you may be hitting it
ha! that seems to have done it. Yeah shadow and macros are the bane of existence. I've spent a day or two debugging them [in the past]... not fun
this is modified from demo-1-hello-world
it works for me
(honestly that's just regular cljs stuff that I should've figured out, but oh well - thanks for the help anyway π)
we are happy to get this kind of question for the time being at least
btw what are you trying to do
I'm screwing around with a fulcro-ish decorator for e/defn. Not very far along yet b/c I got stuck here
i dont know fulcro, what should i google to see what you're going for
this thing?
The first example sorta covers how it works. components are actually entities in a normalized database
the "normalized" bit makes data storage easier to reason about in my experience than something like reframe
client database you mean?
i dont understand the query bit, which i suppose is the important part you want
this has all the other lifecycle bits though
The query bit is important re. Prior thread about declarative data path planning only. The above might cover my needs though
Do you want to do another call and compare notes on this? We have a thing called "HFQL" which is comparable, its a bit too rough to show in public
I want to understand pathom better and especially how you're using it