Fork me on GitHub
#beginners
<
2019-05-10
>
v3ga01:05:37

Does anyone happen to have a gist or example of a web app without using routes as a global? I’m curious now

kosengan09:05:01

👋👋 beginner here

mloughlin09:05:28

welcome 👋

maximtop11:05:06

hi can someone explain me, please, why this line of code is not working (map #(str "bla") (range 5)) while this is working (map #(str "bla" %) (range 5))

bronsa11:05:13

#(foo %) expands to (fn [x] (foo x)), while #(foo) expands to (fn [] (foo))

maximtop11:05:38

map is not working with function without arguments?

Lennart Buit11:05:08

yeah, it expects a function with a single argument

bronsa11:05:40

in (map f x) f must take one argument

bronsa11:05:55

in (map f x y) f must take 2 arguments and so on

maximtop11:05:04

Ok, I've got you, thanks

Jakub Holý (HolyJak)16:05:12

Is there a place to discuss improvements to the beginner experience of Clojure lang and tooling? Thanks!

donaldball17:05:18

I don’t think anyone would mind a discussion starting here at least.

seancorfield17:05:31

Yes, I think a discussion about the "beginner experience" is appropriate for this #beginners channel!

Jakub Holý (HolyJak)20:05:26

What do I lack wrt beginner experience? 1. I just discovered that the out of the box experience of clojure.test is disappointing, outputting a single long line with two large maps when (is (=..)) fails. Not very impressive for my colleagues :( I guess Kaocha is much better. 2. We should have a web site about beginner-optimized setup, including Kaocha for test, this project I don't remember that replaces the default docstrings with better ones, something like Pretty for less scary error reports...

skykanin16:05:40

is there any support for setting up and testing a mock database with SQL queries using the java.jdbc wrapper?

robertfw17:05:20

That would be out of scope for java.jdbc. For setting up databases I use migratus

seancorfield17:05:23

@nicholas.jaunsen What are you looking for? We use SQL migrations and wrote our own little function for that, but as @robertfrederickwarner notes, there are specific migration libraries. Assuming you're asking about migrations?

skykanin17:05:37

no, I just wanted to setup a mock database to test if the queries I've written work as expected with mock data

seancorfield17:05:32

Sounds like migrations to me: you want your test suite to be able to start with an empty DB and build all the tables/metadata your code needs, and populate it with test data.

seancorfield17:05:06

I would strongly advise using the same database locally for testing that you'll be using in production. SQL -- and DDL in particular -- differs across databases so trying to substitute an in-memory database or local one (SQLite, H2, Derby, HSQLDB) is a difficult path to take. Docker makes this easy.

seancorfield17:05:19

Happy to discuss how we do this in #sql @nicholas.jaunsen

seancorfield17:05:31

Yes, I think a discussion about the "beginner experience" is appropriate for this #beginners channel!

seancorfield17:05:11

@holyjak What would you like to see improved? (It's already a lot better than it was a few years ago 🙂 )

💯 4
seancorfield17:05:53

That's really hard to read @kari.marttila -- can you edit the message and surround the code part with triple backticks?

lilactown17:05:27

is there a reason to use type dispatch as opposed to passing the config around as a map?

Kari Marttila17:05:49

Wait a sec, I try to reformat...

Kari Marttila17:05:49

If I need a configuration protocol and two distinct types that implement that protocol (in order later to dispatch in another protocol based on the types), e.g.:

(defprotocol ConfigP
 "Configuration protocol."
 (get-config [this] "Gets configuration"))
(defrecord SingleNodeConfigR [my-config]
 ConfigP
 (get-config [this] (:my-config this)))
(defrecord AwsDynamoDbConfigR [my-config]
 ConfigP
 (get-config [this] (:my-config this)))
Is there some way in Clojure to make some kind of "base record which would implement ConfigP and then SingleNodeConfigR and AwsDynamoDbConfigR would just extend the "base record" without needing to implement the get-config function?

Kari Marttila17:05:28

It may just be that after 20 years of Java programming my mindset is still too object oriented. 🙂

Alex Whitt17:05:28

Sounds very OO

Kari Marttila17:05:35

Yes. Sorry. 🙂

seancorfield17:05:44

No, Clojure frowns on inheritance for reuse like that (and it's generally a bad pattern in OOP as well).

Kari Marttila17:05:32

What would then be a Clojure idiomatic way if I have a domain which needs to operate with two different data stores (SingleNode and AwsDynamoDb). I'd like to create some kind of configuration record (either SingleNode or AwsDynamoDb) and then pass that record to domain protocol which would dispatch to two domain record implementations based on those types?

Alex Whitt17:05:23

Maybe multimethods?

Kari Marttila17:05:18

Yep. I guess I need to read more my Clojure books to get my mindset to more functional...

seancorfield17:05:20

What's wrong with just using hash maps?

Kari Marttila17:05:45

I somehow thought that you need types to dispatch in protocols?

Alex Whitt17:05:45

+1 for maps > records

seancorfield17:05:46

Configuration is generally "just data".

seancorfield17:05:55

You should think about the protocols of the data stores themselves -- higher-level.

seancorfield17:05:03

You'd have a DataStore protocol for that... Assuming you really need/want to go down the protocol path in the first place.

Alex Whitt18:05:03

This is how I approach these kinds of problems:

(defmulti get-product-groups :domain-type)

(defmethod get-product-groups :aws-dynamo-db [{:keys [products]}]
  ;; Do aws-specific stuff here
  (conj products :AWS-behavior))

(defmethod get-product-groups :single-node [{:keys [products]}]
  ;; Do single-node-specific stuff here
  (conj products :single-node-behavior))


(get-product-groups {:domain-type :aws-dynamo-db
                     :products    [:a :b :c]})

(get-product-groups {:domain-type :single-node
                     :products    [:d :e :f]})

Kari Marttila18:05:59

My idea was to create a domain protocol like this:

defprotocol DomainService
  (get-product-groups []
    "Gets product groups")
  (get-products [pg-id]
    "Gets products for a product group, returns list of items: [p-id, pg-id, name, price]")
...
... and then make two defrecords:
(defrecord Single-node []
  DomainService
  (get-product-groups ...
and
(defrecord Aws []
  DomainService
  (get-product-groups ...
... and then in server to call the DomainService protocol with the configuration and the dispatching would happen based on the configuration (either Single-node or Aws...).

Kari Marttila18:05:54

... I guess this is the next major step in my Clojure study path - learn to think in Clojure. I have studied Clojure the language but I feel I still cannot think in Clojure.

Alex Whitt18:05:34

For me, it's been about shifting my thought process from "objects with types that have state and behavior" to "data flowing through functions"

noisesmith18:05:39

mostly in idiomatic clojure we don't make new protocols

Kari Marttila18:05:53

Aha, that's interesting.

noisesmith18:05:55

they are kind of a last resort

👍 4
Kari Marttila18:05:11

So, I have really misunderstood my Clojure books...

noisesmith18:05:41

defining records / deftypes with no fields is a code smell

mloughlin18:05:48

what helps me is writing it like a really simple imperative process, to get all the OO/interface-y stuff out of my brain, and then adding clojure-y stuff back in by refactoring

Kari Marttila18:05:37

So, what would be an idiomatic Clojure solution to this kind of problem: - I have two datastores: A and B. - I need to create separate implementations for A and B how to interact with those datastores (in "domain layer") - In "web layer" I want to be able to call "abstract" domain functions without knowing which datastore actually is used.

mloughlin18:05:59

where does the decision to use Single-node or Aws get made?

Alex Whitt18:05:16

Per my example, multimethods are often the tool of choice for that kind of polymorphism

Kari Marttila18:05:38

When the server starts it reads the configuration and the configuration says whether to run in mode A (single-node) or B (AWS).

mloughlin18:05:53

if you only have two use cases, is polymorphism required? you could cond on a keyword

mloughlin18:05:22

and pass a fn around

Kari Marttila18:05:45

Yes, I could just cond but this is a learning project and I'd like to find some more "generic" "open" solution.

noisesmith18:05:15

cond plus functions is more generic and open than a protocol

seancorfield18:05:52

I think DomainService as shown above is fine, but I agree that having no fields in the defrecord is "odd". I would expect both to have a config field, at least -- a hash map containing configuration. Given that your services probably have a lifecycle -- some sort of start and stop -- it might make sense to use Component here and then the start could read the configuration from whever (and assoc it into the component).

Kari Marttila18:05:22

Ok. Sounds good.

seancorfield18:05:17

If you really only expect to have a few implementations and they all implement the same API, then protocols probably are the way to go.

seancorfield18:05:33

The key there is that all implementations are expected to implement the entire API.

Kari Marttila18:05:16

Yes, that's the idea for the domain. There are N functions and every implementation (A and B) needs to provide implementation for them.

Kari Marttila18:05:47

But how do I dispatch to either A or B? I though that I need a type for that?

seancorfield18:05:24

That has two types. You don't need additional types for config as well -- just use a hash map for config.

Kari Marttila18:05:13

That's the reason I thought I'm going to make ConfigA and ConfigB...

noisesmith18:05:53

I'd be surprised by any config that couldn't be represented as a hash-map

👍 4
Alex Whitt18:05:25

Agreed. I've yet to run into a situation where I've actually had to define a protocol for anything. 99% of the problems I encounter are just data and functions on the data.

Kari Marttila18:05:35

Ok. It's really great to ask "stupid" questions here. Maybe I didn't get the answer I wanted but I learned something else.

Alex Whitt18:05:22

No such thing as a stupid question! A lot of us went through the same process of unlearning

Kari Marttila18:05:55

Yep. 20 years of Java - the unlearning of OOP is probably the hardest thing in learning Clojure.

👍 4
Kari Marttila18:05:31

Damn. The discussion here is so interesting but my wife calls me. It's Friday evening in Finland and time for our traditional Friday evening movie. 🙂 But many thanks to all of you. Tomorrow I think again whether I need protocols or something else.

Adrian Smith19:05:49

Is there a way to get data.json to recursively apply the :key-fn function?

Adrian Smith19:05:21

or is Cheshire's version recursive?

seancorfield19:05:23

@sfyire

user=> (json/parse-string "{\"a\":{\"b\":42}}" keyword)
{:a {:b 42}}
user=> 

seancorfield19:05:44

That's with Cheshire.

seancorfield19:05:04

(reinforcing that it's always easy to test these things in a REPL)

👍 8
Jakub Holý (HolyJak)20:05:26

What do I lack wrt beginner experience? 1. I just discovered that the out of the box experience of clojure.test is disappointing, outputting a single long line with two large maps when (is (=..)) fails. Not very impressive for my colleagues :( I guess Kaocha is much better. 2. We should have a web site about beginner-optimized setup, including Kaocha for test, this project I don't remember that replaces the default docstrings with better ones, something like Pretty for less scary error reports...

Eric Ervin21:05:00

Can we call this the intermediate experience? I had a couple happy years of hacking Clojure before I even thought to use clojure.test . My beginner experience was a lot of learning a more functional style and testing things in the repl.

Adrian Smith21:05:47

in my project (https://github.com/slifin/beeline) if I run

lein run "{\":select\" [\":b\"]}"
I get
["SELECT b"]
as I expected but if I do
lein native-image
and run
./beeline-0.1.0-SNAPSHOT "{\":select\" [\":b\"]}"
I get no output, though I know my program is running because if I change the input I get errors that make sense for my program, what's stopping it from printing?

Alex Whitt21:05:15

@holyjak with certain additions, like https://github.com/nubank/matcher-combinators, you can get a pretty nice testing experience. I agree that the vanilla experience is lacking.

Alex Whitt21:05:17

I also use the autotest feature of https://github.com/marick/Midje, although my tests are written in clojure.test

seancorfield21:05:42

There are a huge number of options for testing in Clojure but I agree the OOBE is poor for failures with complex data in clojure.test. I don't really think that's a "beginner experience" issue -- as @ericcervin says. There have been discussions about splitting clojure.test out into a separate Contrib library that can evolve at its own pace (like spec, tools.deps, etc), as well as a number of improvements that could be made. I took a snapshot of that wiki page (before Confluence is shutdown): https://github.com/seancorfield/clojure-test

seancorfield21:05:52

There's also been a big improvement in error reporting in 1.10 and 1.10.1. Could it still be better? Sure, but it's vastly better than it used to be. I think it's more important to guide beginners to stay with what you get out-of-the-box rather than pointing them at a vast array of libraries that supposedly "improve" the experience. That's how beginners get into a mess with lein and profiles.clj -- some of those "helpful" plugins cause all sorts of weird dependency conflicts and strange behavior.

👍 4
seancorfield21:05:48

I think it's important that beginners learn how to navigate a Clojure stacktrace. We'd definitely benefit from some http://clojure.org guide on how to read stacktraces I think.

👍 4
Lennart Buit21:05:30

I feel thats also dependent on programming language philosophy. For example Pythons philosophy says that it comes “batteries included”. I don’t personally feel that applies to Clojure in the same way. Its more of a mix-and-match kinda deal 🙂!

Lennart Buit21:05:58

I think thats a good thing! Means that there are test framework to your liking

Lennart Buit21:05:04

and build tools to your liking

Lennart Buit21:05:18

.. etc. But makes it slightly harder to find what is where 🙂

Jakub Holý (HolyJak)21:05:30

the problem is it takes time and experience to find those...

seancorfield21:05:29

Eric Normand's REPL-Driven Development course is something I would highly recommend to beginners, since it focuses on getting the most out of simple tooling and becoming really productive in the REPL https://purelyfunctional.tv/courses/repl-driven-development-in-clojure/

❤️ 4
👍 4
seancorfield21:05:39

@lennart.buit Yeah, Clojure != Ruby for example (and, in particular, there's nothing like Rails). And I have a much harder time dealing with Ruby, gems, rake, bundle, and all that stuff. My Ruby environment is always breaking for no apparent reason. Drives me insane. I'm just glad I pretty much only have to use it for Jekyll/Octopress these days!

Lennart Buit21:05:22

yeah, we do Clojure and Ruby at work. So, I am doing the bundle dance still

Lennart Buit21:05:51

I don’t actually know how any of it works, my rvm is complaining since a few months that I am running some strange configuration

Lennart Buit21:05:58

but it still works, so I am not touching it

Eric Ervin21:05:45

Clojure doesn't include batteries, but it is simple to see where I can attach my own power supply (exercise bike/ water mill / etc)

Daouda21:05:30

hey folks, into is used for putting one collection into another. according to clojure documentation. but why this error?

user=> (into {1 2 3 4} [1 2 3 4 5 6])

Execution error (IllegalArgumentException) at user/eval132478 (form-init595175356905984918.clj:1).
Don't know how to create ISeq from: java.lang.Long

Lennart Buit21:05:32

you are into’ing into a map, is that your intention? Or did you mean a set

Daouda21:05:03

just want to tested what have been said in the documentation

Mno21:05:30

basically id doesn’t know how to put that into a map.

Mno21:05:54

it would work like this though (into {1 2 3 4} [[1 3] [2 4] [3 6]])

Mno21:05:17

because then it knows which ones are keys and which ones are values.

Lennart Buit21:05:20

if you want to put your collection pairwise into the map, there is hash-map: (into {1 2 3 4} (apply hash-map [1 2 3 4 5 6]))

Jakub Holý (HolyJak)21:05:38

@seancorfield Good point about libraries messing up the environment. And thanks for the REPL course tip! I agree people should learn to navigate Clojure stack traces. But I think they don't need to start with that from time 0. As a person evangelizing for Clojure, this is one of the things that detract people in my experience. Similarly with the unhelpful test output. Who thought it was a great idea to put two data structures on a single line?? Anyway, I think most decent test runners in most languages provide better experience (at least not outputting a single line :)) so we should not lag behind (too much). I would believe that using (is (= expected-data actual-data)) is quite common though perhaps with smaller data than I had.

seancorfield21:05:10

I'd love to see clojure.test improved. If it does get split out of core into a Contrib, I've already said I'd be happy to take it on as a maintainer and try to bring some of the improvements from Expectations (which I maintain) in terms of failure reporting.

❤️ 4
seancorfield21:05:37

As for stacktraces, there's no reason for tooling to display them by default at this point. Here's the basic clj tool handling an exception:

user=> (/ 1 0)
Execution error (ArithmeticException) at user/eval1 (REPL:1).
Divide by zero

seancorfield21:05:11

And while that didn't get into clojure.main in 1.10:

(! 1092)-> clj -e '(/ 1 0)'
Exception in thread "main" java.lang.ArithmeticException: Divide by zero
	at clojure.lang.Numbers.divide(Numbers.java:188)
	at clojure.lang.Numbers.divide(Numbers.java:3901)
	at user$eval1.invokeStatic(NO_SOURCE_FILE:1)
	at user$eval1.invoke(NO_SOURCE_FILE:1)
... more stacktrace elided ...
it is part of the 1.10.1 experience:
(! 1093)-> clj -A:1.10.1 -e '(/ 1 0)'
Execution error (ArithmeticException) at user/eval1 (REPL:1).
Divide by zero

Full report at:
/var/folders/p1/30gnjddx6p193frh670pl8nh0000gn/T/clojure-5985290963305616578.edn

seancorfield21:05:17

I use Atom/Chlorine and it shows just the first few parts of the stacktrace inline and greys out the .java parts so it's pretty easy to read just the .clj pieces. I'd still prefer it to collapse more out of the stacktrace -- but this is all about 3rd party tooling at this point, rather than core Clojure. 1.10.1 will allow for tools like lein to use a JVM property to control error reporting so it won't even need to hook into the new functions (which would require version testing/feature flagging).

❤️ 4