Fork me on GitHub
#beginners
<
2020-02-17
>
didibus01:02:28

Sometimes I feel a bit bad that I'm using -> with too many nested ->> like situations where almost everything is ->> except for one ->. But heck, I still do it

hindol.adhya03:02:19

Mixing threading macros makes code hard to read IMO. I try to follow Stuart Sierra's advice to the letter.

tjb03:02:44

YAHOOO!!! i was able to inject a database component into my defroutes im so stoked!

tjb03:02:29

fellow beginners: if you are interested in using the Component lib as I have been the past couple of days i highly recommend this video as well as the skeleton i linked before

seancorfield04:02:42

This example uses Component and has the Application Component injected into the request: https://github.com/seancorfield/usermanager-example

admin05509:02:00

@seancorfield I just tried your user-manager repo, it's great to approch Component, thx. A beginner question : How can I handle the code change like lein-ring > auto-reload? ?

seancorfield17:02:50

@ I never used lein ring because I don't like the "magic" of plugins: I prefer to start/stop the server directly in code (as that example shows) so it is easy to start/stop inside a REPL. I also avoid all "watch"-based workflows and auto-reload/auto-refresh. That stuff can easily get people into a mess because when it breaks (and it often does for people), most folks have no idea how to fix it and end up restarting their REPL anyway.

seancorfield17:02:00

I'm with Stu Halloway and Eric Normand on avoiding all that stuff and working instead with a very simple REPL-based workflow and a very tight edit-eval cycle (often without even saving files: I eval every single change I make, as I make it).

admin05518:02:10

@seancorfield One more time, thank you for your workflow details. In mean time, I had also find some information from you on Clojureverse πŸ˜‰ https://clojureverse.org/t/ring-with-clojure-tooling/4732/3?u=prestancedesign

seancorfield04:02:07

So each controller can get at the application, the database, whatever.

seancorfield04:02:27

@tjb had you seen my usermanager repo before?

tjb04:02:20

yeah i shared it a couple days ago and tagged you πŸ™‚

tjb04:02:29

ive been using that + the video to understand everything

tjb04:02:43

it has been key in me understanding everything (+ the help from Lennart)

tjb04:02:11

im very hyped about my progress haha

seancorfield04:02:52

OK, cool. I didn't scroll back far enough to see if it was a source πŸ™‚

seancorfield04:02:46

Glad I could help!

seancorfield04:02:38

(our scroll back is pretty short!)

naoki86star04:02:26

Hi I'm enjoying clojure more and more in learning. I ask about a contat/conj result. In easy first step, I wanted to get a string array for command line like.

user=> (concat (conj ["/usr/bin/ssh"] host) cmdline)
("/usr/bin/ssh" "" "exit" "0")
I needed to add arguments from array. Then it was unexpected output.
user=> (concat (conj (concat ["/usr/bin/ssh"] args) host) cmdline)
("" "/usr/bin/ssh" "-p" "22" "exit" "0")
after all I got my wants
user=> (concat (concat (concat ["/usr/bin/ssh"] args) [host]) cmdline)
("/usr/bin/ssh" "-p" "22" "" "exit" "0")
I wonder what is important when using concat/conj. thanks

tjb04:02:46

@seancorfield do you mind if i ask you a question regarding jdbc.next here?

seancorfield04:02:12

@tjb Fire away!

seancorfield04:02:17

@naoki86star The key thing is that conj adds items in the "appropriate" way for the type of collection. conj [] will add at the end. conj () will add at the beginning. concat produces a list, not a vector.

naoki86star05:02:37

Thanks for your advice.:man-bowing: First I started to use conj just on two word. I agree your example using concat simply when gathering some parameters.πŸ™‚

tjb04:02:46

so i am attempting to query my postgres db and i am getting the following erro

tjb04:02:52

No suitable driver found for jdbc:

tjb04:02:21

my config looks like this

(def ^:private pg-db
  "Connect map we will use to connect to PostgreSQL"
  {:dbtype "postgresql"
   :dbname "patentfitness"
   :host "localhost:3306/patentfitness"
   :user "USER_HERE"
   :password "PASSWORD_HERE"})

tjb04:02:39

me entry point is here

tjb04:02:41

(defn get-patent
  "Get a patent"
  [req]
  (let [db (-> req :database)]
    (println db)
    (sql/query db ["select * from patent"])))

seancorfield04:02:09

For your final example @naoki86star you could say

user= (concat ["/usr/bin/ssh"] args [host] cmdline)

valtteri04:02:20

You need to add postgresql driver explicitly into your dependencies

org.postgresql/postgresql   {:mvn/version "42.2.9"}

seancorfield04:02:46

@tjb yup, like @valtteri says: you need the PostgreSQL driver as a dependency.

seancorfield04:02:03

next.jdbc doesn't add any database drivers -- you have to do that.

seancorfield04:02:08

The docs are very clear about that.

valtteri04:02:12

Also there seems to be something funny with your jdbc-url. I see port there twice?

tjb04:02:16

hmmmm did i miss this in the next.jdbc docs?

tjb04:02:19

probably!

tjb04:02:34

def missed it then let me re-read

seancorfield04:02:49

Also, your :host is wrong. It should just be the hostname. You have the port and the database name in it too.

seancorfield04:02:14

PostgreSQL uses port 5432 by default. MySQL uses port 3306.

seancorfield04:02:47

I think you want

(def ^:private pg-db
  "Connect map we will use to connect to PostgreSQL"
  {:dbtype "postgresql"
   :dbname "patentfitness"
   :host "localhost"
   :user "USER_HERE"
   :password "PASSWORD_HERE"})

tjb04:02:40

woooooot!

tjb04:02:52

everything works, thanks @seancorfield and @valtteri!!

tjb04:02:35

wow im ecstatic about this progress! hell yes!

seancorfield04:02:56

If there's anything that can be clearer in the next.jdbc docs, feel free to open issues on GitHub about it.

tjb04:02:08

will do!

tjb04:02:17

thanks again everyone this slack channel is a lifesaver

tjb04:02:29

have a good night or good morning depending on your timezone πŸ™‚

naoki86star08:02:32

Hi , now studying on repl. When I changed to other namespace on repl, I annoyed for a while.

app.core=> (use app.sample)
Syntax error (ClassNotFoundException) .....
I've found to put a apostrophe before name-space string. I wonder what is this apostrophe? (ns app.sample)is okay...

andy.fingerhut10:02:38

The apostrophe is also called a quote, and 'expression can also be written (quote expression) . Such an expression is prevented from being evaluated.

andy.fingerhut10:02:26

use is a function, and like all functions in Clojure, all arguments are evaluated before the function is called.

andy.fingerhut10:02:27

ns is a macro, which you can't tell just from looking at the name, but using (doc ns) will tell you if it is a macro. Macros can have their own custom rules for whether they evaluate their arguments, or not. ns does not, and this is the difference between needing a quote for use , but not for ns

naoki86star10:02:19

I see. πŸ’‘πŸ’‘ Thanks for unveiling my details that I didn't understand! These help me not only to let me know more but also will help to solve similar my trouble.πŸ™‚

teodorlu09:02:30

Hey! Quick question about what's what I should write in my ns declaration when using Java interop. The following appears to work:

(ns test.java-interop)

(defn ->pgobject [v]
  (doto (org.postgresql.util.PGobject.)
    (.setType "json")
    (.setValue (json/write-str v))))

(->pgobject {:x 1 :y 2})
;; => #object[org.postgresql.util.PGobject 0x133a7eee "{\"x\":1,\"y\":2}"]
is this what I should be doing, or should I add some :import things in the ns declaration?

leonoel09:02:10

You can do it like that, because java classes are automatically imported. The purpose of :import is to avoid typing package name multiple times.

teodorlu09:02:33

Perfect. Thanks!

abdullahibra12:02:42

how can you write tests which pass values between each other?

michael.e.loughlin12:02:07

do you have a pseudo code example of your situation? It sounds like something I would normally avoid doing - I always learned that tests should be strictly independent of one another.

abdullahibra12:02:51

rest service for example: 1- create a record 2- update a record 3- get record 4- delete it

abdullahibra12:02:10

you need to get the id from step 1.

abdullahibra12:02:17

to pass to 2, 3 and 4

ackerleytng12:02:44

I try and do this by first testing the creation

ackerleytng12:02:52

Then assuming the creation works

ackerleytng12:02:04

Use the creation to test updating

ackerleytng12:02:44

Not sure if there's a better way to do it though

michael.e.loughlin12:02:03

IMO you have 5 tests there, one for each of your 4 steps and an end-to-end test, each can be run in their own separate database transaction (setup/test/rollback) EDIT: If you don't own the REST service can each step just go into one big test?

michael.e.loughlin12:02:37

What happens if you cause a bug in test 2? Tests 3 and 4 will be flagged as failing as well because they depend on 2. If you're working on the Delete functionality do you now have to run Test 1, 2, 3 every time you change Delete?

teodorlu13:02:48

On the other hand, in order to test deletion, there actually needs to be some value in the db, and if that is not working, you won't be able to test deletion. But I second not trying to make tests depend on each other.

michael.e.loughlin13:02:27

It comes down to what you're testing with each test, and why, which is annoying because it means you have to think really hard :exploding_head:

teodorlu13:02:17

Mm. I was thinking one might be testing "too low level" if one intends to test a db write, instead of a piece of functionality.

michael.e.loughlin13:02:19

Fair enough. If it's a REST API you control there's probably DB crud tests already written!

levi.costa114:02:40

I need help to where to start with clojure

aviv15:02:08

I'd recommend to start with this MOOC http://iloveponies.github.io/120-hour-epic-sax-marathon/index.html Do all the exercises and you'd be fine.

levi.costa114:02:23

i don't speak a very beautiful english πŸ˜„

doby16214:02:48

Are you looking for a recommendation on a "getting started" guide or stuck on something?

doby16214:02:48

Are you looking for a recommendation on a "getting started" guide or stuck on something?

tzafrirben14:02:01

https://www.braveclojure.com/foreword/ is a good online book to start with http://www.4clojure.com/ practice some of the fundamentals

grounded_sage14:02:06

I’m a little confused with threads. From what I read you can have 2 threads per core. I’m using core.async async/thread . It seems I can run more than the math of 2x cores would allow. I would expect some kind of error here. What is it doing under the hood?

doby16214:02:13

your hardware has a maximum number of threads it can run in parallel, simultaneously, but there is no limit to how many threads your hardware can run by juggling threads and switching between them quickly. In practice many threads are used for lightweight tasks that don't take much cpu time, such as waiting for input, so threads need to be able to be juggled to be useful

teodorlu15:02:31

Three different concpts: 1. Your CPU has for example for hardware threads 2. Your OS runs hundreds of threads (one or more per app you're running) 3. With core.async you can use "green threads" that don't even use an OS thread (less overhead). In practice, you might be able to run hundreds of thousands of these.

levi.costa114:02:48

@doby162 i looking for recommendation with getting started, books, etc,

doby16214:02:38

Other than @tzafrirben's good suggestions, I would also suggest https://clojure.org/guides/getting_started But probably brave clojure first.

levi.costa114:02:13

I'm migrating fom PHP, i belive i have difficulty

admin05515:02:54

@ I come from PHP too. I begin to learning Clojure/ClojureScript since 8/9 month and the trip is awesome! So welcome! πŸ™‚ Some links : β€’ https://exercism.io/tracks/clojure (you can compare the results exercices with different language that you know like PHP for learning basics ) β€’ If you're interested with front-end and SPA development, there are some videos for free : https://www.learnreagent.com/

aviv15:02:01

Any cleaner way to get-specific-map from vector-of-maps depend on specific value atm I do: (first (filter #(= (:key %) val) [m1, m2,....]) but this is not readable IMO and looks bad in the project

tzafrirben15:02:45

using a thread macro?

(->> [{:key 1} {:key 2} {:key 3}]
     (filter #(= (:key %) 2))
     first)
you can also replace #(= (:key %) 2)) with a β€œreal” function

aviv15:02:10

it'll look better but still looking for some other way not to use the filter/predicate/first work (too much)

aviv15:02:19

hmm i'll stay with this for now I guess

mark54015:02:02

@, the (first (filter ...)) pattern is very common in Clojure for what you're doing. I recommend using it, especially if you are a beginner, since you will see it often in other's code.

mark54015:02:08

However, you may also be interested in the some function, which returns the first truthy result of a fn you pass.

(let [ms [{:key 1} {:key 2}]
      val 1]
  (some #(when (= (:key %) val) %) ms))
=> {:key 1}

aviv15:02:25

btw, is there any way to unpack from a sequence? in python I'd do something like res1, rest2 = foo()

aviv15:02:44

as in this case I'd want to pick more than 1 item (right now we are picking first

tzafrirben16:02:32

You can destruct a sequence if this is what you mean (I'm not very familiar with python) (let [[a b c] [1 2 3]]...

ben.sless18:02:57

Another sweet pattern I learned about recently, which is also more extensible, is

#(= (:k %) v) => (comp #{v} :k)
It's especially strong if you want to add other options the value can be, just add members to the set. cons: always. In this case, creating a set. If v is constant, however, you can define the entire composed function outside. Also, set lookup is slightly slower. You gain readability and flexibility, pay slightly with performance

heatonsam15:02:26

Can someone give me an opinion on this style of writing functions using let down to the return? I've found myself doing this and it helps me organize the code conceptually as well, but I probably could have used some kind of macro to chain things as well I asume. Example:

(defn map-with-changed-element
  "Returns a map with element identified by id modified by query-map"
  [id maps query-map]
  (let [args (walk/keywordize-keys query-map)]
    (let [element (list-filtered-by-id id maps)]
      (let [allowed-keys (select-keys args (keys element))]
        (let [new-element (merge element allowed-keys)]
          (assoc-in maps [(keyword id)] new-element))))))

mark54015:02:46

Threading macros can be nice, but it is also very useful to have names for the intermediate values, just like you have done. But the let form doesn't need to be nested, and using a single let will make it more readable. Each binding in a let can refer to the previous bindings.

(defn map-with-changed-element
  [id maps query-map]
  (let [args         (walk/keywordize-keys query-map)
        element      (list-filtered-by-id id maps)
        allowed-keys (select-keys args (keys element))
        new-element  (merge element allowed-keys)]
  (assoc-in maps [(keyword id)] new-element)))

heatonsam15:02:08

Oh, that's cool. I did not realize I didn't have to nest them. Thank you!

mark54016:02:05

Sure! It is a nice feature.

ben.sless18:02:46

@ wait until you learn about multiple bindings in for / doseq ! it's really useful! (TLDR, you won't need to write nested loops)

doby16215:02:38

At first glance this looks like a good opportunity for the threading macro https://clojure.org/guides/threading_macros

aviv15:02:08

Are there any benefits of using defrecord instead of a simple map Let's say i'd like to implement a singly-linked-list in clj. I could either choose to define (defrecord Node [v next]) or do the same with a map {:v v1 :next n1} Ofcourse it's more readable but with the clojure notion of representing entities as maps, how is defrecord any useful?

ben.sless18:02:07

I don't know if what you're implementing specifically requires a singly linked list, but clojure already has one out of the box, the cons cell.

(cons head tail)
returns the equivalent of
{:v head :next tail}
With the added bonus of already implementing the ISeq interface, meaning first and rest work on it, returning the value and tail, respectively.

alexmiller15:02:02

there are a number of nuanced tradeoffs

alexmiller15:02:19

records have a strong type, which can be useful to hook into protocol dispatch

alexmiller15:02:56

they are implemented as a class with fixed known fields - generally that can be more efficient in memory and field access than generic maps

alexmiller16:02:10

but that also means modifications yield a new record instance, not a map with structural sharing - the impacts of that vary, it really depends whether that's better or worse

aviv16:02:21

So as a rule of thumb, what should I go for

alexmiller16:02:44

there is no one answer - it really depends on understanding the tradeoffs

gerred16:02:56

i usually progress (using data around performance or evidence of some other form) from maps to records or types

alexmiller16:02:00

for making a new data structure like a linked list, that's actually what deftype is designed for

alexmiller16:02:25

(although I'm not sure why you wouldn't just use lists for that)

aviv16:02:49

just needed a quick use-case before I post a question here πŸ™‚

aviv16:02:12

i'm actually trying to implement a zipper data structure

aviv16:02:21

so i'd go for the record

matthewlisp17:02:20

I have a SEQ of MAPS I want to drop EVERYTHING after ITEM X from the seq. how would you do that?

matthewlisp18:02:47

made it with reduce

matthewlisp18:02:07

it doesn't because i don't want to check anything, i want to drop everything that exists after item X

timcreasy18:02:23

Alternatively, perhaps that is just β€œtake”?

matthewlisp18:02:58

take is problematic because i would have to specify a fixed number, and since i'm working with a result from a 3rd party service... i don't trust it

matthewlisp18:02:37

reduce can do it when i check for the last value from the empty collection that it's using

hindol.adhya18:02:14

take-while not X, and then add back X.

hindol.adhya18:02:27

With reduce is also okay. I presume you are short circuiting with reduced?

mark54019:02:37

Here is what @hindol.adhya said (take-while and add back) using concat:

(let [coll [1 2 3 4]
      val  3]
  (concat (take-while #(not= val %) coll) [val]))
=> (1 2 3)

hindol.adhya19:02:21

I like to write (take-while (complement #{val}) coll). It is a matter of preference I guess. Just throwing it out there as a possibility.

mark54019:02:57

Yet another way: (take-while (partial not= val) coll)

alexmiller19:02:52

there's a take-until jira ticket out there with a variety of impls

alexmiller19:02:52

for a bunch of reasons (some mentioned there), I'd say odds are low that this will ever make it into core, so take what you need :)

levimanga20:02:45

Hello : ) , I'm wondering if there is a package to covert data into "type metadata"

andy.fingerhut20:02:30

Can you give an example of what you mean by type metadata? And what kind of data you would want to convert into that?

levimanga20:02:54

I think that I've misused the term, but I want something like this {:a 0, :b "aa"} -> {:a Num, :b String}

seancorfield20:02:30

You can call type on any Clojure value to get its type and you can use types as values -- but you'll likely want to massage the various numeric types to a common base type.

levimanga20:02:33

using type I get PersistentArrayMap, but I want something more specific, seems like calling type on each value would do what I want

levimanga20:02:45

so I'm wondering if there's a library for that

alexmiller20:02:17

there are one or two libs to infer specs from examples

alexmiller20:02:23

which seems in the ballpark

seancorfield20:02:12

@ Yeah, I meant you'd need to walk the data structure and call type on the values of it.

didibus06:02:29

Don't need a library, its a one liner:

(reduce-kv (fn[m k v] (assoc m k (type v))) {} {:a 0 :b "aa"})

fran.fhb21:02:02

How would I implement a spec that depends on another variable other the the actual value? For example, I’d like to define a spec for:

(defn first-element [sequence default]
  (if (or (empty? sequence) (nil? sequence))
    default
    (first sequence)))

(s/fdef first-element
  :args (s/cat :sequence seq? :default any?)
  :ret any?
  :fn ...)
In a static typed language (with some support to generics) I’d do something like:
first-element<T>(sequence: Seq<T>, default: T): T

alexmiller21:02:55

you can check that in the fn spec here

alexmiller21:02:11

but spec doesn't have parameterized specs

fran.fhb21:02:42

humnn got it! So do you thing any? is a reasonable spec or is there a better way of representing default in specs?

seancorfield21:02:00

@fran.fhb Note that seq? probably isn't what you want there: it means "does this thing implement ISeq?" and lots of things that behave as sequences do not. Also, empty? will return true for nil so you don't need both checks there.

fran.fhb21:02:49

Indeed! I refactored it and it works perfectly, thanks!

seancorfield21:02:13

You'll have to "step back" from static types when thinking about Clojure to some extent. Sequences (and collections) are often heterogeneous -- and Clojure does value-based equality checks on collections so you'll find, for example, that (= [1 2 3] (map inc (range 3))) which may be surprising if you're thinking in terms of static types.

fran.fhb21:02:50

Makes sense, I’ll try to switch my mindset! Thanks for the concrete example!

seancorfield21:02:35

Also, first calls seq on its argument so it can return the first element of anything that is seqable? and something that implements ISeq is treated specially, as an optimization.

fran.fhb21:02:01

From the docs for first, I guess, I could assume coll? would be a better predicate, right?

seancorfield21:02:50

That checks whether something is an IPersistentCollection -- seqable? is more general.

seancorfield21:02:18

user=> (coll? "Hello")
false
user=> (first "Hello")
\H

seancorfield21:02:45

user=> (seqable? "Hello")
true
user=> (seqable? [1 2 3])
true

seancorfield21:02:41

Sequences are an abstraction in Clojure, rather than a concrete type. Lots of things can be treated as sequences, because seq works on a lot of things.

fran.fhb21:02:42

Thanks! now it’s really clear!

nbtheduke01:02:18

sequence? might be closer, right?

seancorfield04:02:43

@ what is sequence? I'm talking about built-in predicates that can be used in Spec.

seancorfield21:02:02

user=> (seq? [1 2 3])
false
user=> (first [1 2 3])
1

alexmiller21:02:23

this is a good example actually of exactly the kind of constraint we've been working on wanting to have better tools for in spec 2 around function specs

alexmiller21:02:41

a big difference from types and parameterized types is that spec is much more about sets of values

alexmiller21:02:11

the thing you really want to say here is that the result is exactly one of the values from the input

alexmiller21:02:19

it's not just that the input is a "coll of ints" and the result is an "int". it's the result is one of that set of values in the input collection.

fran.fhb21:02:33

Ohh that makes so much more sense now, thanks! I’ll follow the development of specs closer! thanks!

mattias50421:02:58

Risking my head, I just have to ask anyone who knows more - reading the leaves and the wind, one conclusion would be that Clojure asymptotes towards something like what Haskell does... that is, a ghost in the machine that acts on function definitions (and stuff?) that tells you if you’ve messed up. JavaScript emerges as TypeScript. Python, bastion of no types, gets types. Clojure gets something. Is this all inevitable? Is this always the path?

mattias50421:02:39

(Honest, if more than regularly confused, question.)

seancorfield22:02:16

Not sure I follow... Spec isn't a type system -- it's more powerful in some ways and it's available at runtime for data validation and conformance.

seancorfield22:02:42

It's true that some languages are getting optional/gradual type systems either within the language or as a dialect -- but I don't think that's inevitable (or even desirable, in many cases). Clojure has a dynamic system for specifying predicates that hold for values at runtime which is rather different.

seancorfield22:02:23

core.typed is an example of a library that provides an optional, external way to "type check" Clojure code but it is a research project (and it is in the process of being fairly substantially reworked, as I understand it, based on lessons learned from earlier incarnations and research into how data types flow through code).

andy.fingerhut22:02:39

Another view is that both Python and Clojure do have types, but it is not the symbols that name parameters and local variables that you associate with types, but all values at run time do have types, and very restricted ways that one can be converted to others. Haskell, C, Java, and many other languages associate types with names, and endeavor to check at compile time that you only do operations on those names that are allowed on values/objects of those types.

andy.fingerhut22:02:49

Where Clojure and Haskell are more like each other than different, is their strong emphasis on manipulating immutable values using pure functions. Haskell is in many ways more strict than Clojure is about this -- you can very easily write code that mutates data in Clojure, without having to put a special type signature on it like you would have to in Haskell. Except for a few languages (e.g. Erlang, and I think maybe Elm and Elixir), most languages are what I would call 'mutable data by default', meaning that while they can have libraries implementing immutable data structures, those libraries are not the default used by most developers of those languages, so if you try to develop in those languages with mostly immutable data, you cannot use most of the libraries of the language.

didibus07:02:09

Also mutable variables

didibus07:02:18

That's a big one as well

didibus07:02:27

Clojure variables are immutable by default. At least locals, and while vars are not, the convention is so strong here to not re-def that I'd consider it being immutable by default.

lockdown-22:02:55

That last phrase is not actually true IMO, look at immerjs

lockdown-22:02:55

That last phrase is not actually true IMO, look at immerjs

andy.fingerhut22:02:50

Are you saying that immerjs is a library that lets you use most existing JavaScript libraries, using immutable data structures?

lockdown-22:02:02

Sure, you can put it that way.

andy.fingerhut22:02:19

If so, that surprises me. I would expect that in JavaScript, there are many libraries that assume objections, collections, etc. that you pass to them can be mutated, and would not work if you tried to pass them an immutable version of that object/collection/etc.

andy.fingerhut22:02:42

And that they are among the most widely used JavaScript libraries.

andy.fingerhut22:02:14

But I will be the first to admit to avoiding JavaScript programming myself, and passing this on second hand as heard from others.

seancorfield22:02:09

Looking at immer.js, it is not what Andy was talking about.

seancorfield22:02:57

immer.js doesn't prevent other JS code from modifying the "immutable" data -- it's just an API that implements an explicit transformation that doesn't modify the original, but instead produces a new version of the data.

lockdown-22:02:25

@ what library assumes that? if one such exists, is either a pretty bad/obscure or is some framework instead of a lib

seancorfield22:02:24

What Andy means is that because JS primitives can modify data structures, you can't "just use a lib" that provides immutable data structures without avoiding passing that data to those primitives.

lockdown-22:02:27

the whole point of immer is to work with "JS primitives"

lockdown-22:02:56

and not mutate the data

seancorfield22:02:04

@ immer does not prevent any data from being modified by other code.

lockdown-22:02:19

do you mean like another thread?

seancorfield22:02:03

The data structures you use immer with are not immutable: it's just providing a formal API for expressing "change" that creates new versions of data.

seancorfield22:02:37

If you express a change from A to B with immer, and then pass the result to an arbitrary JS library, nothing prevents that library from mutating B.

lockdown-22:02:47

yes, its a single API call, the end result is the same

seancorfield22:02:04

I think you're misunderstanding what Andy and I are saying...?

lockdown-22:02:05

you modify data, you get new data

lockdown-22:02:39

libraries don't hold your data

seancorfield22:02:15

But JS libraries can and do objects you pass in to them. Immer doesn't prevent that.

seancorfield22:02:51

Every single JS library you use would have to be written using Immer to avoid mutation.

lockdown-22:02:54

libraries operations are like any other operation, if you don't want a library to mutate, you simply just use the library api inside immer

seancorfield23:02:08

You're missing the point.

seancorfield23:02:26

The data is not immutable. Any library code can migrate it.

seancorfield23:02:40

Using Immer to avoid that is a programming discipline.

seancorfield23:02:45

That's not immutability.

lockdown-23:02:35

yes, if you adhere to immerjs, you have to use it eveywhere you process data, but I don't see how that is not immutabitliy, or that you can't use JS libraries because they are not compatible

lockdown-23:02:11

the initial issue was that if you work an immutable lib in JS you can't use most libraries, this not true, as immer proves

seancorfield23:02:40

We'll have to agree to disagree.

ales.najmann23:02:10

but immer is just a tool to prevent mutation to happen, it is a discipline. if you handover that structure to some lib, and it mutates something it destroys the assumption. as was said, that's not guarantee.

andy.fingerhut23:02:29

I guess a better way of saying what I am trying to say is that you can use immutable data structures in just about any programming language. In many of them, the language and/or commonly used libraries do not encourage it, so you are fighting the tide trying to consistently use immutable data.

ales.najmann23:02:12

programming with immutability means programming with absense of assignment operator I would say

andy.fingerhut23:02:23

e.g. pull in one more dependency that uses commonly used library X, and your carefully constructed land of immutability is now encroached upon.

andy.fingerhut23:02:31

With Clojure and Haskell, immutability is the default -- you are not fighting against the tide. Yes you can use Java libs in Clojure programs that assume mutability throughout, in both internal implementation details and in its API, but it isn't fighting against the tide to avoid pulling those in, in most Clojure programs.

lockdown-23:02:21

@ @seancorfield both of you have it backwards, if a library mutates, immer will give you a new version period. you don't have to adapt libraries to use immer

lockdown-23:02:25

@ sure, but immer makes this seamless, is just one api call and why is so good

ales.najmann23:02:42

"Immer automatically freezes any state trees that are modified using produce. This protects against accidental modifications of the state tree outside of a producer. This comes with a performance impact, so it is recommended to disable this option in production."

andy.fingerhut23:02:28

It is unlikely I will use JavaScript any time in the near future. I have seen libraries in other default-mutable languages that expect to be passed mutable data structures, and part of the implementation is such that if you gave them some kind of defensive-copy-on-write data structure with implementations of operations like 'insert' or 'remove' on a list, they would keep using the original object's pointer/reference, not the copied one.

andy.fingerhut23:02:07

e.g. mylist.insert(new_elem), with no return value, is a common signature in many languages for inserting a new element in a list. The caller has no way to get back a new list, and assumes it can do further mutation operations on the one it was given.

andy.fingerhut23:02:28

The caller of such an API has no way to use a copied-on-write version in later calls.

lennart.buit23:02:12

(FWIW: Newer JS APIs such as Object.assign and Object.freeze make working immutably easier . But you are right in that mutation is pretty much the default in JS)

lockdown-23:02:28

@ this is not true for immer, you will always received a completely new list

andy.fingerhut23:02:30

Is there a JavaScript API call for some kind of list/collection that is commonly used, that looks something like 'mycollection.insert(new_element);`, that does not return another collection, and the caller can now use mycollection to refer to the updated collection?

andy.fingerhut23:02:00

Not in immer, but something that might be completely outside of immer, but widely used in JavaScript code?

lockdown-23:02:07

@ yes, as others said you can achieve immutabililty more easily with modern JS, but immer makes this much less of a chore

andy.fingerhut23:02:21

If there is something like that in JavaScript, and some JS library X uses calls like that in it, and makes those assumptions, then do you believe you can call that library with a collection created via immer, and X will still work?

lockdown-23:02:48

@ yes, absolutely, that's the whole premise of immer

lockdown-23:02:08

you can mutate all you want inside immer, but it will finally give you back a new version

lockdown-23:02:07

and the collection passed in will be untouched

ales.najmann23:02:15

..but you cannot work with the collection in regular way. you have do it in callback to produce function. that's very different mode of operation.

ales.najmann23:02:30

plus, protection is present only in dev mode, in prod mode, it's disabled.

ales.najmann23:02:12

in dev mode it uses proxies and it is fairly expensive

lockdown-23:02:43

the same as atoms are very expensive

ales.najmann23:02:10

I have a feeling that we are getting nowhere here

lockdown-23:02:43

that protection is so you don't mutate outside immer and can be caught during dev

lockdown-23:02:38

it would be silly if an immutable library couldn't guarantee immutability πŸ˜‰

mark54001:02:22

I think Immer is an exception as far as immutability and languages go. I see it as a trick to make existing JS code work when wanting to use immutable data structures. It's cool in that it does this with a very small amount of code by using proxies. But it's probably a mistake to generalize about the language issue based on the existence of Immer.

didibus07:02:08

Immer does look interesting. What happens if you do this though:

const todos = [ /* 2 todo objects in here with their done = false */ ]

const nextTodos = produce(todos, draft => {
    draft[1].done = true
})
 
todos[0].done = true 
console.log(nextTodos[0].done)

// and 

nextTodos[1].done = false 
console.log(nextTodos[1].done)
If I understood the doc. The first one mutates, because immer only freezes the parts you proxied through produce. So its not making it fully immutable. And the second one throws an error in dev, and mutates in prod. In dev, it will have frozen that object, which means that if this is something you pass to a lib you don't own and which expects to mutate it, it will fail, which was @ 's concern I believe.

didibus07:02:03

My other question is, it seems the only advantage over ImmutableJS is that you can still use imperative mutation syntax to describe the changes which means better IDE support?

lockdown-16:02:41

Every operation (including any libraries) you perform inside immer will produce a new version of the input collection

lockdown-16:02:17

if you modify that result outside immer, of course it will mutate depending on the operations you make

lockdown-16:02:56

if you want to never mutate the original collection and always produce a new one, you do all the transformation inside immer, including third party library calls

didibus16:02:11

Right, so that's the problem. Its not immutable by default. This will work fine with React, because React chooses to push immutability, and has a lot of functional APIs.

didibus16:02:54

But any other 3rd party lib which doesn't would cause issue.

didibus16:02:36

Maybe for practical JS, its not a big deal. On the front end, you don't need that many libs anyways beyond React. But I think in Node you'd face that issue a lot more.

lockdown-16:02:20

Of course is not immutability by default, its JS after all, immer is a way to achieve full immutability, at the cost of doing transformation inside a single function call

lockdown-16:02:37

how would 3rd party libs cause an issue?

lockdown-16:02:43

as long as you call the 3rd lib inside the immer container, the original collection won't be mutated, and you will receive a new version

lockdown-16:02:25

all the concern you and others raised are true for immutablejs, not for immer

lockdown-16:02:43

that it was so good about it

didibus16:02:09

Its possibpe that's the case, but that would mean there is something I didn't understand about how it works. From the docs, I don't get that impression though. It seems only calls to produce create a copy, but standard calls will still mutate (or throw in dev if frozen)

didibus16:02:15

function transferMoney(person1, person2) {
person2.money += person1.money 
}

lockdown-16:02:27

you can apply all the mutations you want inside immer, immer will make you sure the original input is untouched and after you have applied all the mutations/transformations,immer will give you back a completely new version of the final result

didibus16:02:28

Say a library had a function like that?

lockdown-16:02:09

you would do:

lockdown-16:02:58

function transferMoney(person1,  person2) {
    produce(person2, draftState => {
        draftState += person1.money
    }
}

didibus17:02:35

But that's not your function

didibus17:02:00

transferMoney is from a library

didibus17:02:54

Though now I think you could do:

produce(person2, draft => 
  { transferMoney(person1, draft) }

lockdown-17:02:05

prodouce(person2, draftState => {  transferMoney(draftState, person1)
}

didibus17:02:29

Ya, I hadn't realized that. This part is pretty clever

lockdown-17:02:01

yeah, you call the lib inside produce

didibus17:02:37

Still requires a lot of care from your part. And might even require some careful knowledge of the inner working

didibus17:02:51

But you might be able too

didibus17:02:31

A real transfer money would modify both, can produce work on more than one coll at a time?

didibus17:02:14

function transferMoney(person1, person2) {
money = person1.money 
person2.money += money 
person1.money -= money
}

didibus17:02:38

It doesn't seem it can, but maybe a future version could. If it added support for transactional produce like that as well, I'd say its a pretty great idea

didibus17:02:08

Thanks for showing me. Its a clever trick. I think we might see more of this in the future.

lockdown-17:02:36

let me try a solution to your version

andy.fingerhut17:02:55

Doesn't that example require writing transferMoney from scratch? If transferMoney was in a library that you didn't want to (or couldn't) modify, is this approach still usable for transferMoney as written?

lockdown-17:02:27

typing here is hard heh

lockdown-17:02:55

produce([person1, person2], draft => {                                                                                               transferMoney(draft[0], draft[1]);
});

didibus17:02:53

Ya I'm sold. This is definitly the best I've seen to handle this problem.

lockdown-17:02:42

yeah, it fixes the problems with immutablejs, you keep your native JS APIs (instead a learning a bunch a new ones) and don't have to transform between immutablejs objects and JS objects all the time for interop

lockdown-17:02:58

so stuff like lodash just works

didibus17:02:15

If their performance claims are true, its definitly a big improvement. I can see it getting lots of adoption

mark54017:02:49

Yes, if you're stuck using JS, or you want to use it for some reason, then immer is a good way to improve things.

lockdown-17:02:28

of course, clojure is way better, but keeping in mind the constraints of JS, this is a clever and ergonomic solution

lockdown-17:02:08

you can achieve the same without immer but it will be way more boilerplate

didibus17:02:32

Right, but I wonder what else that could benefit. ClojureScript also struggles at the boundary between immutable collections and js interop

lockdown-17:02:24

yep. besides extern types, clojurescript has the same issues as immutablejs

didibus17:02:34

So if you could add a similar interface. Like a reverse transient

lockdown-17:02:40

so makes you wonder, if the tradeoff of using clojurescript is worth at all

didibus17:02:34

If ClojureScript colls could proxy themselves as if they were mutable like that only within the context of a special closure

didibus17:02:01

You could now pass them to JS libs expecting mutable colls

mark54017:02:11

Note that in spite of immer performing relatively well, it still requires a shallow copy of every object that has changed. So if you don't need to copy between clojurescript and native objects, for example, then you'll get better performance with clojurescript's persistent data structures, since they're optimized for this. There is a big difference between using optimized immutable data structures and just making shallow copies.

didibus18:02:01

I mean, yes and no. Things are contextual. Since JS is single threaded, you can effectively operate in this transient mode at all times

didibus18:02:46

And I was under the impression ImmutableJS implemented similar fast persistent data-structures

didibus18:02:25

So the benchmarks against that should gives us a good idea for against ClJS as well

lockdown-18:02:26

although immer uses structural sharing your point may be valid, the issue for converting between js objs and clojure data structures is so prominent that there are many third party solutions out there with different tradeoffs each one

didibus18:02:55

The conversion from cljs to js is super slow as well

didibus18:02:20

Doesn't it do a deep copy pretty much O(n)

andy.fingerhut18:02:33

If you use immer to pass some immutable object into some functions, and that thing you pass in is referenced a dozen places within existing mutable objects, the returned value is new, and not referenced from those dozen places?

andy.fingerhut18:02:47

I am not saying I want something that can do that. Just looking for the edges of what it does/doesn't do.

mark54018:02:09

Yes, the returned value is new. That's what I meant by saying that it has to copy the objects that have changed. I mentioned the pitfalls to get more ideas of the edge cases.

lockdown-18:02:41

@ yes, inside immer you are working with a draft of the original input collection, whatever you do to that draft immer will returned a new collection with all the operations you applied to the draft