Fork me on GitHub
#beginners
<
2020-12-23
>
Ty00:12:29

Something at work that's been really painful is people breaking staging by simply not testing their code properly. I want to use lisp to generate some integration tests that verify the status of the API. Using JS the tests really aren't too bad but it's a ton of boilerplate. I'd love to write a DSL or something in Lisp to generate the integration tests. The end result will have to be working javascript code, though. Could anyone point me in the right direction? This is an opportunity for me to learn clojure better, solve a problem that will make my life easier when I get back to work in jan, and potentially convince my CTO that clojure is a real thing. Someone pointed me at parenscript. That's not really what I'm looking for. I don't want to write the tests in lisp and transpile them, I want to write lisp code that generates javascript code that does the testing for me.

seancorfield00:12:19

@tylertracey09 If you can describe the sorts of actions or sequences of actions that need to be exercised by the tests, you could probably write Clojure Specs for those actions. Then you could use s/exercise to generate random, conforming sequences of actions. Then you'd just need code that took each action as input and wrote out the JS equivalent to taking that action against the API.

seancorfield00:12:12

We do something similar at work for some of our apps: we write Specs for "user actions", use those to generate sequences of "pretend users", and then we have an interpreter that runs those actions against the application being tested. That last stage for you would be to generate JS that ran those actions instead of just running the actions directly.

Ty00:12:36

That last part is the one I'm least mentally prepared for right now. Is there an example I can reference that shows an idiomatic way to generate code in other languages? Or rather just generating larger things from templates of some kind? Ideally I would want to write little templates and replace parts of them rather than write long string things in lisp itself.

seancorfield00:12:43

It's something that Eric Normand covers in his three "Property-Based Testing with test.check" courses at http://purelyfunctional.tv -- $49 / month. Definitely worth the money for at least a month or two while you work through those courses (some of his other courses are great too: his REPL-Driven Development course is worth a month's subscription just on its own).

Ty00:12:01

I guess I could just read the templates from a file and replace strings of a certain format with actual data?

seancorfield00:12:21

We use Selmer for template-driven generation of content -- mostly HTML but you could use it to generate JS too.

Ty00:12:37

Interesting, I'll check into it. Thanks!

Ty00:12:57

Would the specs approach be viable for cljs as well?

seancorfield00:12:20

You mean for generating cljs at the end instead of JS?

Ty00:12:45

No I meant for writing my little program in cljs instead of full clojure

Ty00:12:05

Not that I'm particularly opinionated I just have a clojurescript envrionment/project up and running already.

seancorfield00:12:15

I've no idea how much of the generative side of Spec (and test.check) is available for cljs. I don't do any cljs (just Clojure for a decade).

Ty00:12:04

Gotcha. No worries I think I have some other ideas on that front as well if it doesn't pan out. Selmer looks dead simple, exactly what I needed for that portion.

seancorfield00:12:04

Looks like test.check is written mostly as .cljc so it ought to work for cljs.

seancorfield00:12:48

Selmer is pure Clojure, no cljs. But I think there are similar libraries that work for cljs. At least, I'd expect there to be...

Nassin02:12:56

For a function that takes two args, is it possible to pass a single function call to it and destructure the arguments?

Nassin02:12:46

(defn two []
  [3 4])

(defn add [x y]
  (+ x y))

Nassin02:12:15

(add [[x y] (two)])

holymackerels02:12:07

is what you want calling add with the value in two? it could be apply add (two)*

holymackerels02:12:29

that's equivalent to add 3 4

Nassin02:12:50

that changes the semantics, I want the last call to be add not apply

Nassin02:12:30

I though this was possible with destructuring but don't remember

Nassin02:12:41

going to read up on it again

noisesmith02:12:16

@kaxaw75836 destructuring is a syntactic operation inside bindings (function args, let clauses, for clauses, etc.)

noisesmith02:12:27

it can't be done in arbitrary forms

noisesmith02:12:07

maybe a simpler way to put it: you can only destructure in specific contexts where you give a value a name

noisesmith02:12:36

@kaxaw75836 for (add [[x y] (two)]) to work, it would need to rewrite the args to add before add sees them, and there's nothing (besides reader macros) that does that in clojure, and even reader macros don't turn one form into N forms in the parent

noisesmith02:12:46

macros can rewrite parts of a form before emitting it (so something else with add inside it, like eg. let) can do precisely what you want

noisesmith02:12:59

(let [[x y] (two)] (add x y))

Nassin02:12:14

Ok, I see what you mean, yeah guessing hehe since I don't remember much Clojure

noisesmith02:12:38

it's actually a really great property of clojure that makes the language easier to understand than most

noisesmith02:12:59

(the fact that a child form can never rewrite a parent)

Nassin03:12:04

true, was not thinking this at all, @kyle247 was a good answer for the example I gave but I didn't give any context

didibus06:12:33

Yes, apply was the correct answer. It lets you call a function with the arguments being a sequence instead of having to be explicitly given.

(add (two))
;; Becomes
(add [3 4])
Which can work, but for it to work like that you need to make add into:
(defn add [[a b]]
  (+ a b))
Which I think is what you meant by using destructuring. This solution requires changing the function arguments to take a sequence as the first arg, which you then destructure into your arguments. You can also instead change the call site to do this, and that's done using apply.
(defn add [a b]
  (+ a b))
;; And now if you call it like
(apply add (two))
;; It becomes
(apply add [3 4])
;; Which becomes
(add 3 4)

Nassin02:12:39

yep, that's is exactly how I solved right now, was worried about recur not being in tail position with a let but it didn't complain (let [[x y (add)] (recur x y

noisesmith02:12:01

right, tail positions look recursively into the expression tree

noisesmith02:12:09

you can also recur from eg. a branch of an if

Nassin03:12:04

true, was not thinking this at all, @kyle247 was a good answer for the example I gave but I didn't give any context

andarp06:12:32

I find that when writing functions that are a tad bit complex I tend to use a number of let bindings to process different steps instead of inlining them into one functional pipeline. Is this an acceptable Clojure style? I enjoy constructing the oneliners but I tend to not enjoy reading them after the fact.

phronmophobic06:12:41

I find using let bindings to be great. I do also use the threading macros which have the main benefit of not requiring names for intermediate values.

Michaël Salihi09:12:01

Hello! In the loop documentation page, there is this example:

(loop [xs (seq [1 2 3 4 5])
       result []]
  (if xs
    (let [x (first xs)]
      (recur (next xs) (conj result (* x x))))
    result))
I wonder to know why the vector is transform to a seq. (seq [1 2 3 4 5]) For better performance?

delaguardo10:12:54

because empty vector is truthy, empty seq is not

(if [] 1 2) ;; => 1

delaguardo10:12:17

but I think there is no need to transform vector to seq in loop declaration. it is better to check emptiness of a collection using (seq col)

(loop [xs [1 2 3 4 5]
       result []]
  (if (seq xs)
    (let [x (first xs)]
      (recur (next xs) (conj result (* x x))))
    result))

👍 6
Michaël Salihi10:12:16

Perfect answer! Thx @U04V4KLKC 🙂

Michaël Salihi10:12:13

> but I think there is no need to transform vector to seq in loop declaration. it is better to check emptiness of a collection using Yes tottaly agree. It would have been clearer if it had been at the level of the condition and I would not even have asked about this.

andarp11:12:24

To be even clearer - calling seq on an empty collection returns nil - not an empty sequence

noisesmith16:12:40

calling first or next already invokes seq internally, which is a cached transformation - the seq call in the bindings is redundant but not an extra computation

Michaël Salihi16:12:28

OK, this explains why it works without seq. Thank you for this good information @U051SS2EU!

delaguardo16:12:10

Relying on first and next is not really safe, eg.

(loop [xs []
       result []]
  (if xs
    (let [x (first xs)]
      (recur (next xs) (conj result (* x x))))
    result))
;; => Execution error (NullPointerException) at user/eval138 (REPL:5)
So seq is required somewhere, in bindings or in condition expression

👍 3
noisesmith17:12:08

@U04V4KLKC yeah, I hadn't considered the case where xs isn't a literal, but style wise I would put the seq call inside the if

delaguardo17:12:30

yep, I suggested to do the same

phronmophobic21:12:12

doesn’t having the seq call in the if statement (rather just once at the beginning) just add an additional seq call per iteration? I believe I added the example referenced above based on someone’s advice in this slack. I thought it might have even been @U051SS2EU ‘s idea

noisesmith21:12:00

IIRC the seq of a vector is cached

phronmophobic21:12:09

sounds good. I doubt it makes much of a practical difference. Just wondering for my own edification. the example with the seq call on the outside appears more than once as an example in the docs

noisesmith21:12:30

oh - looking at the source it might not be cached

phronmophobic21:12:13

oh right, that makes sense as if it was cached, you might have extra seqs hanging around in memory after iterating over a vector that is kept around after the iteration.

phronmophobic21:12:23

next returns a seq (or nil if empty) and doesn’t calling seq on a seq just return itself? I think the “extra” seq calls would have a minimal impact

noisesmith21:12:46

(ins)user=> (def v [1 2 3])
#'user/v
(ins)user=> (identical? (seq v) (seq v))
false
the overhead of seq'ing it is low, but it's not cached like I thought it was

👍 6
phronmophobic22:12:33

ah, I was thinking about the second iteration where it will have already been converted to a seq by next:

(let [x (next (seq [1 2 3]))]
  (identical? x (seq x)))
;; true

Carlos Alcantara11:12:43

Hello! If the range function is lazy, why does the following function create an infinite loop in REPL?

(defn even_squares
"Returns a seq of even squares less than the given number"
  [max]
  (for [x (filter even? (range))
        y [(* x x)]
        :when (< y max)] y))
but works fine with iterate inc 0

clyfe11:12:29

:when is a filter not a break; when you take past the condition it enters n infinite loop looking to reify a head

Carlos Alcantara11:12:09

thanks @UCCHXTXV4, I just realized it also doesn't work with iterate inc 0

Carlos Alcantara12:12:55

Could I ask what the idiomatic way to write this function?

clyfe12:12:29

:while instead :when

Carlos Alcantara12:12:11

but that also still creates an infinite loop

clyfe12:12:12

You still end up with an infinite lazy seq, but now it always reifies an element, wheres before it would be stuck in an infinite loop after max takes

clyfe12:12:12

Clojure 1.10.1
(defn even-squares [max]
  (for [x (filter even? (range)),
        y [(* x x)],
        :while (< y 100)] y))
#'user/even-squares
user=> (take 2 (even-squares 100))
(0 4)

clyfe12:12:27

actually both have the same problem if you try to take beyond the elements that are yielded, at some point it just loops and never finds more

Carlos Alcantara12:12:49

I think I found it

(defn even_squares
  [max]
  (take-while (partial > max) 
     (map (fn [x] (* x x)) (filter even? (range)))))

👍 6
Carlos Alcantara12:12:01

thank you again for the help @UCCHXTXV4

Raymond Usbal15:12:57

Hello, may I ask how popular is Fulcro among Clojure devs?

Raymond Usbal15:12:16

For a beginner, would you recommend Fulcro?

seancorfield18:12:41

@raymond150 If you haven't already found it, there's a #fulcro channel here, so you could ask there about how beginners have gotten on with it.

Shantanu Kumar19:12:03

Has anybody compared cljfmt and clj-kondo and weighed their pros and cons? Any other linter that people use?

didibus19:12:42

cljfmt is not a linter, its a code formatter

didibus19:12:23

Good complimentary linters would be: clj-kondo, eastwood and kibit

didibus19:12:59

Joker is another one, but it overlaps almost 1:1 with clj-kondo, with the latter supporting more things. So I would just fgo clj-kondo and not bother with joker

didibus19:12:42

Only clj-kondo is fast enough to be editor integrated and run on every code change. For eastwood and kibit, I'd run them less often, like pre-commit, or at build, etc.

mafcocinco19:12:58

funny that apples and oranges are quite similar and it seems not unreasonable to be comparing them.

andarp20:12:43

I'm trying to solve a problem where I want to set a series of default values in a map only if the map is missing values. I can't create the map ahead of time but rather have to do this post map creation. Currently I have a function that takes a map, key, value and checks if the map has that key, and if it doesn't returns an updated map with the default value set (otherwise returns the same map). I am threading this my original map to multiple calls of this function like so:

(defn set-default [m key value]
  (if (m key)
    m
    (assoc m key value)))

(-> my-map
    (set-default :kw1 "default1")
    (set-default :kw2 "default2")
    ... etc)
I am sure there's a better way to do this. Any suggestions where to start? Edit: merge really should do the trick, right...

holymackerels20:12:43

yeah, merge my-map into one with the defaults

🙌 3
Pavel Klavík21:12:00

Hi, I am using clj-http to get some image resources as follows:

(http/get url {:as :byte-array})
The server responses with 308 Permanent redirect. By looking into clj-http documentation, it seems to me that the request should be automatically redirected and I should get 200 response for the redirect. Instead, I am getting 308 response map. What am I doing wrong?

clyfe21:12:27

Are you on most recent version?

Pavel Klavík22:12:25

Yes, updated just now.

Pavel Klavík22:12:05

Actually maybe not, let me check.

Pavel Klavík22:12:46

Ok, so updating from 3.10.3 to 3.11.0 fixed the problem.

👍 3
dpsutton21:12:28

did you try the :redirect-strategy :lax?

Pavel Klavík22:12:24

Ya, I did, but it should not matter since I am using GET request, this changes the behaviour for POST requests. But updating to the latest version fixed the problem.

dpsutton22:12:29

agree. just thinking of things to try to investigate

Pavel Klavík22:12:47

thanks a lot for ideas 🙂