Fork me on GitHub
#clojure
<
2021-08-25
>
Sreenath Guttikonda07:08:16

Is this the right channel to ask for library suggestions?

Sreenath Guttikonda07:08:47

I am looking for an implementation of state charts (hierarchical state machines)

Sreenath Guttikonda07:08:26

i am planning to use this in a hobby project

Sreenath Guttikonda07:08:40

thank you for the suggestions @U2FRKM4TW!

Sreenath Guttikonda07:08:05

i'm new to the Java world, so didn't think to look in that ecosystem

Sreenath Guttikonda07:08:32

will read through the above. thank you!

p-himik07:08:03

It very well might be that there are Clojure libraries - I just didn't find them, so tried looking for Java ones.

Sreenath Guttikonda07:08:28

looks very promising - thank you @U8LB00QMD!

Sreenath Guttikonda07:08:40

if you don't mind @U8LB00QMD - may I ask how you came to be aware of this?

Bobbi Towers07:08:57

It was posted in #announcements a couple of months ago and I gave it a star. Haven't had a chance try it out though.

👍 3
seancorfield16:08:40

Sometimes the #find-my-lib channel can be helpful for questions like this (but it isn't as populated as this channel).

henrik4208:08:20

Hi. I'm using test-ns-hook to control my test suite. I'm trying to use deftest to run tests against some docker containers. There is one (deftest all-sites-available,,,) which polls some http endpoints to verify that the application is up. Now I'd like to return from test-ns-hook if (all-sites-available) "fails". Is there a way to detect this failure? At the moment I'm using (def sites-are-up (atom false)) and reset! that on success but that's ugly.

javahippie09:08:15

Is it possible to see some more code? I am not quite getting the constellation, yet

javahippie09:08:59

Maybe you could consider all-sites-available as a precondition for all tests in that ns, not as a test itself. In that case you could run this check with (use-fixtures :once ...) and skip all dependent tests afterwards if it returns false?

henrik4210:08:50

Here's the code

(defn wait-for-url-available [url],,,)

(def sites-are-up (atom false)) 
(deftest all-sites-available
  (reset! sites-are-up false)
  (and
   (is (true? (wait-for-url-available url-1)))
   (is (true? (wait-for-url-available url-2)))
   (reset! sites-are-up true)))

(defn test-ns-hook []
  (all-sites-available)
  (when @sites-are-up
    (,,,more-tests-here,,,)))
I understand that (is) can be used like a function call that's why I'm using it with (and). (deftest fun) does define a function but I cannot return a value, no? I could just do the whole thing with functions but I wanted to try how it would work out with deftest.

dabrazhe09:08:34

Hi. I am parsing a json string with [cheshire.core :as json], json/parse-string. It converts the high level json into a Clojure map, but the internal json is not parsed. I have to destruct it and parse it manually again. Am I missing smth?

delaguardo09:08:43

you have to do that in case your json data contains some string representing encoded json

"{\"key\":\"{\\\"netsed-key\\\":42}\"}"
is that your case? otherwise cheshire should parse entire json as long it is valid json

dabrazhe09:08:54

I am getting, this one below. It parses policy and content correctly. why not the content of :Content (no pun intended:)

{:Policy
   {:Content
      "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Sid\": \"RestrictAMIs\",\n      \"Effect\": \"Deny\",\n      \"Action\": [\n        \"ec2:RunScheduledInstances\",\n        \"ec2:RunInstances\"\n      ],\n      \"Resource\": \"arn:aws:ec2:*::image/ami-*\",\n      \"Condition\": {\n        \"StringNotEquals\": {\n     
...etc

delaguardo09:08:25

so yes, that looks like the case I just describe.

dabrazhe09:08:14

It there a way to render it into the map in one go?

delaguardo09:08:22

I don’t think so. from json specification that is completely valid structure.

dabrazhe09:08:52

yep, that's what I thought, thank you

dabrazhe10:08:55

Is there a way to simplify this without eval? (note: it's not about + function per se, can be any that takes several parameters)

(let [vecc '[1 2 3]]
 (eval `(+ ~@vecc))
  )

p-himik10:08:49

So you want to be able to compute an arbitrary function at compile time, given a vector of arguments?

p-himik10:08:39

If yes, then

(defmacro compile-call [f args]
  (apply (ns-resolve *ns* f) args))
=> #'user/compile-call
(compile-call + [1 2 3])
=> 6
(macroexpand '(compile-call + [1 2 3]))
=> 6

dabrazhe10:08:41

The function is defined, but the arguments should be passed along, not in a []. Eg (corfunc "command" 1 2 4 etc).

dabrazhe10:08:16

In the case when apply cannot be used

p-himik10:08:18

The arguments will have to be known at compile-time. So it cannot be e.g. [(inc 1) (inc 2)] since the vector will be quoted.

p-himik10:08:27

And why can apply not be used?

dabrazhe10:08:48

apply will work with +, but will it work with  `(corfunc "command" 1 2 4 etc)` , when the arguments are of different type?

p-himik10:08:46

Absolutely.

dabrazhe10:08:29

Alright, will have to test. What about getting rid of eval in the original example, when the function is known?

p-himik10:08:26

Just call apply?

p-himik10:08:32

(apply + vecc).

p-himik10:08:51

Or (apply corfunc ["command" 1 2 4 etc]).

dabrazhe10:08:27

yes, it does apply apply to all args. Thanks!

👍 6
kulminaator10:08:06

In general if you need to eval you probably made a mistake 😄

😁 3
dabrazhe10:08:22

How do you call your macro with this funtion, @U2FRKM4TW. It appears to fail

(defn corefunc [one & two]
  {:one one
  :two two})

p-himik11:08:32

How did you try calling it?

dabrazhe11:08:37

In all possible ways ) , eg (compile-call corefunc [1 2 3 ])

p-himik11:08:04

Which version of the macro did you use exactly?

dabrazhe11:08:09

(defmacro compile-call [f args]
  (apply (ns-resolve *ns* f) args))

p-himik11:08:44

Use (macroexpand '(compile-call corefunc [1 2 3]) and see what it outputs. It should become obvious what's wrong. Keep in mind that macros don't always result in plain values - they output new forms to be evaluated.

dabrazhe11:08:17

The error is

(compile-call corefunc ["one" 1 2 3])
; : Cannot call 1 as a function.
Macroexpand result looses 1
{:one "one", :two (2 3)}
I am fine with apply. just wanted to understand macros a bit better, but it's ok for now, if it's so hard to debug

p-himik11:08:24

It's not hard to debug at all in this case. It doesn't lose 1 on my end:

(macroexpand '(compile-call corefunc ["one" 1 2 3]))
=> {:one "one", :two (1 2 3)}
The error is in (1 2 3) - this form means "call 1 as a function and supply 2 and 3 as its arguments". And longs are not callable values in Clojure.

p-himik11:08:30

One way to fix it would be to wrap the usage of two in vec - then (1 2 3) will become [1 2 3], which won't cause any troubles.

p-himik11:08:17

And if you need specifically a list in :two and not a vector, you can use (cons list two).

(defn corefunc [one & two]
  {:one one
   :two (cons list two)}

(compile-call corefunc ["one" 1 2 3])
=> {:one "one", :two (1 2 3)}

p-himik11:08:09

But if you don't have a very good reason to compute such things in compile time (negligible performance improvement is not a good reason), I'd stick to regular functions instead of macros.

dabrazhe11:08:13

at what point ["one" 1 2 3])) becomes (1 2 3)?

p-himik11:08:10

You supply "one" 1 2 3 to a function with an args vector of [one & two]. Upon calling that function, one is assigned the value of "one" and two is assigned the value of (1 2 3).

p-himik11:08:57

https://clojure.org/guides/learn/functions#_variadic_functions Such questions about fundamentals are better suited for #beginners

dabrazhe11:08:38

I was sure that 1 2 3 becomes a vector, for some reason )

dabrazhe11:08:01

And it's actually clojure.lang.ArraySeq

dabrazhe11:08:56

Why is it trying to evaluate the clojure.lang.ArraySeq as a function, when assigning it to two?

p-himik11:08:40

Macros produce code, not data. What happens when you write (1 2 3) in Clojure? Just in the REPL or as a line of code on its own. It blows up with the same exception because 1 is not callable.

dabrazhe11:08:34

This is the macro's behaviour then, alright

p-himik11:08:50

Of all macros, not just that macro.

👍 3
kennytilton12:08:57

Not sure how much code I have to show for this one, but I sure have a lot if needed. 🙂 I just think this comes down to a simple question. I am writing a macro to define macros. I have done this before, so I think I have the syntax down, (Old Lisper here.) But in this case, I want the macro to expand into sth referencing a namespaced type, ReactNative to be specific: (def-view-macro View rn/View) ie, I want a View macro that will expand into code referencing a React Native (rn) View (in a create-element, fwiw). The problem: Syntax error compiling at (src/myapp/mxrgen.clj:41:1). No such namespace: rn No doubt. The reader tries to read rn/View before even looking at def-view-macro. OK, in a CLJS I can require ["react-native" :as rn], but macros are authored in CLJ files, where that "require" will not hunt. I think the solution would be to generate the "rn/View" token in the expansion in a way that (a) is recognized as an rn type when invoked in a CLJS expansion, but which never asks CLJ to recognize the generated "rn/View" as a namespaced token. ie, I want rn/View in the expansion without letting CLJ see it. Am I doomed, or is there some ' magic that I am missing?

donavan13:08:18

Can it possibly be done without aliasing the ns? so 'full.react-ns/View in the macro phase? I think that’s how I’ve done what I think you’re trying to do before

kennytilton14:08:16

Thx, @U0VP19K6K. I considered that, but did not know how to translate ["react-native" :as rn] to sth un-aliased. tbh, I am not familiar with that syntax, a string naming the required component. 🤷

p-himik14:08:23

Ah, that's an interesting problem then. @U05224H0W Your article above doesn't mention such requires. Do you perhaps have any recommendations?

Ed14:08:25

can you expand into cljs function calls that then call rn/View??

donavan14:08:37

@U0PUGPSFR Ah, that’s a good point! I never did this with npm modules so I’m not sure either I’m afraid.

donavan14:08:58

There may be a format that is not string based…

kennytilton14:08:51

Great minds think alike! But then my code looks like, say, (mxn/View rn/SafeAreaView etc etc) and when I get to other widgets it would be (mxn/Widget rn/Switch ...). Hmmm, maybe I can go DRY differently. This is for passing to the helix.core/$ macro which does accept strings and keywords for its own magic with "div". I have a question into @U4YGF4NGM in the #helix channel as well.

ghadi14:08:51

there is not enough information here. Need to show definition of def-view-macro

ghadi14:08:24

there is no problem if the reader reads a symbol rn/View

kennytilton14:08:28

Wait, strike that, I was hacking at it ^^^

kennytilton14:08:02

Ok, this is where I started: (defmacro def-view-macro [type rn-type] `(defmacro ~type [mx-props# jsx-props# & children#] `(tiltontec.model.core/make :myapp.mxreact/mxrn.elt :sid (swap! myapp.mxreact/sid-latest inc) :kids (tiltontec.model.core/cFkids ~@children#) :rendering (tiltontec.cell.core/cF (prn :creating ~'rn-type#) (helix.core/$ (mxfnc (apply helix.core/$ rn-type ~jsx-props# {} (doall (map #(tiltontec.model.core/mget % :rendering) (tiltontec.model.core/mget ~'me :kids))))))) ~@(apply concat (into [] mx-props#))))) Usage (def-view-macro View rn/View) fails on "No such namespace: rn"

kennytilton14:08:20

Not surprised the reader cannot resolve "rn", tbh, since the alias is not defined in this clojure file.

lilactown14:08:08

I have tried to go down this route before and run into issues with macro-defining-macros

lilactown14:08:33

not saying it can't be done but I gave up, the interaction between the Clojure macro namespace and the CLJS namespace was too complicated and hard to resolve

kennytilton14:08:51

Yeah, macro-defining-macros are fun. I did OK in mxWeb because I never had to reference a type: I just needed strings to get to the DOM manipulation I was doing. Thx for confirming I am doomed. Copy and paste here I come! :)

donavan15:08:13

Yeah, the only way I could get it to work was to always keep CLJS NSs fully qualified as plain symbols but those were Clojure modules not NPM ones

donavan15:08:10

Probably worth asking on Clojurescript channel though

thheller15:08:53

FWIW if you develop the macros using the recommended companion CLJS namespace then you can put your :require in that and setup whatever symbols you need there

thheller15:08:54

getting the string aliases is also possible but a bit more involved

lispers-anonymous19:08:12

When using (:require ["react-native" :as rn]) you aren't really getting an alias that is resolving to something called "react-native" you are getting a javascript object, right? Could you pass in a function that returns whatever rn/View is? like a really simple #(.-View rn) And then use that in your macro instead? The idea would be to defer evaluation until it's executed in js land and not in clojure, which seems like what is happening now. I have never attempted to go this far with macros before so no idea if it would work.

kennytilton23:08:20

Well, @UDVJE9RE3, the Clojure Gods did not suggest that, so... how would my macro generate the .-View? This isn't Lisp, you know. 🙂 Good compromise found, however: https://clojurians.slack.com/archives/CRRJBCX7S/p1629932400134600 It does do what you suggest, reaching out for the rn/View in the expanded code once in CLJS-land. Heading out for 🍾 ....

lispers-anonymous00:08:40

I'm glad you've found something that works! To answer this > how would my macro generate the `.-View` If you know that you want to use some property called View on a javascript import, then you have a symbol to work with. You can build the .-View symbol in clojure. Consider this macro, dumb example, but for illustration

(defmacro field-extractor [obj field]
  (let [get-field (symbol (str ".-" field))]
   `(~get-field ~obj)))
If I've imported a js library like so
(:require ["react-select" :as react-select])
I can access any value on that imported react-select object by passing in the object and the property name separately
(e/field-extractor react-select Creatable) ;; => #object[CreatableSelect]
You could consider something like that for your code. This seems like it would offer you some more flexibility, no need to have a map to translate :View -> rn/View , and you aren't locked into using rn as your alias for the "react-native" import in all the namespaces that use your macro.

lispers-anonymous00:08:23

This might be more like your example

(defmacro lift-js-fn-to-ns [obj field]
  (let [get-field (symbol (str ".-" field))]
    `(defmacro ~field [x#] ;; could easily just be a `defn`
      ((~get-field ~obj) x#))))
Using (:require ["moment-js" :as moment]) to work with a function on moment called utc
(macroexpand-1 '(lift-js-fn-to-ns moment utc))
;; => (clojure.core/defmacro utc [x__280962__auto__] ((.-utc moment) x__280962__auto__))
Then calling it defines the macro in my namespace
(lift-js-fn-to-ns moment utc)
;; => #'example/utc
And then I can invoke it with
(utc "2020-01-02") ;; => #object[Moment Thu Aug 26 2021 00:39:34 GMT+0000]
Pretty neat!

kennytilton07:08:57

Haha, that approach occurred to me but I thought, as I said ^^^, "Ha, this is not Lisp!". And I have played with Clojure symbols but was always disappointed, so I was even less incline to explore that path! I'll give it a try. Thx!

💯 3
catjam 3
grzm14:08:05

clojure.tools.logging lets you set the logging implementation using the clojure.tools.logging.factory Java property, and includes some corresponding implementations out of the box (frex, clojure.tools.logging.impl/slf4j-factory). There's also a clojure.tools.logging.impl/disabled-logger-factory, which would be nice to use in this same way. However, clojure.tools.logging.impl/disabled-logger-factory define the factory itself, while the others define a 0-arity function that returns the factory; the latter is the behavior expected by the value set for the clojure.tools.logging.factory property. This seems like an oversight in the tools.logging implementation. Is a 0-arg function that returns the disabled-logger-factory something that would be reasonable to add clojure.tools.logging? I can add my own function to do this, but it seems like a better fit in the library itself.

p-himik14:08:58

I'm writing a Clojure implementation for a Python library that's a heavy user of inheritance, sometimes multiple, along with a plethora of isinstance checks. I want to avoid shuffling it too much as I want to keep the Clojure version up to date with the Python one, so I'm trying to structure the code in at least somewhat similar way. So far, my only idea about mimicking this multiple inheritance with isinstance checks is to add a keyword for each type, derive the types from them, derive keywords from each other, and implement a version of isinstance that would use isa?. Is there a better approach?

p-himik15:08:26

I guess an alternative would be to make all predicate functions that check for isintance in the original code, like is-data-range? , multimethods, and define them as true only for specific classes that were created by defrecord .

lgessler15:08:56

is it not feasible to depart from the original design somewhat and just rely on multimethods on ordinary maps? that'd be my first instinct

p-himik15:08:25

> somewhat This particular part is not feasible, I'm afraid. The isinstance checks are everywhere.

lgessler15:08:09

yow, that's unfortunate. just curious, what's the python lib?

p-himik15:08:19

It heavily relies on classes, hierarchies, metaclasses, property descriptors - few of the many reasons that made me stop using Python. :D But the library itself is great, especially because it has a JS counterpart that just doesn't have alternatives IMO.

noisesmith16:08:14

> mplement a version of isinstance that would use isa? yes, if you use derive to define the hierarchy, isa? should do exactly what isinstance did, alternatively you can use "marker interfaces" (interfaces with no methods), and implement the apropriate interfaces on each record based on what isinstance should return

noisesmith16:08:07

of course you can use protocols instead of interfaces, with no methods to define it comes out about the same I think

emccue16:08:04

I have a somewhat cursed impl of python's classes in clojure - if you want to directly translate

p-himik16:08:56

Do you have any recommendation, along with the "why"? So far: • Use keyword hierarchy with derive and isa? • Make the predicates into multimethods that dispatch on the class of the record • Create marker interfaces/protocols

p-himik16:08:58

@U3JH98J4R I'm somewhat curious but at the same time 80% sure that won't work, unless it's more than somewhat cursed. :D The library heavily relies on metaclasses and property descriptors as well, so I have to adapt, I don't see how I could directly translate that.

noisesmith16:08:22

(definterface IAnimal)

(definterface ISilent)

(definterface ILarge)

(definterface IHarmless)

(defrecord Panther [color]
  IAnimal
  ISilent
  ILarge)

(let [panther (->Panther "black")]
  {:animal (instance? IAnimal panther)
   :silent (instance? ISilent panther)
   :large (instance? ILarge panther)
   :harmless (instance? IHarmless panther)})
{:animal true, :silent true, :large true, :harmless false}

emccue16:08:29

multiple inheritance you could do. but not much more than that

emccue16:08:08

also have you tried "just" using libpython-clj?

noisesmith16:08:12

@U2FRKM4TW each of those three can accomplish what you want, I think the question to answer is which of them has lowest friction to translate from python and keep in sync - are the properties that isinstance checks defined when defining the class (marker interface), are they added later from some other procedure? (defmethod)

noisesmith16:08:04

using derive / hierarchy is probably the most flexible on the clojure side, but least likely to look like the python code

p-himik17:08:24

Thanks! Since the code isn't supposed to be that flexible, marker interfaces should do. And now I recall that Clojure itself uses them. @U3JH98J4R Nope, and I definitely won't. I don't like Bokeh's Python API at all. It's immensely overloaded IMO and has a lot of "magic", making it brittle and hard to debug.

Joshua Suskalo16:08:13

I'm trying to do the following to allow an optional dependency in my library, but whenever I require this namespace, it's telling me the fmnoise.flow namespace is not found. Is there something more I have to do here to make this work?

(try
  (require 'fmnoise.flow)

  (extend-protocol fmnoise.flow/Catch
    Signal
    (caught [s] (throw s)))

  (catch Exception _))

Joshua Suskalo16:08:10

It seems to me like this should work, and it's similar to what I've seen elsewhere, but for whatever reason the compiler just doesn't like the reference to the protocol.

Alex Miller (Clojure team)16:08:21

it resolves that protocol name at compile time. another way to go with this is to put the extension in a separate namespace and optionally decide to load it

Joshua Suskalo16:08:38

Aaah, that's a good thought. Thanks alex!

darshanime16:08:22

hi 👋 i’m trying to integration clojure.spec into my project and want the spec/nstrument to be activated for all specs.

:instrument {:injections [(require 'myservice.main)
                                       (require 'orchestra.spec.test)
                                       (orchestra.spec.test/instrument)
                                       (println "~~~~~~~~~~~~~~ ran")]}
I have this 🔝 in my project.clj, lein with-profile instrument test :only myservice.foo-test errors out with an invalid spec as expected but this does not:
lein with-profile instrument cloverage

darshanime16:08:08

I see

Error encountered performing task 'cloverage' with profile(s): 'instrument'
Suppressed exit
in lein output, how do I see what’s wrong?

noisesmith16:08:40

@darshanime usually you want with-profile +foo instead of with-profile foo - the cloverage task might define important things in its profile that it won't get if you replace its profile

noisesmith16:08:04

(the difference between foo / +foo is replacing config vs. merging configs)

darshanime16:08:30

tried it, but I still don’t get the spec failure I expected

Error encountered performing task 'cloverage' with profile(s): 'base,system,user,provided,dev,instrument'
Suppressed exit
anyway to check the error mentioned above?

noisesmith17:08:53

so is the issue here that either lein or cloverage is hiding the error you expect to see rather than showing it?

darshanime17:08:32

yeah, i have a clojure test that should fail (if function args are being tested) since i’m passing an invalid spec which should fail. when using cloverage, the failure doesn’t happen…

noisesmith17:08:40

also, what value is added by doing spec checking during coverage inspection? why not do these as separate tasks?

darshanime17:08:15

hmm, do you recommend doing lein test first and then lein cloverage?

darshanime17:08:25

even with

lein with-profile +instrument test :only ...
i get the test failure as expected, but still see
Ran 1 tests containing 2 assertions.
0 failures, 1 errors.
Tests failed.
Error encountered performing task 'test' with profile(s): 'base,system,user,provided,dev,instrument'
Tests failed.

noisesmith17:08:30

depending on context yeah - you can make a custom alias using do that executes them in order of that's a common need - like the last one in the sample project.clj

:aliases {"launch" ["run" "-m" "myproject.main"]
            ;; Values from the project map can be spliced into the arguments
            ;; using :project/key keywords.
            "launch-version" ["run" "-m" "myproject.main" :project/version]
            "dumbrepl" ["trampoline" "run" "-m" "clojure.main/main"]
            ;; :pass-through-help ensures `lein my-alias help` is not converted
            ;; into `lein help my-alias`.
            "go" ^:pass-through-help ["run" "-m"]
            ;; For complex aliases, a docstring may be attached. The docstring
            ;; will be printed instead of the expansion when running `lein help`
.
            "deploy!" ^{:doc "Recompile sources, then deploy if tests succeed."}
            ;; Nested vectors are supported for the "do" task
            ["do" "clean" ["test" ":integration"] ["deploy" "clojars"]]}

noisesmith17:08:08

try lein check - all that does is load up all clj files in source path, and then show any errors / reflection warnings that come up

noisesmith17:08:15

at least with lein do ... you can execute your separate subtasks without spinning up lein again each time

darshanime04:08:07

thanks. also i get the error thingy now, it’s working as expected. cloverage is meant to give you the coverage, and it https://github.com/cloverage/cloverage/issues/296 any test errors to not uglify its output

John Conti20:08:28

During a talk on transducers Rich joked that “you wouldn’t try to do IO in a transducer”. I can understand why doing IO on one of the threads realizing a transducer would be a bad idea. So the question then is, if one has a IReduce (jdbc reducible query) and one wants to process it with a function that will do IO, is transduce the wrong idea, but using reduce is the right idea?

John Conti20:08:01

And does this mean using doseq would process it like a lazy sequence (retaining the head) instead of how transduce or reduce would?

noisesmith20:08:56

why would doseq ever retain the head of anything?

noisesmith20:08:55

you could still use transduce to do your IO, just do it in the reducing function, not the transducer

3
noisesmith20:08:30

the transducer is there to process your data (data in, data out) and isn't the right place for side effects

☝️ 3
ghadi21:08:35

...just a nit on terminology, transducers don't process data

ghadi21:08:48

transducers transform reducing functions, which process data

💯 3
noisesmith21:08:02

right, thanks

ghadi21:08:11

and as noisesmith said, IO is absolutely fine in a reducing function

seancorfield21:08:13

Also, you can't use doseq on an IReduceInit.

seancorfield21:08:24

So, yes, if you need to process a reducible query, either reduce or transduce are fine, even if your actual reducing function is doing I/O.

seancorfield21:08:44

(or any other function that will eagerly process an IReduceInit)

didibus21:08:43

Are IReduceInit not seqable?

didibus21:08:09

Ok, I guess not

didibus21:08:44

Seems it be possible to implement first, rest and next using reduce :thinking_face:

noisesmith21:08:21

#(reduce conj [] %) ?

noisesmith21:08:49

eagerly consumes, gives you a seqable

noisesmith21:08:55

I guess that's just into

✔️ 3
John Conti21:08:19

I think with doseq I was suggesting what @U04V70XH6 said above, the doseq would avoid IReduceInit and instead just eagerly consume the whole result set. But then my thought there was that even though the doseq might be computing a reduction, it would keep all values of the sequence until it exited. This thread has helped a lot. Thank you. The reduction function makes perfect sense. The transducer transforms, and the reducer sends the result to a sink with IO as one option.

noisesmith21:08:51

I wouldn't expect doseq to ever hold onto a value it is done processing, though if you used the dumb reduce conj I suggested above to get a seqable for doseq it would hold the head

John Conti22:08:36

I quess then I wonder if sequence would produce a lazy version of the IReduceInit useful if one were consuming it and say writing a textual representation to a socket or other channel that might be slower.

didibus23:08:38

Sequence will, but it'll still be chunked, so you got to be careful with IO

didibus23:08:22

Now I'm not sure about head retention with it :thinking_face:

seancorfield23:08:15

You can't call sequence on an IReduceInit either.

seancorfield23:08:43

Easy way to check in the REPL:

dev=> (def x (reify clojure.lang.IReduceInit (reduce [_ f init] [])))
#'dev/x
dev=> (doseq [y x] (println "Hi!"))
Execution error (IllegalArgumentException) at dev/eval179328 (dev.clj:1).
Don't know how to create ISeq from: dev$reify__179322
dev=> (sequence x)
Execution error (IllegalArgumentException) at dev/eval184697 (dev.clj:1).
Don't know how to create ISeq from: dev$reify__179322

👀 3
💯 3
didibus23:08:41

Hum... can you even reduce over it?

didibus23:08:55

I get: > java.lang.ClassCastException: sandbox8122$reify__8761 cannot be cast to clojure.lang.IReduce

didibus23:08:57

Oh, haha, ok you can only if you provide an init value to reduce 😛

didibus23:08:40

Ya, if something is exclusively an IReduceInit and nothing else, seems all you can do is use reduce (or transduce) with passing it an init value.

didibus23:08:52

Just so used to having collections implement so many more things, take it for granted

walterl23:08:58

From the peanut gallery: this was a very informative thread. Thanks all! 🙌

seancorfield23:08:06

@U0K064KQV Just to show with IReduce:

dev=> (def x (reify clojure.lang.IReduce (reduce [_ f] 0) (reduce [_ f init] 1)))
#'dev/x
dev=> (doseq [y x] (println "Hi!"))                      
Execution error (IllegalArgumentException) at dev/eval184916 (dev.clj:1).
Don't know how to create ISeq from: dev$reify__184910
dev=> (sequence x)                                                             
Execution error (IllegalArgumentException) at dev/eval184920 (dev.clj:1).
Don't know how to create ISeq from: dev$reify__184910
dev=> (reduce + x)
0
dev=> (reduce + 42 x)
1

seancorfield23:08:51

(but since Rich says IReduce was a mistake, I don't tend to use it)

didibus02:08:54

I looked into that actually, and I found an article about category theory lol. It seems that reduce with an init is a catamorphism, and that makes it it can handle reducing things that don't have a neutral element, or reducing into a different type than the type of the elements. And I guess from a programming standpoint, reduce without an init for those cases would error on an empty collection, and would be possibly unintuitive in the case of collection of length 1

didibus03:08:44

I think the last two might be why Rich thinks it's a mistake.

jaihindhreddy12:08:56

For anyone interested, Rich talks about IReduce being a mistake, in his Inside Transducers talk, shortly after https://youtu.be/4KqUvG8HPYo?t=1753, where he says the semantics of reduce was one of the worst things he copied from Common Lisp 😄

👀 3
John Conti14:08:18

Well, actually in @U11BV7MTK’s link to those transducers, it is the transducer that is doing the IO (in the case of ‘.’ printing). This is specifically what has been (believably) pointed out to be a bad idea. Now I do not think this would actually cause a problem. But if one replaces print a dot with doing 1000's of records of IO on a shared resource, it looks like a big problem.

dpsutton14:08:45

yeah context on this one is important. It is in an example repo, on a command line style problem (migrating data). I think its clever, and its IO is just spitting out to std out.

John Conti15:08:19

To wrap up (maybe not, but let’s see) this great thread (thank you all who participated). I over-reach with this, which is mostly intentional, in an effort to flush out any details that didn’t organically surface in the conversation: 1. Doing IO in a transducer is putting a suspendible, memory allocating, and long running operation on a background thread pool designed for the opposite. There is no contractural guarantee to service that IO with care (or even preserve the exceptions for you, I can say with some confidence 😉 ). Therefore do not do IO in transducers. 2. But wait, there is more! The reducer argument to transduce is the same as good old reduce , use it do IO after the transformation. To do IO before the transformation simply use map etc. 3. There is still lots of machinery in sequences and core.async that will happily service IO requests. I find allocating a pool specifically for this is helpful . 4. Libraries for things like Rx (I miss beicon for the JVM) have similar IO pool allocating strategies for operators that will do IO.

John Conti15:08:54

Super big thanks @U04V70XH6 for the dive into the interfaces, outstanding! And more thanks for @U0K064KQV for rounding out all the comments and filling in the gaps. I really appreciate the good help.

John Conti18:08:50

Also I would add the lore associated with IReduce is very enjoyable. Totally new info (and useful background in understanding the design rationale, not always easy to reverse out of a design).

jaihindhreddy19:08:01

The one thing that still bugs me a little, is the fact that IReduceInit eats reduced i.e, on early-termination, the implementation is supposed to return the final value, as opposed to the clojure.lang.Reduced wrapper object (Rich talks about this in the same talk). I dunno why IReduceInit was designed this way, as opposed to having the implementations return the wrapper, which would make more information available to callers.

didibus20:08:08

You'd return the reduced to the caller of reduce?

noisesmith20:08:38

it seems odd to return a reduced, but nothing prevents you from returning (reduced (reduced v))

jaihindhreddy08:08:43

I believe the way to implement IReduceInit is to respect reduced and terminate early, but when this happens, return the value in the reduced wrapper, and not the wrapper object itself. reduce can remain this way (i.e, not return reduced wrapper) but the interface contract can be to return reduced. As noisesmith pointed out, we can of course double wrap and detect early-termination, perhaps yielding a spin on ensure-reduced that ensures something is reduced twice, or not at all. Doesn't seem as elegant TBH. Something tells me this way is more error prone.

John Conti15:08:01

☝️Which would certainly be useful in the case of an actual reduce doing IO, but maybe less so on a transducer that is pure and processing data in a pipeline (and hence “unlikely” to exit early unless it throws).

dpsutton13:09:43

@U07S8JGF7 this is the thread