Fork me on GitHub
#clojure
<
2019-12-03
>
yannvahalewyn00:12:55

I’m taking up the habit of writing a “tip of the day” on our office’s whiteboard. We have a lot of juniors, so elementary tips are most welcome. What are your “tip of the day” topics you would’ve liked to see when you were starting out?

seancorfield00:12:44

with-open and lazy sequences do not mix 🙂

yannvahalewyn00:12:28

definitely! someone actually ran into that last week.

yannvahalewyn00:12:55

avoid side effects in lazy sequences is a good tip of the day for us

jumpnbrownweasel00:12:58

Isn't the with-open issue not just about side effects, but more generally that a lazy sequence can't be returned out of the middle of a binding block, when materializing the sequence will require those bindings?

didibus01:12:37

No, it's not related with bindings

didibus01:12:01

The issue is that with-open closes the file before the lazy sequence is realized

didibus01:12:47

But, bindings have a similar effect, in that the bound variable is released before the lazy sequence had a chance to use it

didibus01:12:23

That's only true of you capture the binding, but if you capture the bound value you're good

jumpnbrownweasel01:12:39

Yes, you're right, that's what I was trying to say.

jumpnbrownweasel01:12:07

Both of them create a scope, outside of which you'd better not reference the bound vars, and lazy sequences make it easy to make that mistake.

didibus01:12:57

But still, the end result is that lazy sequences shouldn't be used with side effects. That means they shouldn't use any global var, be them dynamic or not. And they shouldn't use a file either or anything else that's a side effect

jumpnbrownweasel01:12:35

If they're realized before exiting the scope, why not?

jumpnbrownweasel01:12:43

I still think the problem is when they escape the scope.

didibus01:12:12

Well, carefully using them sure. But, what you're effectively describing is using lazy sequence eagerly :p

didibus01:12:29

Since you're going to realize them before they are needed

didibus01:12:39

So I find learning to use the eager fns instead for side effect is a good practice

didibus01:12:02

Those would be: doseq, dotimes, run!, mapv, filterv, etc.

didibus01:12:12

And transducers as well

didibus01:12:58

It's fine to wrap a doall around a lazy-seq too, assuming you're careful that nested lazy seqs are also wrapped and all that

jumpnbrownweasel01:12:21

Yes, that's the kind of thing I was referring to.

didibus01:12:12

But, all I can say is, when I was a more beginner Clojure dev, I'd use a ton of doall, and now I don't think I used a doall in the last year

didibus01:12:22

Same goes for dorun

didibus01:12:59

Nothing wrong with them, but I guess I just got the instinct to use other things instead

jumpnbrownweasel01:12:53

I understand, thanks.

didibus01:12:45

I guess I also just changed the way I code in general as to minimize and isolate most side effect, so that probably contributes to me not using those as much

jumpnbrownweasel01:12:57

Recently I've been using run! as he recommends. But, for example, sometimes I use filter on the source collection, which returns a lazy seq. So I am using a mixture of lazy and strict, which doesn't seem all that bad.

didibus06:12:14

On a side effecting source collection?

jumpnbrownweasel15:12:48

No, the source is just data but may be a lazy seq. My point is just that it is not specific enough, and could be confusing to beginners, to say that you can't mix side effects with lazy sequences. I think what would be useful is a more specific description of what to avoid. For example, if we could define exactly what you mean by "side effecting collection", in terms a beginner could understand, that might help. But I think that is actually not so easy to understand. IMO even beginners need to read about and understand laziness and its pros and cons, as described in Stuart's page for example. A one liner that just says "don't mix them" I think isn't enough.

jumpnbrownweasel15:12:00

Or at least, it wasn't enough for me as a beginner.

jumpnbrownweasel15:12:51

Maybe I'm wrong, maybe it is enough to say: Don't use any function that produces a lazy sequence to perform side effects. For example, functions passed to map should not perform side effects. However, lazy sequences may be used as inputs to functions such as run! that do not produce a lazy sequence themselves, and the function passed to run! may perform side effects.

jumpnbrownweasel15:12:13

Almost one line, if that is really enough as a rule of thumb.

didibus23:12:55

I can see how even understanding what a side effect is can be challenging to a beginner at first

didibus23:12:52

And understanding what a lazy seq is as well

didibus23:12:42

Ya, you may be right. Don't do any side effects inside the function passed to the sequence fns such as map, reduce, filter, remove, etc.

didibus23:12:18

That might be clearer. Where a side effect is making use of any global variable, of any shared mutable container, data-structure, object or binding, and of any IO operation.

jumpnbrownweasel05:12:32

Yeah, that seems pretty clear.

jumpnbrownweasel00:12:01

Seems like anything that does bindings is a little dangerous and requires special care.

jumpnbrownweasel00:12:41

Using lazy sequences inside the block seems fine, as long as you materialize them inside. It's when they escape the block that there is a problem.

noisesmith00:12:19

@mark540 the way it's usually stated is that mixing laziness and IO is the problem

Alex Miller (Clojure team)00:12:33

I think really understanding lexical scope vs dynamic scope vs whatever laziness is (deferred realization) is at the root of many, many misunderstandings in Clojure

Alex Miller (Clojure team)00:12:14

and separately what happens at read vs compile vs macroexpand vs runtime vs print

jumpnbrownweasel00:12:57

I've been working in Java for some time and was surprised to see how often dynamic bindings are used in Clojure, since I consider them the equivalent of thread locals in Java, and although very useful I've always tried to avoid using them as a shortcut to arg passing. But I think I understand that they're traditional in Lisp, and when made very explicit they're OK. Still, I try to avoid them except in certain cases.

jumpnbrownweasel00:12:59

I should clarify. Bindings are not equivalent to thread locals, it's just that thread locals can be used to implement bindings. And I think thread locals are better used for caching, for example, rather than to avoid arg passing.

lilactown01:12:06

the majority of libs never use bindings

lilactown01:12:16

it’s fallen out of favor in recent years I believe

lilactown01:12:27

(I say this as I’m writing a lib that uses binding…)

Alex Miller (Clojure team)01:12:23

You should never use dynamic vars. Except when you do.

😆 16
💯 4
alexbaranosky10:12:22

There is great wisdom in this one.

vemv02:12:56

dyn vars remain my guilty pleasure... next time the need arises I'll try to think hard what a proper alternative would be (without introducing a new function argument and passing it around a gazillion defns) one time we replaced them with a new field in a specific defrecord (Component pattern). Another possibility would be something like https://github.com/steveklabnik/request_store (s/Rack/Ring)

potetm03:12:52

just pass in fn args

potetm03:12:07

it works super duper well

potetm03:12:01

only real excuse I can think of is, “Ok. I’m writing a lib. I want to make a non-breaking change, and dynavar is the only way forward.”

potetm03:12:17

even then, I believe the word might be, “hack”

potetm03:12:26

iow - consider making a new fn

seancorfield03:12:20

As someone who inherited a library based on dynvars (`clojure.java.jdbc`) and eventually steered it to a library that is not based on dynvars, I'll echo everything @potetm said: Just. Don't. /cc @vemv 🙂

❤️ 4
vemv03:12:05

Sure thing, https://stuartsierra.com/2013/03/29/perils-of-dynamic-scope is well kept into account But lib != app, particularly a large one that might have inherited some complexity In an app, passing args around can mean "couple everything to everything else", so things like Component or request storage can shine

alexbaranosky10:12:32

Technically, often the code is coupled either way. It's just a question of whether the dependency is clear or hidden. Dynavars don't remove the fact that one piece of code is dependent on the other.

vemv10:12:49

having a chain of N namespaces :require ing each other is a form of coupling, that is forced when going the defn route Component or request storage (those were my words, not "dynavars") allow ns's to not depend so strongly on each other, allowing certain modular designs

potetm03:12:38

imo apps have even less excuse for this kind of brouhaha

potetm03:12:24

usual caveats: mistakes, time, priorities, etc etc

👍 4
potetm03:12:16

component pattern is the opposite of dynabinding

vemv03:12:59

sure, make sure to understand my OP

potetm04:12:27

Yeah, I thought you might already understand. Wanted to make it clear.

👍 4
potetm03:12:47

request storage seems like a wonderful way to make a mess 🙂

potetm03:12:24

usual caveats: mistakes, time, priorities, etc etc

👍 4
dominicm06:12:27

I think the pattern that is attempted to be avoided is when you have layers of code (let's use system -> view -> controller -> model for arguments sake). If you add a new thing to your system that a model function needs, then the view, controller and model function need an additional argument of "foo" to be able to pass it down to the model.

☝️ 4
dominicm12:12:18

To be clear, I'm not certain it's a bad thing that they all need updating. I'm just trying to articulate the problem in order to push discourse forward.

dominicm12:12:37

One solution to this is stamp coupling, but I find the vagueness of that very confusing.

potetm13:12:58

Or, don’t cut arbitrary lines in your code for no concrete gain?

dominicm13:12:04

They somewhat exist no matter what :). You have to cut your code somehow.

potetm14:12:53

I would argue, for the case of a standard webapp, you basically need: 1) a handler fn for a route, 2) some helper fns. Everything else (e.g. “domain”, “service”, “controller) is an arbitrary, and unhelpful, cut.

4
dominicm16:12:19

Where does a query for a user go? Request function or elsewhere?

dominicm16:12:52

How do you divide your handler functions up? Where do they live? By what they operate on? What if they operate on multiple Entities?

dominicm16:12:40

(these are genuine questions which I'm in constant pursuit of novel answers too, as I think they help define a strategy very well.)

potetm17:12:08

Query for a user: call a shared fn & pass the data

potetm17:12:26

How you divide up handler functions is just code shuffling.

potetm17:12:15

The only impact it has is, “How fast can I find the fn I’m looking for?” Keep shuffling them until the answer to that is, “It doesn’t bother me.”

potetm17:12:18

As long as handlers aren’t co-dependent (don’t do that), they can be moved ad nauseam with literally zero impact (negligible cognitive overhead, zero runtime impact).

dominicm10:12:54

Do you have one big namespace with everything in? How do you decide when to put something in a different namespace?

potetm18:12:02

Seems fine to start with one big one.

potetm18:12:14

I make a new one whenever it feels right 😄

potetm18:12:48

I know that’s not very satisfying. But it’s important to remember: this will not make or break you. It’s arbitrary, and it’s super easy to change.

dominicm19:12:00

What I'm confused about is, what division would you use?

potetm22:12:24

I’m saying: first and foremost, it doesn’t matter

potetm22:12:18

after that, like, entity/similar endpoints/whatever-feels-reasonable

potetm22:12:33

The entire point of shuffling is to find things faster. So try to keep an eye on that and update when it “feels slower.”

dominicm07:12:04

Interesting. This aligns quite well with something I have been thinking about, that I think is relevant to this decision. Your unit of modularity is the function, not the namespace. (ish).

potetm14:12:32

obviously a lot more goes into real modularity

yannvahalewyn06:12:06

> What are your “tip of the day” topics you would’ve liked to see when you were starting out? Interesting stuff ofc, but more various tips are welcome!

amalantony14:12:37

Hey guys, I'm fairly new to Clojure. I come from a full stack Javascript background. I'm planning on building a production REST API in Clojure and am interested in knowing what's the standard libraries/frameworks that's mostly used by Clojurians for this purpose. In my initial research I see a lot of options like Ring/Compojure, HTTPKit, Pedestal etc. Which one of these are the most prevalent and maintained? In the Node.js world, Express is what most people use to build web backends. Is there an equivalent in Clojure?

rutledgepaulv14:12:35

Equivalent is probably ring/compojure. Some people outgrow those and move to more data driven things like pedestal or compojure.api or yada. Others keep using the basics of ring but maybe swap routers to something like reitit

rutledgepaulv14:12:04

Httpkit is just an implementation of a ring server. The most popular is embedded jetty but some people opt for undertow, httpkit, tomcat, etc. I'd start with jetty if you're not aware of a reason to choose something else. The jetty implementation is bundled with ring by default

👍 4
amalantony15:12:07

Thanks for the helpful summary @rutledgepaulv. What is the state of things on the Database/ORM side? What libraries should I be looking at? My REST API would mainly be doing CRUD operations on Postgres for the moment.

cddr15:12:12

If it really is just CRUD, have you considered https://postgrest.org/en/v6.0/. Apologies for the clojure person years I've just destroyed but I'd still hire them and get them doing more interesting stuff.

Joe Lane16:12:12

@U20DT8LSE My recommendation is to use https://www.hugsql.org/ for clojure + sql. Use sql for what it's good at, then use clojure for dynamically generating functions to call your sql expressions. Your DBA's will love you, and you will be doing basically vanilla SQL + Comments, and you can leverage the full power of the SQL database of your choice (PG in this instance) without fighting an ORM (One of the many classic ORM problems)

👍 8
cddr16:12:18

This is actually very topical at the moment as I think it's basically what @U050CTFRT was talking about yesterday at re:clojure. We shouldn't be making "artisanal, hand-crafted CRUD APIs", for each company who needs one. We should be writing (or using) an API that can be put in front of any database (or rather any instance of a database of a specific type).

4
Tamizhvendan S08:12:33

@U20DT8LSE Toucan + Honey SQL

👍 4
Tamizhvendan S08:12:14

I have used it in couple of my projects and it turned out well..

Ahmed Hassan08:12:28

Simplfied EDN Query Language for SQL. https://github.com/exoscale/seql Turn Clojure data structures into SQL. https://github.com/jkk/honeysql

dharrigan14:12:34

I built a production-able RESTful service using reitit. Works great. https://metosin.github.io/reitit/

👍 8
dharrigan15:12:58

ORMs aren't really a thing in Clojure-land.

Prometheus16:12:58

Hey quick question. When returning an empty list with ring, why does the return type become octet stream, event though I have wrap-json-response middleware?

Prometheus16:12:54

*content-type

Prometheus16:12:52

(def app
  (-> (wrap-defaults app-routes api-defaults)
      (wrap-cors :access-control-allow-headers [:accept :origin :user-agent :x-request-with :cache-control :cookies]
                 :access-control-allow-credentials "true"
                 :access-control-allow-origin [#"^(http(s)?://)?localhost:(\d){4}"]
                 :access-control-allow-methods [:get :put :post :options :delete])
      wrap-cookies
      ;; print-request
      (wrap-authentication backend)
      (wrap-authorization backend)
      logger/wrap-with-logger
      (wrap-json-body ,,, {:keywords? true :bigdecimals? true})
      wrap-json-response))

seancorfield16:12:44

Probably because something in there calls seq on the response (which returns nil from ())?

seancorfield16:12:40

You could try explicitly returning [] (empty vector) to see if it special-cases vector? before it calls seq (just guessing).

Prometheus17:12:02

I explicitly returned [] from the function , and in both cases the route/function returns [ ] with application/octet-stream

Prometheus17:12:37

(defn get-users-events
  [request]
  (if (authenticated? request)
    {:status 200
     :body (json/encode (find-users-events
                         (:user
                          (:identitiy request))))}
    (throw-unauthorized {:message "unauthorized"})))

seancorfield17:12:36

If you return a non-empty vector, does it work as expected? (to confirm whether the empty sequence is a special case)

Prometheus17:12:47

it does not, it returns the vector (not empty) but still has content-type octet stream

Prometheus17:12:18

this is mysterious, because I have another resource that returns json, and they are the same

Prometheus17:12:02

hmmm, it looked into it, and the other resource also returns octet stream

Prometheus17:12:48

After looking closely at the other resource I finally got it to work, the issue was that the frontend go-block was misbehaving, and that was where I was testing the interface. Thanks for the help @U04V70XH6

seancorfield17:12:00

OK, so it's not related to being empty. Ah, OK, I don't think wrap-json-response actually sets the content-type maybe? I seem to recall running into this with some of the middleware and being surprised the content-type is not set automatically...

Prometheus17:12:24

I didn’t get it to set content-type

Prometheus17:12:43

hmmm, so how would you recommend setting the content-type?

seancorfield17:12:50

Looking at wrap-json-response it only puts application/json as the Content-Type if it isn't already set...

seancorfield17:12:20

...so something in the middleware stack is setting the content type for you (probably in the default middleware? And by the time wrap-json-response gets control, there's already a content type...

Prometheus17:12:39

I’m a bit new to the language 😅

(def app
  (-> (wrap-defaults app-routes api-defaults)
      (wrap-cors :access-control-allow-headers [:accept :origin :user-agent :x-request-with :cache-control :cookies]
                 :access-control-allow-credentials "true"
                 :access-control-allow-origin [#"^(http(s)?://)?localhost:(\d){4}"]
                 :access-control-allow-methods [:get :put :post :options :delete])
      wrap-cookies
      ;; print-request
      (wrap-authentication backend)
      (wrap-authorization backend)
      logger/wrap-with-logger
      (wrap-json-body ,,, {:keywords? true :bigdecimals? true}) wrap-json-response))
This is my middleware, do you see anywhere that might cause the issue? 🙂

Prometheus17:12:09

maybe setting wrap-json-response before wrap-cors?

seancorfield17:12:57

wrap-cors should almost certainly be on the outside of the stack but it's wrap-defaults that is setting the content type for you.

Prometheus17:12:58

that didn’t work…

seancorfield17:12:41

Middleware ordering is a thorny problem.

Prometheus17:12:12

haha yeah I’ve had my fair share of frustrations these past few weeks, I must admit 😅

seancorfield17:12:14

If you are only returning JSON, you can put wrap-json-response closer to your handler than wrap-defaults

Prometheus17:12:24

good idea 🙂

seancorfield17:12:49

I would put the json body / response at the top, then defaults, then auth, then logging, then cors.

Prometheus17:12:52

hmmm still doesn’t set the content-type

seancorfield17:12:10

Also note that defaults includes wrap-cookies already.

seancorfield17:12:35

Paste your current middleware stack/wrapping...

Prometheus17:12:08

(def app
  (-> (wrap-json-response app-routes)
      (wrap-json-body ,,, {:keywords? true :bigdecimals? true})
      (wrap-defaults ,,, api-defaults)
      ;; print-request
      (wrap-authentication backend)
      (wrap-authorization backend)
      logger/wrap-with-logger
      (wrap-cors :access-control-allow-headers [:accept :origin :user-agent :x-request-with :cache-control :cookies]
                 :access-control-allow-credentials "true"
                 :access-control-allow-origin [#"^(http(s)?://)?localhost:(\d){4}"]
                 :access-control-allow-methods [:get :put :post :options :delete])))

seancorfield17:12:05

I would add some logging middleware between wrap-json-body and wrap-defaults so you can see what headers are set at that point...

seancorfield17:12:49

That order looks pretty similar to our order at work for an API that always returns JSON...

Prometheus17:12:06

haha, then it must be something I’m doing wrong outside of the middleware

Prometheus17:12:03

I’ll publish the calls all the way down to the db call

Prometheus17:12:53

This:

(GET "/event" request (get-users-events request))
calls:
(defn get-users-events
  [request]
  (if (authenticated? request)
    {:status 200
     :body (json/encode (find-users-events
                         (:user
                          (:identitiy request))))}
    (throw-unauthorized {:message "unauthorized"})))
Which in turn calls:
(defn find-users-events
  [user-id]
  (let [response (db/lookup-a-users-events user-id)]
    response))
And then lastly calls:
(defn lookup-a-users-events
  [id]
  (jdbc/query psql-db
              ["SELECT * FROM event WHERE owner = ?" id]))

seancorfield17:12:09

Ah, you are not letting wrap-json-response handle the JSON conversion. You're doing it yourself, explicitly.

seancorfield17:12:47

You should not need that json/encode call -- that's the whole point of wrap-json-response

seancorfield17:12:16

Has that fixed it?

Prometheus17:12:37

(the irony of writing perfectly imperfectly)

seancorfield17:12:29

https://github.com/ring-clojure/ring-json/blob/master/src/ring/middleware/json.clj#L115 -- wrap-json-responseonly takes action if the response is a collection (not a string, which is what json/encode returns).

Prometheus17:12:57

I really like how helpful the clojure community is. Thank you @U04V70XH6

restenb17:12:45

trying to update an older project from 1.8.0 to 1.10.0 and it's a major PITA

restenb17:12:59

:message Could not locate clojure/spec__init.class, clojure/spec.clj or clojure/spec.cljc on classpath.

restenb17:12:18

it compiles but REPL won't start properly due to the above error message

noisesmith17:12:09

when was clojure.spec a thing? was there a clojure.spec -> clojure.spec.alpha migration?

restenb17:12:38

really not sure what this error is trying to tell me

restenb17:12:51

i did update to latest version of core.async as well just in case

noisesmith17:12:52

there's no such ns as clojure.spec, the ns is clojure.spec.alpha

Alex Miller (Clojure team)17:12:13

spec was added as a dep of Clojure in 1.9.0

Alex Miller (Clojure team)17:12:42

if you could share a full stack trace, might be able to help pinpoint the issue

Alex Miller (Clojure team)17:12:25

but agree with noisesmith that something is trying to load clojure.spec namespace, which is not a thing

noisesmith17:12:30

maybe some lib was using a pre-release of clojure.spec that did have a clojure.spec ns?

restenb17:12:51

well I have some requires lying around on clojure.spec still so most likely just that

Alex Miller (Clojure team)17:12:00

that ns did exist in 1.9 alphas of Clojure but I'm not aware of anything out there using those

restenb17:12:59

what about old clojure.spec.test? clojure.spec.test.alpha now?

Alex Miller (Clojure team)17:12:56

and clojure.spec.gen.alpha

leira20:12:43

I just installed clojure on NixOS using nix-shell -p clojure. I ran clj, it downloaed some packages, and started the repl successfully. It created a ~/'?'/.m2 folder to hold the packages. My question is, is this ~/'?' folder normal?

noisesmith21:12:44

if it helps, the standard location for the cache is ~/.m2/repository - maybe it's doing something fancy with HOME?

justinstoller22:12:55

Maybe someone fat fingered it? I always accidentally type ~?.m2

sogaiu20:12:21

that doesn't sound normal

sogaiu20:12:45

at least on other operating systems, i don't think that's typical

sogaiu20:12:14

may be https://discourse.nixos.org/ could be a helpful place to ask?

leira20:12:35

Well, I know it is not a typical setup, Nix over Debian linux.

leira20:12:43

Will ask there, thx~

sogaiu20:12:49

good luck 🙂

g20:12:59

is it considered bad practice to try and deliver a lazy sequence through ring’s piped input stream? is there any guarantee this will realize it?

sogaiu20:12:18

i don't know the answer to your question, but there is a #ring, fwiw

g20:12:34

thanks!

borkdude23:12:22

why doesn't this produce #'foo/x?

$ clj -e "(ns foo) (ns bar) (binding [*ns* (find-ns 'foo)] (def x 1))"
#'bar/x

hiredman23:12:43

you can imagine def to be a macro like

hiredman23:12:52

(defmacro defx [name value]
  (declare name)
  `(intern (find-ns '~(ns-name *ns*)) '~name ~value))

hiredman23:12:08

binding is a runtime effect, def determines the name to be defined at compile time

noisesmith23:12:37

this is why (defn f [] (def foo 1)) creates foo in the current ns, with value unbound

borkdude23:12:49

btw, if you want to kill an existing REPL, an effective way is:

(set! *ns* (find-ns 'barfoo))
nil
Execution error (NullPointerException) at java.util.concurrent.ConcurrentHashMap/get (ConcurrentHashMap.java:936).
null