Fork me on GitHub
#beginners
<
2020-10-11
>
Eric Ihli02:10:05

I'm trying to write a macro that wraps a bunch of forms in try/catch blocks.

(defmacro try-forms [bindings & children]
  `(let* ~(destructure bindings)
     (map
      (fn [child#]
        (try
          child#
          (catch Exception e# (println
                               (format "Couldn't evaluate %s because of %s" child# e#)))))
      '[~@children])))

(try-forms
 [some-float 5.0]
 (format "val: %f" some-float)
 (format "val: %d" some-float))
;; => ((format "val: %f" some-float) (format "val: %d" some-float))
That's close. I'm getting a list of the forms that I want. They just aren't being evaluated. I could think to try wrapping child# in an eval, but that would try to evaluate the literal form child# and not the form that child# represents ( (format "val: %f" some-float)), right? I'm having a hard time discovering the solution to this.

walterl11:10:46

Looks like you probably want this:

walterl11:10:08

Which expands like so:

; eval (root-form): (macroexpand '(try-forms [some-float 5.0] (format "val: %f" some-float...
(let*
 [some-float 5.0]
 (try
  (format "val: %f" some-float)
  (catch
   java.lang.Exception
   e__69339__auto__
   (clojure.core/println
    (clojure.core/format
     "Couldn't evaluate %s because of %s"
     "(format \"val: %f\" some-float)"
     e__69339__auto__))))
 (try
  (format "val: %d" some-float)
  (catch
   java.lang.Exception
   e__69339__auto__
   (clojure.core/println
    (clojure.core/format
     "Couldn't evaluate %s because of %s"
     "(format \"val: %d\" some-float)"
     e__69339__auto__)))))

Eric Ihli13:10:19

Thanks walterl! That makes sense. I'm, going to go play around with it some more.

Jim Newton11:10:34

There is an interesting feature/bug/loophole in the Record concept w.r.t. namespaces. In one namespace I've defined a Record and a slew of functions to manipulate the instances thereof. actually I've defined several different Records in several different namespaces which are independent from each other. the objects form different types of graphs to represent particular problems. In a 3rd namespace, I have functions which output the graphs to graphviz dot format. The xyz-to-dot functions can access the record instances as if they were hash maps. This means there is no need t make my dot namespace require the namespaces of the records for which the code is manipulating.

andy.fingerhut12:10:09

I am not sure which aspect of this you consider to be a loophole or bug, but if it is that you expected that the fields of records to be private/hidden/inaccessible-without-access-privileges, then I think that is considered a feature of Clojure, not a bug. 'Immutable data doesn't need encapsulation' is a rough paraphrase of something Rich Hickey has said on multiple occasions -- if I can find an actual quote I will link it here.

andy.fingerhut12:10:31

In Clojure, records are intended to look like maps. defrecord is a thing in case you want to use the new type for defprotocol, and for lookup and memory efficiency of the record fields you define. But you can assoc new keys onto a record, and get back a normal Clojure map if you add a key that isn't defined in the defrecord type.

andy.fingerhut12:10:00

Some of this is from the http://clojure.org doc page on data types, which discusses properties of defrecord and deftype: https://clojure.org/reference/datatypes

Jim Newton11:10:38

this means the namespace topology of my project does not match the actual loading requirements of my program.

andy.fingerhut12:10:32

I believe that if you want to actually create objects in memory that are instances of that record type, you cannot (easily) do so without require'ing the namespace with the defrecord form.

andy.fingerhut12:10:04

Reading their fields as if they were a regular Clojure map can be done without that.

Jim Newton11:10:59

not sure whether this is a feature or a bug.

Panagiotis Mamatsis14:10:25

Good afternoon everyone. I was seeing a video on YouTube. It's called "Clojure for Java Programmers Part 1". At some point Ritch mentions that you can fix a bug in a running instance of a program! I was wondering how is it possible to do that? Isn't Clojure a compiled language?

andy.fingerhut14:10:16

A typical way is to connect to the running JVM process via a REPL connection, and evaluate def and defn forms to redefine functions or values on the fly

andy.fingerhut14:10:44

Clojure’s compilation unit is not a whole file, but each top level form separately

andy.fingerhut14:10:24

Even in a REPL, each form is compiled before it is executed (compiles to JVM byte code)

Panagiotis Mamatsis15:10:11

Oh! This means that even if my application is a jar file I can still connect to it and fix bugs?

andy.fingerhut15:10:20

Yes. In practice, many people who deploy JAR files to a collection of machines often prefer to fix bugs by using more standard "update the JAR, deploy the JAR as if it was a new version" methods, but if you have a single server, or want to investigate the running data in a live server to track down some bug, the connect-via-REPL-and-evaluate-forms approach can be useful for read-only detailed investigation, too, whether you use that method to do live bug fixing or not.

andy.fingerhut15:10:29

Restricting oneself to "standard deployment mechanism" can be useful for reproducibility, e.g. the fix is checked into revision control, etc.

Panagiotis Mamatsis15:10:48

I totally agree with the last reply. But the "connect using REPL" blew my mind! I thought that BEAM based languages had this feature!!!

andy.fingerhut15:10:32

I believe that Erlang has this feature, but I haven't used it or other languages running on the BEAM VM, so don't trust my statement on the matter.

andy.fingerhut16:10:00

Not all, but many Lisp family languages have this capability, and Smalltalk family languages.

andy.fingerhut16:10:45

And Clojure isn't the only Lisp that is able to compile each form before executing it, e.g. several Common Lisp implementations can do that.

seancorfield19:10:58

Note that if you enable "direct linking" as part of your compilation process when building your (uber) JAR, then you cannot easily "patch" the live, running app from a REPL (because you would also need to re-def every upstream call site of the function you change and, recursively, their callers all the way up to -main. If you are not using that, then you can re-def functions on the fly in production via a REPL and the change take immediate effect. We do this in production for one of our apps where we do not want any downtime due to redeployment (and for complex reasons it is a single instance).

seancorfield19:10:55

Also bear in mind that such changes only stay in affect until the next app restart (unless you also update the source and/or JAR file with the same changes applied via the REPL).

Eric Ihli15:10:13

Is there a way to achieve the following functionality? I want to have a flag that I can enable that will change the behavior of format so that it prints the raw forms rather than the evaluated values. Something like the code below (which doesn't work binding can't take the value of a macro.

;; Replace all %d, %,.2f, etc.. with %s. Then apply format to the new string and the unevaluated args.
(defmacro format* [s & args]
  (apply format (string/replace s #"%[^% ]+" "%s") args))

(def ^:dynamic format clojure.core/format)

(binding [format format*]
  (format "Hi %s. Half your age of %d is %.2f"
          (:name person)
          (:age person)
          (/ (:age person) 2)))

andy.fingerhut15:10:49

To clarify what you are trying to achieve, you want a modified version of format that in some cases works exactly like format does, but by changing some global flag then you would like the output of your example format call to be the following? Hi (:name person). Half your age of (:age person) is (/ (:age person) 2) ?

andy.fingerhut15:10:57

If so, that sounds achievable by defining a new macro, e.g. named my-format, where each invocation of my-format expanded into some code like this:

(if *print-raw-forms*
  (format "Hi person with age %s." '(:age person))
  (format "Hi person with age %d." (:age person)))

andy.fingerhut15:10:57

There is no way I can think of to get the output shown above by changing the definition of a function like format, because in Clojure the args to every function call are evaluated before the call occurs.

Eric Ihli16:10:55

Thanks. So, I'm understanding that there is no reasonably convenient way to get it to "just work with code written with the existing format ".

andy.fingerhut16:10:25

If you want unevaluated arguments, then there is no reasonably convenient way to do that for any existing function, which includes format

andy.fingerhut16:10:20

It is possible for existing macros, or for new macros that you write yourself

athomasoriginal17:10:25

Macro Question: I wrote a macro that slurps in an .svg file and outputs hiccup. This is CLJS

;; macro is pseudo code
(defmacro inline-svg 
  [path]
  (assert (string? path))
  (let [hiccup (-> (slurp path)
                   (transform-to-hiccup))]
    `~hiccup))

;; callsite
(inline-svg "path/to/file.svg")
The above works great. However, if I were to change the callsite to something like
(inline-svg (:icon-name icons))
The above doesn’t work because. TMK the (:icon-name icons) isn’t evaluated. How could I evaluate the first arg first? Thanks all!

andy.fingerhut17:10:51

It is evaluated, but as a macro invocation, as you have written it, it is evaluated at compile time, i.e. when your code is loaded or require'd.

andy.fingerhut17:10:53

Is there any reason you need inline-svg to be a macro? It seems like it could just as well read "path/to/file.svg" at run time and convert it to hiccup then, couldn't it?

andy.fingerhut17:10:15

i.e. why not change inline-svg to a function?

athomasoriginal17:10:29

Good question. A little more background is this is CLJS. So my goal is to not have to do the “slurp + transform” at runtime. That’s why, in this case, I thought a macro would be best.

andy.fingerhut17:10:56

If icons in the expression (:icon-name icons) already has the value you want when the code is compiled, somehow, then likely you can use it in a macro invocation. If not, i.e. if the value of (:icon-name icons) cannot be known until run time, then there is no way to avoid the transformation at run time, that I can see.

athomasoriginal18:10:17

Yeah, icons is a hardcoded map like

(def icons {:path-1 "path/to/blah.svg"}

andy.fingerhut18:10:48

There may be some differences between Clojure vs ClojureScript macros that I am not well prepared to answer, me being more familiar with using macros in Clojure, and I know there are some differences, or restriction in ClojureScript. For example, if that hardcoded map is defined in the same file as the macro is, then it isn't clear to me why it would not work. If the hard-coded map is in a .cljc file, and the macro is not, then you will likely need to find someone besides me to tell you whether that is possible.

👍 3
andy.fingerhut18:10:37

I think that perhaps when compiling ClojureScript, all macros are expanded in a Clojure/JVM process during compilation, which is why I am guessing that if the def of icons is in a cljc file, its value might not be 'visible' to the macro at compile time.

athomasoriginal19:10:22

Indeed. After a little more research, I found mfikes posts (in case a future person is trying to do something similar) which sheds some light on the issue: https://blog.fikesfarm.com/posts/2016-01-05-clojurescript-macros-calling-functions.html https://blog.fikesfarm.com/posts/2015-09-07-messing-with-macros-at-the-repl.html

andy.fingerhut20:10:29

Mike is definitely a person who should know the ClojureScript compiler and its effect on how macros are processed in ClojureScript very well.

👍 3
💯 3
Day Bobby17:10:09

I have some code that determines if an action is to be executed in current environment. I probably can use some library to inject an env var like CLOJURE_ENV (method I learned from node.js land) and use that for the branching logics. But I'm wondering since lein knows about my profiles already (dev, test, uberjar<-prod), can I somehow use that instead, or is there a better, more idiomatic way?

practicalli-johnny17:10:22

If it's just a couple of environment variables,you could just use System/getenv It there is quite a few, then aero is an excellent project https://github.com/juxt/aero

practicalli-johnny17:10:14

Leiningen profiles are useful for running the app locally with a different environment

Day Bobby17:10:42

hello there, first off thank you for your useful courses, I learned a lot from them. Funny you mentioned aero , it's a 🔥 library, I'm trying to take advantage of #profile (https://github.com/juxt/aero#profile). To use it I must pass the current environment into read-config call so thats really why I needed the solution.

Day Bobby17:10:31

I guess i can just do System/getEnv to get the current profile from shell and use it there

Day Bobby17:10:42

Thank you again!