Fork me on GitHub
#hyperfiddle
<
2023-02-20
>
Carsten Behring08:02:42

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.

πŸ‘€ 2
Carsten Behring08:02:17

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 ?

πŸ‘€ 2
xificurC09:02:02

β€’ 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

πŸ‘ 4
Carsten Behring09:02:51

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)

Carsten Behring09:02:30

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)

Carsten Behring10:02:18

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

xificurC10:02:25

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

Dag10:02:03

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

πŸ‘€ 2
βž• 2
Dustin Getz11:02:51

we will work on an optimized dockerfile that runs the Electric compiler at build time. it’s the compiler that uses too much memory

πŸ‘ 2
πŸ’― 2
Dag12:02:53

Awesome If you are still doing zoom sessions I am very interested

πŸ‘€ 2
JAtkins16:02:01

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?

πŸ‘€ 2
Dustin Getz16:02:50

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

Dustin Getz16:02:40

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

Dustin Getz16:02:18

Does this answer your question? I'll provide an example after I am certain I understand the question

JAtkins16:02:10

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.

πŸ‘€ 2
Dustin Getz16:02:57

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

JAtkins16:02:03

hm... is this on the roadmap or are there technical limitations on doing something like this?

Dustin Getz16:02:26

This summer maybe

JAtkins16:02:46

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?

JAtkins16:02:05

(probably through e/watch!)

Dustin Getz16:02:22

Yes you can join dynamic missionary DAGs but missionary DAGs do not have managed network, so no client/server transfer

πŸ‘ 2
Dustin Getz16:02:06

> 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

JAtkins16:02:22

unrelated - is it possible to orchestrate multiple clients together? e.g. server, plugin, and client?

Dustin Getz16:02:56

N-sites is on the roadmap

JAtkins17:02:36

> 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.

πŸ‘€ 2
JAtkins17:02:30

πŸ‘€ 2
JAtkins17:02:10

the image could use an update, but this is sorta the idea

Dustin Getz17:02:31

Yeah, this is the direction the Hyperfiddle product is exploring

πŸŽ‰ 2
Dustin Getz18:02:48

Rereading your example - to be clear, we have not just e/defn but also e/fn (lambda)

Dustin Getz18:02:55

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))

Dustin Getz18:02:01

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))

Dustin Getz18:02:40

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

JAtkins22:02:41

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. {})))

Dustin Getz22:02:01

what error do you see?

Dustin Getz22:02:20

i cant think of a reason that this wont work

JAtkins22:02:21

[: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
--------------------------------------------------------------------------------

πŸ‘€ 2
Dustin Getz22:02:03

thinking

πŸ™ 2
xificurC22:02:42

Try new Thing please

loading 2
πŸ‘€ 2
Dustin Getz22:02:59

try

#?(:cljs (:require-macros [user.demo-1-hello-world :refer [wrap-defn]]))

JAtkins22:02:09

[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 {})))

Dustin Getz22:02:43

we have an unknown shadow cljs issue likely related to macros and hot code reloading that we dont understand yet

Dustin Getz22:02:45

you may be hitting it

JAtkins22:02:39

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

Dustin Getz22:02:59

this is modified from demo-1-hello-world

Dustin Getz22:02:00

it works for me

JAtkins22:02:11

yup, can confirm

πŸ‘ 4
JAtkins22:02:12

(honestly that's just regular cljs stuff that I should've figured out, but oh well - thanks for the help anyway πŸ™‚)

Dustin Getz22:02:52

we are happy to get this kind of question for the time being at least

Dustin Getz22:02:17

btw what are you trying to do

JAtkins22:02:47

I'm screwing around with a fulcro-ish decorator for e/defn. Not very far along yet b/c I got stuck here

JAtkins22:02:31

I might be thinking too inside-the-box here, but the API might be nice

Dustin Getz22:02:05

i dont know fulcro, what should i google to see what you're going for

JAtkins22:02:04

The first example sorta covers how it works. components are actually entities in a normalized database

JAtkins22:02:55

the "normalized" bit makes data storage easier to reason about in my experience than something like reframe

Dustin Getz22:02:09

client database you mean?

JAtkins22:02:42

if it looks good I'll post a GH gist

Dustin Getz22:02:09

i dont understand the query bit, which i suppose is the important part you want

Dustin Getz22:02:21

this has all the other lifecycle bits though

JAtkins23:02:50

The query bit is important re. Prior thread about declarative data path planning only. The above might cover my needs though

Dustin Getz13:02:47

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

Dustin Getz13:02:26

I want to understand pathom better and especially how you're using it

JAtkins13:02:07

Sure, sounds good. I'll put together some pseudo code for it beforehand, cuz our internal code is big and a mess πŸ™‚

πŸ‘ 2