Fork me on GitHub
#beginners
<
2022-09-11
>
sp16:09:42

Hello! I have a 2 part question. 1. What is an idiomatic way to get the nth offset of every element in a vector? For example, I want every 2nd element starting from index 0 of the first item, 4th element of the second item, 6th element of the third item etc etc.:

(func ["abcxxxyyy" "defxxxyyy" "ghixxxyyy"] 2)
#_=> [\c \x \y]
2. Given part 1, how do I cycle properly if the index is out of bounds?
(func ["abcxxxyyy" "defxxxyyy" "ghixxxyyy"] 4)
#_=> [\x \y \x]

Martin Půda17:09:00

Try this:

(defn offset [v n]
  (->> (iterate #(+ % n) n)
       (mapv #(nth %1 %2) v)))

(offset ["abcxxxyyy" "defxxxyyy" "ghixxxyyy"] 2)
=> [\c \x \y]

(defn func [v n]
  (->> (iterate #(+ % n) n)
       (mapv #(nth %1 %2) (map cycle v))))

(func ["abcxxxyyy" "defxxxyyy" "ghixxxyyy"] 4)
=> [\x \y \x]

Bob B17:09:42

map-indexed , nth, and cycle can be put together (with some math) to accomplish this as well

👍 1
Martin Půda17:09:02

(defn offset [v n]
  (map-indexed (fn [i s] (nth (cycle s) (* n (inc i))))
               v))

(offset ["abcxxxyyy" "defxxxyyy" "ghixxxyyy"] 2)
=> (\c \x \y)
(offset ["abcxxxyyy" "defxxxyyy" "ghixxxyyy"] 4)
=> (\x \y \x)

varun18:09:53

How does clojure turn maps and vectors and such into functions, such that you can "invoke" them on things such as the following?

((vector 1 2 3 4) 2)

didibus18:09:10

Are you asking about the implementation details of it? Or are you asking if it's possible for you to do the same for some of your own types?

varun18:09:44

i suppose both.

Alex Miller (Clojure team)18:09:50

Invokeable things implement the IFn interface. The Clojure colls implement that. You can do the same.

varun18:09:24

ah ok i see. thanks

didibus18:09:31

For the first, Clojure uses an Interface (Java interface) to represent callable functions, the interface is IFn. If the first element of an s-expression implements the IFn interface, Clojure will call it's -invoke method, which executes it as a function. Vector is implemented in Java and derives from IFn, thus it implements the IFn interface, meaning when it appears as the first element it can be used as an IFn and called as a function.

❤️ 2
didibus18:09:53

Your own types can also derive from the IFn interface when you implement them, and they would also be callable as functions.

didibus18:09:17

And by your own types, I mean things you create using deftype or defprotocol

didibus21:09:01

In Clojurescript, IFn is a Protocol, instead of an Interface. Protocols and Interfaces are very similar, but the main difference is that an Interface has to be implemented on the type itself where the type is implemented. It means you cannot extend an existing type, only types you control you can implement the interface for. A Protocol allows for the type to be extended after it is defined. That means in Clojurescript you can extend any type to IFn, making anything callable as a function if you want.

Pavel Bazin21:09:24

Hey folks, hope you all going great! I’m attempting to write some simple backend software which communicates to a database. Everything going alright, however, one thing I can’t grasp — how to deal with errors? For example we have users table which has unique SQL constraint in its schema (over email field). Therefore, if we try to add a certain email twice we get AlreadyExists back. How to deal with that error? Namely, how to distinguish one error from another? Something like a type-matching, or some similar mechanism. Unsure how helpful it will be, but I have something Scala-like in mind:

def addUser(user) = {
  res: Either[User, Error] = db.add(user)
  
  res match {
    // Meaning User type is returned
    case Left(u) => // Return user
    case Right(err) => // Process an error
  }
}
Is something like that possible using for example keywords or some sort of types?

didibus21:09:28

What exactly do you mean by you get an "error" back? Do you mean you get an Exception back? Meaning you get an instance of a type that derives from java.lang.Throwable? And do you mean you get that exception back as a value? Like some function returns it? Or do you mean the exception is thrown? Or do you mean that the function returns some value that models some kind of error, but doesn't do so using any of the standard Exception types. Like say it just returns a Boolean flag: {:error true} or something like that?

didibus21:09:43

Generally, I/O functions in the JVM and thus often in Clojure, since most I/O in Clojure is done through Java interop will throw Exceptions if they encounter an error. To handle that, you use try like so:

(defn add-user [user]
  (try
    (add db user)
    (catch Exception e
      ;; Process error e)))
And if your "process error e" actually does nothing and just returns the error itself, it's common then to just not handle the exception at all, thrown exceptions automatically short-circuit and bubble up the stack until a try/catch is encountered, so you'd only surround a try/catch where you actually do something with the error.

1
Pavel Bazin22:09:42

Thank you so much for your answer @U0K064KQV! What exactly do you mean by you get an “error” back? I think I’m looking for a mental model of how to think about errors in Clojure and how to handle them. Perhaps I’m having hard times of not thinking about errors as values, but as exceptions. Looks like the last option you’ve mentioned is something I’m looking to. Like {:status :success :result [something]}. What is a conventional and idiomatic way of Clojure to deal with errors overall? Or I better say exceptions. Since my message I come up with something looking like:

(defn add-user
    [name email]
    (try 
      (add-user-helper name email)
      (catch Exception e
        (case (.getErrorCode e)
          ; 1062 is MySQL error for duplicate entry
          1062 :already-exists-error
          :db-error))))

Pavel Bazin22:09:49

Thank you for the idea of bubbling up exception until try/catch. I believe that could actually do what I intend to.

seancorfield22:09:43

@U01EMQNKBNF The exception classes form a hierarchy so, when working with JDBC, it is common catch specific types in the hierarchy. For example, for a duplicate entry error, you'd get https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/SQLIntegrityConstraintViolationException.html

🙌 1
Pavel Bazin23:09:10

Thank you @U04V70XH6! Figured so, however one thing got me puzzled:

(try 
  ; Helper simply calls to `jdbc/db-do-prepared`
  (add-user-helper 
    "name surname" 
    "") 
    (catch Exception e e))
REPL prints back #error{} witch lots of details including two types of exceptions somehow. A vector of:
[{:type java.sql.BatchUpdateException
  :message ...
  :at ...
 {:type java.sql.SQLIntegrityConstraintViolationException
  :message ...
  :at ...}]
If I try to get catch of SQLIntegrityConstraintViolationException :
(try 
  (add-user-helper 
    "name surname" 
    "") 
    (catch SQLIntegrityConstraintViolationException e  "caught it!"))
I’m getting back unhandled thrown exception. However, if I catch BatchUpdateException it works as expected:
(try 
  (add-user-helper 
    "name surname" 
    "") 
    (catch BatchUpdateException e  "caught it!"))
Returns caught it. How to handle those grouped (unsure how to name two exceptions together) exceptions if I want a particular one of them?

seancorfield23:09:54

The #error{} stuff is the printable representation of an Exception object and I suspect the vector you are seeing is the :via portion: exceptions can have a chain of causes as they are caught and rethrown up the tree. I'm a bit surprised the integrity violation is getting wrapped in a batch update exception here, so I'll need more information about your code: what Clojure JDBC library are you using? What database are you using? What JDBC driver are you using? and What exactly does your add-user-helper function do?

1
didibus00:09:19

Only the top exception wrapping the others can be caught with catch. So like, it seems maybe you call some batch update method, and that throws a BatchUpdateException which contains inside it an exception that was thrown from lower down the stack. Think of it like, you're getting an exception from the BatchUpdate, it tells you BatchUpdate failed. But you can inspect the Exception, and it might contain "causes", which is basically the BatchUpdate has captured what exceptions resulted in itself failing, and that's the other exception you see inside it.

1
didibus00:09:46

I think a BatchUpdateException is more complicated to handler, because prior to the update failing, a bunch of other update could have succeeded. So when you retry, you have to make sure you don't retry the whole batch, but just what failed or hasn't succeeded yet.

didibus00:09:40

I believe you're supposed to check this: https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/BatchUpdateException.html#getUpdateCounts() and that will tell you for each update in order that you made, which one (based on array index) have succeeded/failed.

didibus00:09:48

Basically, it's non-trivial exception handling you're dealing with here

seancorfield00:09:26

@U0K064KQV At work we catch the SQLIntegrityConstraintViolationException but we use next.jdbc and it uses the raw .execute() JDBC method. I suspect the .executeBatch() method is catching and wrapping that with the BatchUpdateException which is unfortunate -- and I'm wondering if @U01EMQNKBNF is using clojure.java.jdbc instead? (this is one of many reasons that I changed things in next.jdbc)

Pavel Bazin22:09:57

Gentlemen, first of all my kind thanks for your responses. It now makes a perfect sense to me. @U04V70XH6 was using clojure.java.jdbc and was calling db-do-prepared function to insert entities into MySQL database (8.0.23). I was not happy much with API of clojure.java.jdbc so I found your library next.jdbc . After migration I was able to get SQLIntegrityConstraintViolationException exception as I was expecting. Domain logic code got no change, all what was done is a switch from db-do-prepared of clojure.java.jdbc to execute! of next.jdbc . P.S. Thank you for the libraries, I’m trying to build understanding of Clojure and reading your code was helping me to develop some “sense” of the way of thinking (namely honeysql).

Pavel Bazin22:09:50

@U0K064KQV you explanation made perfect sense to me, my thanks! > Only the top exception wrapping the others can be caught with catch. I had a suspicion of that. Is there any way to unwrap stacked exceptions? Despite that the issue above solved, I’m still curios about navigating over wrapped exceptions. Is that behaviour is something specific about Clojure or it scales up to Java itself? I dealt with JVM in Scala, but there people don’t use exceptions normally.

seancorfield22:09:06

Ah, cool. I figured it might be the difference between c.j.j and next.jdbc. Although I maintained c.j.j for years, I didn't write the core of it and so there were some quirks in the code that I didn't understand at the time so I left them in there. By the time I wrote next.jdbc, I'd learned a lot more about JDBC (it's amazing for what it tries to do but it's still pretty awful in places 🙂 ) so I tried to iron out those c.j.j quirks!

seancorfield22:09:43

You can call ex-cause on an exception (or .getCause which is the underlying Java method). As long as you're on Clojure 1.10 or later.

1
seancorfield22:09:09

ex-cause will return the "wrapped" exception up the chain until it returns nil.

seancorfield22:09:27

As for Scala, there are two schools of Scala developers: the Haskell-style folks (no exceptions, generally) and the "better Java" folks (lots of exceptions 🙂 ). Or at least those were the two sides of the Scala community back when I used it in production (the 2.7/2.8 era -- we shifted to Clojure when Scala 2.9 appeared).

Pavel Bazin23:09:32

Ah great, understood the ex-cause and getCause , good knowledge! Seems that JDBC is dreading on all JVM-as-a-host ecosystems. You put it very politely in words 🙂. I believe Scala community got more diverse and it extended to everything in between, some sort of FOOP (made up Functional-Object Oriented Programming). I believe two factors helped i) big profile libraries such as Spark, ii) boom of “big data”. Right now I’m having hard times of working with exceptions. They are feeling like they are breaking the flow of the program compared to monads / types as errors. I mean, in programming errors are not so exceptional, they are kind of common, especially with I/O 😅.

didibus01:09:55

What I like about exceptions is in the default case, it does the right thing, short-circuit everything and bubble up. That's what I want 90% of the time, and I hate having to deal with errors only to bubble them up. I think error Monad can mimic similar behavior, but it also forces the use of Monads. That said, if you want, some people do use Monads for errors in Clojure using one of: https://github.com/adambard/failjure or http://funcool.github.io/cats/latest/#exception for example, there might be some other libs. They tend to simply wrap Exceptions in a Monad, so the exception is returned instead of thrown

Pavel Bazin22:09:22

Thank you @U0K064KQV ! I think that what I miss the most is compiler help with exceptions. In Scala you would define possible returning types and program will be defined at the compile time. Therefore, if you take your types with care, and if your program will compile, the thing will work without issues (of course there could be issues with logic, no compiler would help with that).

didibus05:09:56

Ya, you won't get typed exceptions here, but I don't know, there's a flip side, maybe it takes a while to realize, but to me the types are often in the way between me and my correct program behavior, especially with exceptions, forcing me to handle things that I don't care about, most errors I want to either retry or log and return an error to the client/user. So forcing me to think about each and every type of error everywhere, or that something could throw an error or not everywhere, it's just cumbersome. Instead, wrap your top-level function inside a (try ... (catch Exception e)) and have that log and return a client/user error. And then at places where you can retry, put another one to retry, and maybe on that one, you can select a few more specific exception types you know are possible to succeed on retry, aka, they indicate transient issues.