Fork me on GitHub
#clojure
<
2024-04-03
>
Eugen09:04:18

hi, I am porting some go code to clojure and woud like to know how/if I can implement the go pattern of checking things as I go and stopping processing once a condition fails. Go seems to use multiple returns to solve this issue and they just return error. But with clojure not having a "return" statement, how would I go about this ? I guess my main interest is: how do I stop doing work once I know it's no use going forward, in a processing flow that is procedural ?! For people interested in sharing solutions: I prefer easy to read and reason than one liners / macros . https://github.com/slackhq/nebula/blob/bbb15f8cb1ecdc7e423ffd1a85a3fc8c0898bf95/cert/cert.go#L694

Eugen09:04:39

I considered cond-> but it does not short circuit

p-himik09:04:46

There have been plenty, plenty of discussions on error handling and early returns. Feels like the most recent was just a week ago.

Eugen09:04:46

thanks, I was going to look for it. thank you for the ling

Eugen09:04:13

seems to be a common issue I guess - maybe docs can be improved / patterns documented ?!

p-himik09:04:50

Also, since that Go function seems to return either an error or a nil, the conversion to Clojure is trivial and doesn't require any gimmicks or particular approaches. Just use (if-let [error (do-stuff)] error (... do more stuff ...)).

p-himik09:04:34

> maybe docs can be improved / patterns documented ?! Docs PRs are always welcome. :) But note that there's no "the" approach.

Eugen09:04:15

not sure if it's that simple with the error or nil . Will have to think about it. Thanks for the suggestion though

Eugen09:04:46

> Docs PRs are always welcome. :) But note that there's no "the" approach. I'm currently trying to formulate the thing in my head, not ready for a PR 🙂 But I do have a feeling that it's something recurring => opportunity .

p-himik09:04:02

FWIW, here's how I'd write something like that there. Used or instead of if-let and used a vector to return two values in one case. IMO it's even easier to reason about then the original.

(defn verify-private-key [curve ^bytes key]
  (if (not= curve ns-details/curve)
    "curve in cert and private key supplied don't match"
    (if nc-details/ca?
      (case curve
        :Curve_CURVE25519
        (when (not= (alength key) ed25519/private-key-size)
          ;; Why do they use a constant to check for the length
          ;; but a hard-coded value to report a mismatch?
          "key was not 64 bytes, is invalid ed25519 private key")

        :Curve_P256
        (let [[priv-key err] (.NewPrivateKey (ecdh/P256) key)]
          (if err
            "cannot parse private key as P256"
            (let [pub (-> priv-key (.PublicKey) (.Bytes))]
              (when-not (bytes-equal? pub nc-details/PublicKey)
                "public key in cert and private key supplied don't match"))))

        (str "invalid curve: " curve))
      (let [[pub err]
            (case curve
              :Curve_CURVE25519
              (curve25519/X25519 key curve25519/Basepoint)

              :Curve_P256
              (let [[priv-key err] (.NewPrivateKey (ecdh/P256) key)]
                [(when-not err (-> priv-key (.PublicKey) (.Bytes))) err])

              [nil (str "invalid curve: " curve)])]
        (or err
            (when-not (bytes-equal? pub nc-details/PublicKey)
              "public key in cert and private key supplied don't match"))))))

p-himik09:04:36

Could also remove some unnecessary duplication, but didn't want to deviate from the original too much.

Eugen09:04:42

thank you

p-himik09:04:33

And I find such things more often than one would expect, where using the simplest, dumbest solution on top of immutable data and absence of arbitrarily placed returns makes things easier to reason about. Not too long ago, I had to rewrite MusicXML parser and validator from Python to Clojure. Took me a bit because the original code was ported from C++ and used a lot of variable overrides, mutation, early returns. In the end, the code became simpler and I found a few bugs that were caused by a mismatch of the initial authors' expectations and what the MusicXML format allows.

p-himik09:04:47

Of course, I'm not saying that it's always the case. Some algorithms' implementations do benefit from less functional approaches.

Eugen09:04:59

I kind of feel that too. my brain is wired less functional, and like you said: some situations are easier to implement less functional and maybe then improve

Daniel Galvin10:04:26

When i am doing things that have multiple IO ops and can fail at somepoint during execution of a series of functions. i try to leverage this idea https://medium.com/appsflyerengineering/railway-oriented-programming-clojure-and-exception-handling-why-and-how-89d75cc94c58 basically make a macro which returns result and error, if error is true, just propagate it. you wrap in a try catch and you can handle the scenario in a readable way

👆 2
Eugen11:04:36

yep. (with-pirate-voice "I've been serchin' all my life for this ARRRgh" ). IMO this shold be part of clojure.core Thank you @U069RSXM1CP . Seems like there are a few libraries implementing the concept: one is https://github.com/druids/rop

Daniel Galvin11:04:59

Oh nice 😄 ive not even considered using a library for this haha ill take a look

Eugen11:04:34

there are a few, I am still exploring

Daniel Galvin11:04:41

i think ive seen https://github.com/adambard/failjure in a project before 😄 another ROP impl

Eugen11:04:32

yep, failijure looks quite good. cljs support, optional specs, ok syntax

Noah Bogart13:04:10

makes me wish we were on clojureverse forums just so we could point everyone to the same thread lol

👍 2
p-himik14:04:35

How is that different from pointing to a particular message here?

Noah Bogart14:04:07

bumping/pinning threads in forums is more obvious than searching for specific threads in slack.

p-himik14:04:46

Slack also has pinning. :) No clue though whether it's limited in some way or not.

Alejandro17:04:56

Would better-cond work?

Eugen18:04:01

I'm focusing on failjure now. seems to be good for what I am trying to achieve . Will know more once I've written more code with it

Eugen18:04:09

thanks for the suggestion though

Alejandro18:04:11

Same idea, but less monadic and more exceptionic

1
slipset19:04:43

There was a discussion over in #clojuredesign-podcast as well https://clojurians.slack.com/archives/CKKPVDX53/p1712678059534529

1
pieterbreed13:04:29

Hi all, I just ran into a situation where this issue affected me and wanted to mention it here. (https://clojure.atlassian.net/browse/CLJ-2794) This relates to gen-class not implementing the default implementations on interfaces correctly. What happened for me was that the kafka client library has an interface which in a recent update got a new overload, with a default implementation calling the old overload. Because of this issue in gen-class, the default implementation did not fire and caused a break on my side. What was a backwards-compatible change for other JVM languages, was a breaking change for me. ☮️

Alex Miller (Clojure team)13:04:28

don't think we are going to address in 1.12, but it's in my list for 1.13

pieterbreed13:04:54

thank you for the update

pieterbreed14:04:25

btw - in gen-class you can do this kind of thing for overloads with same arity but different parameter types: prefix-methodName-ParamType1-ParamType2 and so on. What is the correct ParamType for byte[]? I used prefix-methodName-String-bytes. Will this work like I expect?

Alex Miller (Clojure team)14:04:05

I doubt it :) off the top of my head, don’t know, that is implementation detail, not part of public api of gen-class

pieterbreed15:04:42

It might not be part of the public API but it's for sure part of the implied API, if StackOverflow is anything to go by. It solves a problem people have with interop. For future me's benefit: It would be prefix-methodName-String-byte<> to match an overload of methodName that takes a String in first position and a byte-array in second position.

pieterbreed15:04:00

Anyway - thanks for input 👍:skin-tone-4:

Alex Miller (Clojure team)15:04:29

it's quite likely for that expectation of internals to be broken in 1.12 with the new class array syntax

pieterbreed15:04:07

that is a little bit frightening.

Alex Miller (Clojure team)15:04:41

well, these are internals

Alex Miller (Clojure team)15:04:13

I have not seen the changes so far affecting anyone in this way, but we are redo-ing a lot of it right now, not sure if that will affect things. it's possible that syntax there is coming from clojure.java.reflect, which is not changing, in which case nothing would change

Alex Miller (Clojure team)15:04:56

I looked at the code, it's not based on either of these so shouldn't change

phill09:04:40

I remember at a conj one of the Clojure luminaries commented "there is no dishonor" in stepping back to the host language to meet interop constraints. Perhaps instead of relying on gen-class internals, this occasion calls for a solution that doesn't use gen-class!

jpmonettas13:04:48

I'm having a hard time getting rid of a reflection warning around a JavaFX ListCell and proxy-super, not sure if it is possible. Already asked this in the past but the solutions didn't work. Details on the 🧵 ...

jpmonettas13:04:06

I'm implementing something like this :

public class MyCell extends ListCell {

    public MyCell() {    }

    @Override protected void updateItem(Number item, boolean empty) {
        // calling super here is very important - don't skip this!
        super.updateItem(item, empty);
        ...
        }
}
and this are the two things I've tried :
clj -Sdeps '{:deps {org.openjfx/javafx-controls {:mvn/version "21.0.3-ea+1"}}}'

user=> (import 'javafx.scene.control.ListCell)
user=> (set! *warn-on-reflection* true)
user=> (defn list-cell-factory []
         (proxy [ListCell] []
           (updateItem [item empty?]
             (proxy-super updateItem item empty?))))

Reflection warning - call to method updateItem can't be resolved (target class is unknown).
and also :
clj -Sdeps '{:deps {org.openjfx/javafx-controls {:mvn/version "21.0.3-ea+1"}}}'

user=> (import 'javafx.scene.control.ListCell)
user=> (set! *warn-on-reflection* true)
user=> (defn list-cell-factory []
         (proxy [ListCell] []
           (updateItem [item empty?]
             (let [^ListCell this this]
               (proxy-super updateItem item empty?)))))

Reflection warning - call to method updateItem on javafx.scene.control.ListCell can't be resolved (no such method).

jpmonettas14:04:05

I'm starting the repl from scratch before trying since there is some stateful stuff going on this ListCell static initializers and it complains about toolkit not initialized in this contrived example if you eval the proxy twice

Noah Bogart14:04:00

Can you just write this in java?

jpmonettas14:04:06

well, I mean it works in Clojure, my question is about this kinds of reflections, trying to see if there is a way

dpsutton14:04:09

i had an issue about this a while back.

dpsutton14:04:24

the answer turned out to be related to public/protected/private

dpsutton14:04:49

ah. i think that’s the same issue and it’s me and hiredman answering.

dpsutton14:04:59

(me asking and hiredman answering i mean)

jpmonettas14:04:25

oh I see, so the reflection can't be avoided

jpmonettas14:04:51

I guess we should have a reflection skip meta system then for this cases, (ala :clj-kondo/ignore), so we can keep or reflections log clean

Ravi15:04:34

Hi I am trying to use malli to validate the input LocalDate string in params I did try to use malli.experimental.time but seems like it cannot handle the localDate string Ideally i used regex ealier but this breaks the Swagger doc as json is not able to convert regex. So if someone has some experiance with this that will be helpful

(def some-obj
  [:map
   [:rate [:maybe number?]]
   [:myDate [:maybe [:re #"^\d{4}-\d{2}-\d{2}"]]]
   [:myDate2 [:maybe [:re #"^\d{4}-\d{2}-\d{2}"]]]])

Want to validate localDate string like "2020-12-02"

Samuel Ludwig15:04:49

#malli/maybe #ring-swagger is probably going to have a more knowledgeable base for this 😄

Ben Sless15:04:03

You can't validate a string with a predicate for a type, you need to coerce first

Ben Sless15:04:14

Is this for http?

1
p-himik15:04:16

I just tried with :query [:map [:a [:re #"a"]]] being in :parameters of an endpoint. The picture is what I get in the generated Swagger UI for the field.

p-himik15:04:18

@UK0810AQ2 The input is a string, as per the last line in the code block.

yes 1
Ben Sless15:04:19

Since you mention swagger I assume it is, you need a time transformer https://github.com/metosin/malli/blob/master/src/malli/experimental/time/transform.cljc

p-himik15:04:41

To add to my first comment - I use the latest alpha versions of all the relevant packages.

Ben Sless15:04:16

When configuring the coercion, compose a transformer for json and time formats

👀 1
Ravi15:04:40

@UK0810AQ2 can u please help me with a small eg !

Ben Sless15:04:55

I need to find an example for configuring transformers

Ben Sless15:04:17

When creating the coercion, update the default opts https://github.com/metosin/reitit/blob/master/modules/reitit-malli/src/reitit/coercion/malli.cljc#L84 Stick in your own provider

Ben Sless15:04:39

You can use the private provider var

Ben Sless15:04:53

Or provide your own transformer

Ravi15:04:44

Swagger Error

Caused by: com.fasterxml.jackson.core.JsonGenerationException: Cannot JSON encode object of class: class java.util.regex.Pattern: ^\d{4}-\d{2}-\d{2}

lukasz15:04:13

Best to move this to #CLDK6MFMK but... You need to register a custom encoder for Regex class in Cheshire: https://github.com/dakrone/cheshire?tab=readme-ov-file#custom-encoders

Ravi15:04:48

we are using charred for json implementations we still need to make changes in Cheshire ?

lukasz15:04:25

because you're getting an error from Jackson, it would suggest that the swagger generator uses Cheshire internally, what Json library your own code is using has no influence

🆗 1
seancorfield17:04:55

Also @U06C2P5N21L Please don't post multiple top-level messages in a channel about the same issue. Post details in a thread to keep the communication here easier for everyone to follow and respond to.

Ravi20:04:07

@U04V70XH6 noted, thanks !

seb23116:04:48

Hello any help with date parsing? At the moment this

(java.time.LocalDate/parse "01-Jul-99" (java.time.format.DateTimeFormatter/ofPattern "dd-MMM-yy"))

seb23116:04:04

>#time/date "2099-07-01"

seb23116:04:27

whereas I'd like #time/date "1999-07-01"

lukasz16:04:29

Why obviously? There's nothing in the input string that would help the date parser to determine if the year is 1099, 1999 or 2099

☝️ 1
daveliepmann16:04:49

Docs for https://docs.oracle.com/javase/10/docs/api/java/time/format/DateTimeFormatter.html says yy is a year type, which is defined thus: > Year: The count of letters determines the minimum field width below which padding is used. If the count of letters is two, then a reduced two digit form is used. For printing, this outputs the rightmost two digits. For parsing, this will parse using the base value of 2000, resulting in a year within the range 2000 to 2099 inclusive.

daveliepmann16:04:02

looks like there's a way to override that setting but it's a little gnarly https://stackoverflow.com/a/32783553/706499

seancorfield17:04:26

Also @U0KGSPTGB Please don't post multiple top-level messages in a channel about the same issue. Post details in a thread to keep the communication here easier for everyone to follow and respond to.

seb23119:04:57

Noted @U04V70XH6 👍 Thanks for the tips @U05092LD5

👍 1