Fork me on GitHub
#beginners
<
2022-11-15
>
mister_m01:11:54

deps.edn adjacent question: say I am in a namespace defined in a file within a project I am working on in the repl - I have a deps.edn file that defines a :paths vector of "src/main" where that file lives. I also define an :alias which has an :extra-paths vector of "src/dev". I am writing some tools to help me out when developing this project. How can I invoke a function in a namespace defined within the path vector given to the :dev alias? The namespace with the function I want is defined as (ns visualization ...) within the file src/dev/visualization.clj file

mister_m01:11:43

This is just a matter of requiring or evaluating the namespace & its code and then using the literal namespace name like: (visualization/my-func) is that right

seancorfield01:11:30

So you'll start your REPL with -M:dev and then in the REPL you can (require 'visualization)

mister_m01:11:26

So the repl needs to be first restarted when a dependency is added - there is no way to modify the existing classpath with new deps while that is running

seancorfield01:11:22

Well, I use the add-lib3 branch of tools.deps.alpha so I can add dependencies to a running REPL -- see my dot-clojure repo -- but there's no "built-in" way.

mister_m01:11:25

but the -M directive just tells clj to also look at :dev for additional stuff to put into the classpath

seancorfield01:11:03

It's a bit more complicated than that, but for the purposes of this discussion, yes.

seancorfield01:11:40

It also pulls in JVM options and main options if present in the alias(es) specified.

mister_m01:11:03

right on; thanks

Dallas Surewood09:11:12

Hi. I'm a bit new to Clojure/ClojureScript, and I'm finding it difficult to be productive. I have been learning it for around 6 months now. Read Brave Clojure and Grokking Simplicity. Even contributed to a few code bases. I've been making a sort of DnD character sheet thing in Clojure/ClojureScript and long story short, I am finding it hard still to get things done as fast as I'd hoped. A few reasons for this: • I'm finding testing hard. • The ad hoc "find what your project needs" ecosystem is hard to navigate • I am often running into nil arguments or badly formed maps, and I haven't found a good pattern to quickly find these bugs yet. • If.I am using my app and something doesn't work right, I find it hard to get from "Let's find out what arguments I sent to the server" to "Let's isolate the problem function and pass these arguments' • Managing state in Clojurescript seems really difficult to do right, as it can be in React. I could use general advice for writing safe, scalable code. Right now my app is pretty simple and it's still complicated to me. I'm enjoying the language, but I'm not sure I feel more productive than I would with node/react or something

pavlosmelissinos10:11:43

Can you share any of your code? It might help you get more spot-on pointers. Based on the reasons you mentioned: > I'm finding testing hard. What's your setup now and what's the process you're using? What do you find harder about testing in Clojure compared with other languages? > The ad hoc "find what your project needs" ecosystem is hard to navigate Agreed... That's the sole reason I haven't done as much web stuff as I'd like. Clojure is all about good design so you don't regret your decisions later and the web is really complicated and has a lot of moving parts so it's very hard to get right. For instance, rails is very good at covering ~75% of the possible use cases but if you want to get beyond that it makes it very difficult for you. JS is what it is. You have to set aside a significant portion of your time every couple of years just to learn the latest framework. Neither of those is good design imo. Clojure's attempt is different. It builds great solutions for specific problems and then lets you combine them in the way that is best for your project. > I am often running into nil arguments or badly formed maps, and I haven't found a good pattern to quickly find these bugs yet. That hasn't been my experience, do you have a specific example? Again, having some code would help. I mean this kind of bug mostly comes up during development so if you properly validate/coerece your user input asap, it shouldn't be that much of a problem. It helps if you split your code into components (high cohesion, low coupling is a good principle to follow in Clojure as well) and add some light specs to the interfaces of the components. > If.I am using my app and something doesn't work right, I find it hard to get from "Let's find out what arguments I sent to the server" to "Let's isolate the problem function and pass these arguments' This is about minimizing the area of the problem, isn't it? Here's what I do to get there: 1. Reproduce the problem locally, 2. use def a lot to capture the value of arguments before the place that is likely to have the error Then take that def and iteratively go deeper and deeper into the stack until you have the specific thing that causes the bug (this is practically tree traversal, so finding the actual bug is fast, as long as you don't have lots of side-effects here and there)

Valentin Mouret11:11:39

I can say I share some of your concerns, and it’s definitely not easy to navigate the ecosystem at first. It takes time and experimentation, but it’s definitely worth it in my opinion, and the pain-points you mention are actually solved by Clojure strengths. They are quite dissimilar to other programming languages, so it will take some time to get used to them. 1. Testing is hard With Clojure and REPL driven development, you should find you have fewer tests to write. Functions are smaller, pure, do one thing, and you use them all the time from the REPL. Thus, tests can focus on being integration tests. 2. The ecosystem is hard to navigate Definitely agree. Also, I find myself patching together many components, which can be a bit complex. Maybe you should use a framework like https://luminusweb.com for now? It could remove some complexity and allow you to feel more productive. You can still dive deeper later. :) Note that I did not try it myself. 3. Maps and keywords Yeah, that’s an area I currently find myself navigating as well. It’s part of the tradeoff for a dynamic language. https://clojure.org/guides/spec and/or https://github.com/metosin/malli can help and I found them to be quite powerful. For instance, when I develop my web-app, I get informed with a browser alert every time the state of my frontend is inconsistent. I can also generate random states for tests. Also, fully qualified keywords are better handled by the editors (renaming, used/not used, unknown). E.g. ::my-keyword instead of :my-keyword 4. Debugging is hard Actually, I find it’s one of Clojure’s strength. The application is running and I can access it with the REPL. It’s then easy to go from the outside in to find the issue. It’s all data, so it’s easy to inspect. This means unlocking another of Clojure’s strenth, which is REPL driven development. If you did not experiment with it, you are in for a treat. :) 5. Managing state in ClojureScript Give a shot at re-frame. I am using it for my web apps and I never had to worry about state. With shadow-cljs, it becomes great because the app can reload when the source changes but the state is preserved and observable from the REPL. E.g.: (:some-key @re-frame.db/app-db) That can seem overwhelming with many things to learn. To me, it’s well worth the tradeoff. At first, maybe using luminus can help you go faster. Try this video for a glimpse of what’s possible: https://vimeo.com/230220635

kennytilton15:11:57

(a) What is your current stack? (b) For debugging I just use loads of print statements. I never step/inspect. Too slow. (c) "nil arguments and bad maps": yep. My strategy in coding is to throw sanity checks right at the start of many functions. Just bang them in and write my function. These will catch all sorts of bugs early, before they lead to a mystery further downstream. If you like extra work, use spec to help create these assertions. (d) as for state, again, what are you using now? hth!

Benjamin C19:11:01

With regard to debugging and testing, I've found https://github.com/AbhinavOmprakash/snitch and https://github.com/hyperfiddle/rcf to be incredibly useful.

Dallas Surewood20:11:47

Thanks for the responses everyone. Here is a bit about my project: 1. Character sheet handler for a tabletop rpg 2. Backend: a. Reitit/Ring b. Buddy auth c. JDBC to postgres d. HugSQL for queries (not sure how to test these either) e. integrant for component system 3. Frontend: a. Rum b. Accountant/Secretary for routing c. Just using atoms with rum cursors to manage state for now This would be an example of how I pass a route to reitit ["/login" {_:get_ (fn [req] (login/login req query-fn))}] I'm not using coercion right now. Maybe I should? I did use a spec/valid? call in (login) to validate it. Here is an example of all the code involved in the login route. I have tried to keep all my "side-effectful" code in the top function, and then just passing data to the actual business logic of the app. Always trying to keep business logic as pure as possible. I was told in functional programming, it's better to fetch data at the top level or as close to the top. rather than pass query functions down the call stack. You'll also notice I have login! and -login!. It was just easier to reload code by having -login! be the "real' function.

(s/def ::email string?)
(s/def ::password string?)

(s/def ::user
  (s/keys
   :req-un [::email ::password]))

(defn db-get-user! [repo email]
  (let [user (repo :get-user-by-email {:email email})]
    (select-keys user [:id :password])))

(defn validate-login [password encrypted-password]
  (if (hashers/check password encrypted-password)
    false
    "Login failed. Are you sure you have the right email and password?"))

(defn add-identity-to-session
  [session identity-value]
  (assoc session :identity identity-value))

(defn add-session-to-response 
  [response session]
  (assoc response :session session))

(defn respond-to-login [validation-error user-id session]
  (if (not validation-error)
    (let [new-session (add-identity-to-session session user-id)]
      (-> (http-response/ok new-session)
          (add-session-to-response new-session)))
    (http-response/forbidden validation-error)))

(defn -login! [form session repo]
  (let [{database-password :password
         user-id :id} (db-get-user! repo (:email form))
        input-password (:password form)
        validation-error (validate-login input-password database-password)]
    (respond-to-login validation-error user-id session)))

(defn login! [request repo]
  (if (s/valid? ::user (:params request))
    (-login! (:params request) (:session request) repo)
    (http-response/bad-request)))
I have functions like add-session-to-response and add-session-to-session that are sort of operating on my "entities," but I just feel there's no real layers being established here. Would I put these kinds of functions in an "session" and "response" layer so they can be used in other layers easily? I like the idea of stratified design, but it's a bit confusing sometimes. How often am I gonna want to add session to a response in this exact same way? I don't want to over abstract. I did respond-to-login because I wanted to get the business logic out of -login! which was impure The query-fn is just an integrant component (JDBC connection)

Dallas Surewood20:11:58

@U043A78TYQK I would love to know how you are getting your browser to tell you when your state is wrong

Valentin Mouret21:11:22

Well, I am using re-frame where the whole state is in a big map. I use Malli to create a spec for it. Every function that updates the state is in a single file by convention, and after they do their stuff, they check the state is valid. On dev, if it invalid, it makes a browser alert.

Valentin Mouret21:11:21

Not sure it’s a great idea, but so far so good. :)

Dallas Surewood08:11:19

@UEQPKG7HQ Did you have any thoughts on the code I shared?

pavlosmelissinos08:11:07

Didn't have the chance to check it out yet, sorry. Let me take a quick look...

pavlosmelissinos09:11:18

> HugSQL for queries (not sure how to test these either) First of all, you should have a dev database (you can easily run a docker container with postgres, that's what we're doing) For testing I think most write a function that wraps the db operation in a transaction that will always rollback after it completes This is with clojure.java.jdbc, should be similar with next-jdbc or whatever you're using:

(defn with-doomed-trans [f]
  (let [conn (test-db-conn)]
    (jdbc/with-db-transaction [trans conn]
                              (jdbc/db-set-rollback-only! trans)
                              (f trans))))
> I have functions like add-session-to-response and add-session-to-session I don't think these add a lot of value, I'd ditch them > You'll also notice I have login! and -login!. It was just easier to reload code by having -login! be the "real' function. I don't get this! Other than that, Slack is not the best place for a code review 🙂 I'm not sure I'll be able to put a lot of time into a review right now but you might get some more feedback if you put the code on github and ask again on #code-reviews

slk50012:11:39

Noob question: I'm reading Programming Clojure 3rd edition. In this book there's definition: "a function call is simply a list whose first element resolves to a function." I really don't like/get that word 'resolves' could it be just 'is a function?' -> full sentance "a function call is simply a list whose first element is a function."

skylize12:11:13

Not quite the same thing. A list is just a list. Not relevant whether it starts with a function until the evaluator attempts to call it as such.

(let [foo '(+ 1 2)] ; <- this is a list
  (eval foo))       ; => 3
  
(let [bar '(1 2 3)] ; <- also a list
  (eval bar))       ; 💣 Tries to cast a Long to IFn
                    ; so it can call it.

👍 1
Alex Miller (Clojure team)12:11:04

“resolve” means to evaluate a symbol in a namespace context, yielding a var. The var’s value is a function that can then be invoked

👍 2
slk50013:11:55

that's nice explanation what's happening under the hood, but it's too complicated with unnecessary details for a simple introductory definition of a function call

slk50013:11:44

'a function call is simply a list whose first element is a function."' -> can this statement be denied ?

skylize13:11:13

Eventually it becomes important to understand how symbols and vars and resolution work in Clojure. Choosing to say "resolves to" instead of "is" primes you to recognizing such distinctions, for when you start to care about such things.

slk50013:11:12

(let [foo '(+ 1 2)] ; <- this is a list
  (eval foo))       ; => here is function call
  
(let [bar '(1 2 3)] ; <- also a list
  (eval bar))       ; => here is function call - which throws an error

skylize13:11:38

The evaluator throws an error because it tries to resolve the number 1 to a function and fails because 1 does not point a function.

skylize13:11:11

(let [foo '(1 2 3)       ; A list. We haven't tried to resolve anything.
      bar (cons + foo)]  ; We can still change it before passing it to evaluator.
  (eval bar))            ; => 6

slk50013:11:46

hmm thank you for explanation

delaguardo13:11:56

btw, why do you think "resolves to" is incorrect and should be changed to "is"?

slk50013:11:32

I'm not saying is incorrect. Just maybe to broad & foggy for a newbie reader

slk50013:11:19

I get your point,

slk50013:11:09

But it same manner I could write adding is adding values which resolves to numbers e.g (+ 2 (Integer/parseInt "3")). Or I could just write adding is adding numbers.

delaguardo13:11:06

there is also another reason: in clojure some datastructures can implement IFn protocol and resolve to a function ({:foo 1} :foo) — this is a function call but {:foo 1} is not a function

👀 1
metal 1
Alex Miller (Clojure team)14:11:17

@UPBB20W20 you're making a good point too. as one of the authors of the book, there is always a pedagogical choice to make about how of much the real model is useful / necessary at a particular time. I don't know if there actually is a "right" answer that works for everyone everywhere, but perhaps this is a place where the simpler lie is the more useful one at that point in the book, not sure. we are still glossing over lots of details (things that eval to a function, macros!!, inlining, direct linking, etc)

👍 2
Alex Miller (Clojure team)14:11:01

and all of these avoid the most interesting (but sophisticated) point here which is that function invocation in Clojure is itself an abstraction

slk50014:11:30

@U064X3EF3 you are right. that's a really gentle & correct explanation

skylize14:11:54

@U064X3EF3 I'm guessing this is supposed to say "\space"? > There are a few special named characters: \newline \spec \tab, etc. https://clojure.org/guides/learn/syntax#_character_types

kennytilton14:11:43

Haha, in another thread we are batting around the quality of a particular exercise on a Clojure tutorial site. These questions are super hard, and looking at my own doc, I can say I sure do not have the answer. I find I lack the ability to leave the deeper dive for another day. I also like to be super precise with language to avoid hand-waving or miscommunication. So "resolve to" seems like a nice compromise, tho I see your point. Wait till you get to Common Lisp! :rolling_on_the_floor_laughing:

slk50015:11:02

btw. anyone have some articles, books about 'how to write tutorial?, book? how to explain'.

kennytilton17:11:17

Wow, google has a ton of hits. I picked this https://www.learnenough.com/tutorial-writing-tutorial at semi-random and was delighted to find their tutorial on why tau is a better circle constant. I think Vi Hart nailed it, too. Anyway, before I googled I was going to suggest finding doc you like and emulating that. Speaking of which, re-frame has excellent doc.

❤️ 1
bschrag13:11:16

I have a macro that's working fine at the repl, but I get a syntax error when I try to evaluate my test. Any insights would be appreciated. At the repl: template-matcher.core> (with-matching-template-bindings ["*foo" "bar"] *foo) "bar" From core_test.clj: (deftest with-matching-templating (testing "with-matching-template-bindings" (is (= "bar" (with-matching-template-bindings ["*foo" "bar"] *foo))) )) When I try to evaluate that (via Cider C-M-X): Syntax error compiling at (/home/bschrag/C_home/code/Cogex/template-matcher/test/template_matcher/core_test.clj:64:18). Unable to resolve symbol: with-matching-template-bindings in this context Other tests that call functions (not macros) are working fine. Any tricks to writing tests for macros?

delaguardo13:11:26

how do you require the namespace which define this macro in your test namespace?

bschrag14:11:01

(ns template-matcher.core-test (:require [clojure.test :refer :all] [template-matcher.core :refer :all]))

delaguardo14:11:56

then it should work. Try to reload ns definition for the test namespace

bschrag14:11:23

Bingo! Thanks... 🙂

delaguardo14:11:12

btw, I personally try to use :refer :all as less as possible. In many cases having an alias will help you refactor, debug and navigate your code in the future.

delaguardo14:11:08

this also highlighted as warning by default by the tools like clj-kondo and clojure-lsp

bschrag14:11:02

Okay. I will have to learn about those. I'd just been mimicking some tutorial code.

2FO17:11:00

Howdy, Is there a way to enable the pprint and doc functions in rebel-readline?

delaguardo18:11:00

you can create or adjust user.clj file to have something like (ns user (:require [clojure.repl :refer [doc]] [clojure.pprint :refer [pprint]])) this namespace always load when present in class-path.

🙏 1