Fork me on GitHub
#beginners
<
2023-03-15
>
Zach λ12:03:03

Hey, I hate to ask this, but I'm trying to read https://mitpress.mit.edu/9780262045490/software-design-for-flexibility/ which is written in Scheme, so I need to learn a lisp pretty deeply (I know basic lisp, and am experienced in FP), and decided on clojure. I'm an experienced programmer, but am feeling overwhelmed at the options, and reading dry docs and memorizing new syntax is more mentally draining for me than I can explain. Can anyone recommend any good "fundamentally understand clojure" resources that neither assume you're a new functional programmer or have a PHD? I would like to get into the internals, since I'll need to be writing macros, but I can't seem to find the right "resource", I guess

👀 2
alpox12:03:04

I found https://www.braveclojure.com/ to be a pretty good introduction to clojure

6
🙏 4
2
José Javier Blanco Rivero13:03:16

Hello Zachary! I am not an experienced programmer but I would recommend you to first try to understand the concept of symbolic expression (s-exp). Beyond basic data types (booleans, numbers, strings, etc.) everything is built upon s-exp. This is basically the syntax to learn in the LISP family, so there is not pretty much to memorize. An s-exp is nothing but a list where the first position is a function and everything else is an argument or parameter. A set of nested s-exp is called a form. What macros do is to manipulate forms, so that some expressions are evaluated and others are substituted with s-exp passed as parameters. As for books for learning Clojure that are neither so basic nor so difficult, I like Chas Emerick "Clojure Programming" and Alex Miller et alia "Programming Clojure". Both have really good chapters about Macros. I hope this helps!

❤️ 2
kennytilton13:03:28

Macros? Did someone say macros? Let's go! :man_climbing: Two things. First: "I would like to get into the internals, since I'll need to be writing macro..." Hmm. The beauty of Lispy macros, Lisp or Clojure, is that we are still writing conventional Lisp/Clojure in our macroexpansion implementations. I think the smart kids call them "homoiconic". Anyway, I am not sure "internals" works, depending on how you meant it. Aside: There is quite a trick to writing macros, because while writing them we have to juggle the two contexts "now I am transforming the code at compile time" to "this code I am producing will produce this runtime behavior". That context-switching while typing took me a while to get past. Second, Paul Graham goes deep on macros in his book http://www.paulgraham.com/onlisp.html. Follow that link to a free PDF. Not a beginner book. And Siebel's https://gigamonkeys.com/book/ has a solid section on macros. The terrible news is that Scheme macros are so obsessed with hygiene that writing those is indeed much harder than the Common Lisp or Clojure approach. But I still would not consider that internals, just tortured. But you are asking for Clojure resources, so maybe you plan to write Clojure macros while reading a Scheme book. That's good news, except you will I guess have to understand the Scheme macros. I took one look at Scheme macros and ran screaming. hth

❤️ 4
Zach λ13:03:39

Thank you all for the suggestions. The author is one of the people I respect most in the CS world, so I'm committed to it, but it uses fairly advanced scheme which doesn't even run on my mac (by advanced, I mean specific apis/libraries, generics, and of course, macros), so I just need to understand how the compiler works well enough to be able to draw comparisons and understand what isn't really relevant between, say, Clojure (since it's underlying core seems to be the JVM, and seems to play a pretty significant role) vs Scheme which is more "bootstrapped," I guess. That's what I meant by internals, anyway, since I plan to complete the exercises in Clojure(script? I'm a web guy....). I'm struggling with the concepts in the book, it's fairly dense. So I need to make sure the language isn't a hindrance on top of it and I have some fluency - e.g. dynamic dispatch, generic operators, implementing algebras and being able to reduce them using pattern-matching on dynamic/unknown-at-compile-time s-expressions which I don't have a lot of experience with. (My background is in Elixir and TypeScript, which have similarities but less expressive power). I also don't fully understand the idea of code as data, although I'm sure it's simple once you "get" it, so it probably does make sense to read Graham's book. Unfortunately, it adds another few months to what already feels like a 10 year learning plan 😂 All I care about is grokking the ideas, I dont care much at all about scheme, as long as I can translate it in my head. So clojure seems to make a lot of sense.

Frank Henard13:03:33

> I also don't fully understand the idea of code as data (function-call is-just-a-list) This is what makes macros possible and easy. It's easy to make code write code. This is why lisp is great, it's a programmable programming language! Maybe once you get to the point of learning about macros, you will understand this better. Good luck!

❤️ 2
Zach λ14:03:50

Thank you. Im sure you get what I mean when I say it "makes sense" but I still don't "get it" in the way that I could sit down and use it to translate my thoughts into some kind of model. But this has all been super helpful

👍 2
Frank Henard14:03:17

Warning, Clojure will likely blow your mind, and you'll be ruined for anything other than a lisp 🙂

💯 2
Zach λ15:03:29

I hope so, that's the kind of high I'm looking for!

🚬 2
vncz15:03:37

@U04U2VCGDKN I did not like that book that much, even though I have been writing Clojure for a while now

vncz15:03:47

It's not about the Lisp, I just did not find the content that amazing to be honest

Zach λ15:03:31

which book - software design for flexibility?

vncz15:03:44

That is correct

Zach λ15:03:52

I've definitely read mixed reviews

vncz15:03:12

I was not really a fan of the book - but your mileage might vary

Zach λ15:03:20

but every single talk I've ever seen by him has made me sit in silence and think, so I'm willing to give it a chance

Zach λ15:03:26

I will admit

Zach λ15:03:37

I really struggle seeing the general application of the ideas

Zach λ15:03:44

but I think that's my inexperience

vncz15:03:53

Yes, that was my problem as well

vncz15:03:59

Where can I apply these ideas

Zach λ15:03:25

I started a year ago, and spent several months learning abstract algebra and took a look at some of it again

Zach λ15:03:54

and am slowly realizing, or at least convincing myself, there is a very general point hes making

Zach λ15:03:05

im just so far below his level, I cant critique either way

vncz15:03:49

Sure thing - I am not critiquing either; I was just saying that on a personal level I did not feel any great enrichment from reading the book - and I do not think that it is because I am smarter than the author

vncz15:03:59

I had a similar experience with Functional Domain Driven Design

vncz15:03:25

That book also did not really fly with me

brianwitte16:03:45

I was programming 5 years before I came to Clojure. I had a concrete thing that I wanted to build so I used https://github.com/seancorfield/clj-new to generate project scaffold and then I used https://www.amazon.com/Clojure-Programming-Practical-Lisp-World/dp/1449394701 as a reference (don't let the "4 stars" and age of book deter you, it's great and Clojure is a very stable language thus I don't remember anything not working properly). I think having a concrete problem I was trying to build really helped me because I could focus my effort__ on the new language, tools and not the problem domain as much. I think this "applied learning" approach is best for most of us, but especially folks with more experience. Also, my experience with Clojure books is that many of them are quite good and useful

❤️ 2
Pagoda 5B16:03:47

@U04U2VCGDKN about the “code as data” part I guess you can easily grasp it by comparing with the javascript eval function. You can build a javascript expression as a string dynamically at runtime, and then “evaluate” it to some result. Well, you need to build pretty complex expressions to match the syntax of javascript, you guess. In lisp, the syntax of any program is a list. Function calls are expressed as lists where the “head” is the name of the function (a reference to some lambda) and the “tail” is the arguments. So building a lisp/clojure expression as data is just a matter of building a list with the function name as head and the args as tail.

Pagoda 5B16:03:19

I hope this gives you a glimpse of why lisp macros are such a “breeze” compared to more syntactically complex langs

Pagoda 5B16:03:58

Switching between data and code is very simple. And you have quote/splice to mix and match

Pagoda 5B16:03:17

I’m absolutely a beginner here, but I think that’s the hang of it

phill16:03:44

Skim it first, before stressing too much about a 10-year learning plan! It's a very nice book, but it is also humorous. For example, who would choose Scheme for flexibility?! Anyway, Clojure is excellently suited to web work and to general-purpose programming in general, so you might find it and the book interesting. I sure did. ... PS: The book says you have to turn on "GNU extensions". For me, this was a red flag that took a load off my mind! It signals that purity of Scheme is not one of the authors' preoccupations. The authors focused on a higher plane. When it comes to punching the keys, they're OK with ruthlessness. This book is not at all fundamentalist.

Zach λ16:03:59

@U02JTH42R7A from what I understand it's fairly different. javascript eval is done at runtime, which is what makes it dangerous and slow. macros are done at compile time, which means they can become a core part of the language and take advantage of compiler optimizations, effectively letting you create your own DSLs, and is why they are safe

brianwitte16:03:32

also, fwiw to expand on my earlier post @U04U2VCGDKN, I am also interested in CS education and Clojure as a medium for learning FP/PL stuff. For me, building concrete stuff like CLI tools and web apps before delving into the more abstract stuff was helpful. Dropping down into a repl and making things work really helps build a mental model of how the abstractions work

Zach λ16:03:00

Doing programming problems is definitely an option, I just get nauseous from leetcode grinding PTSD even thinking about it. Building something that actually had some use and was creative somehow would be cool, I just feel like I'm not sure CLI hangman would do the trick, and it's hard to bridge that gap 😂

brianwitte16:03:30

right, LOL. not sure if you are into any sports but many leagues/orgs expose APIs that give stats, standings, schedules for free. You can build a CLI app that can search, sort, and filter that data

Zach λ16:03:41

thats true

Pagoda 5B16:03:05

@U04U2VCGDKN sure, I didn’t meant that they’re working the same, macro are executed during compilation, as you say. I used the “eval” comparison to explain what is meant with “code as data”

brianwitte16:03:47

back to work for me, nice avi btw! haha

😁 2
Sam Ritchie16:03:07

I can help with that book if anyone is interested in doing a study session!

❤️ 6
Zach λ16:03:09

I still think there's a fundamental difference. all code is data, sure, cause its a string that gets parsed. but lisp source code is literally a data structure built into the language.

Zach λ16:03:30

But im not sure how all that affects things

Sam Ritchie16:03:54

I’ve been porting Sussman’s MIT Scheme code for a couple of years now and I’m familiar with a bunch of the code in the book… it would be fun to do a deep dive and maybe we could share the recording?

❤️ 4
Zach λ16:03:11

i would take any help I could get

Sam Ritchie16:03:24

It’s an extreme deep end choice for a first book, not that you’d know from the cover!!

Zach λ16:03:43

Im interested in building the generic algebraic reducer, at least working up to it. some of the stuff at the end is more research-focused, its clear he doesnt know the answer to a lot of it, but i think the algebra and arithmetic solvers are pretty applicable generally - from what I can tell, that effectively just what haskell prides itself on, finding new ways to program with algebra

Sam Ritchie16:03:53

Yup, and we have in Clojure a full port of the most turbo version of the algebraic simplifier that he works on on the book

Zach λ16:03:08

im checking that out then

Sam Ritchie16:03:17

So lots of great reference material to go on

👍 2
seancorfield16:03:36

One thing that no one seems to have mentioned in this thread is that macros are generally fairly rare "in the wild" in Clojure code because the approach most people try to take is functions + data, and they only add macros for syntactic sugar. You can build some fairly sophisticated DSLs in data with pure function transformers/execution models...

❤️ 2
seancorfield16:03:16

(and macros don't "compose" the way functions do -- so working programmatically with macros can be painful)

Sam Ritchie16:03:34

Very important point

Sam Ritchie16:03:48

My port of the pattern matching library from the SDF book is my attempt to stick to the @U04V70XH6 prescription here. The pattern matching system is all functions, with a small macro layer on top for sugar, but with the ability to escape hatch to function land

2
Sam Ritchie16:03:26

https://github.com/mentat-collective/emmy/blob/main/src/emmy/pattern/match.cljc function core for a start to the breadcrumb trail, for anyone interested

Zach λ16:03:16

im reading the source code tonight, really appreciate it. thats weirdly coincidental, love it

❤️ 2
kennytilton16:03:27

Code as data?

(defmacro coad [data]
  (prn :code-sees-data data)
  `(do
     (prn :you-see-code '~data)
     (~(first data) :hello-world)))

(coad (prn :himom))
=> 
:code-sees-data (prn :himom)
:you-see-code (prn :himom)
:hello-world

Kerron18:03:41

@U017QJZ9M7W I'd love to take up that offer on a study session of the book!

slk50021:03:37

I have read most of books about clojure in e.g: "Clojure for the Brave and True, The Joy of Clojure, Programming Clojure". I highly recommend this book "The Clojure Workshop: Use functional programming to build data-centric applications with Clojure and ClojureScript" - love this style of presenting the language as researcher.

seancorfield21:03:04

@UPBB20W20 Is that one of the very, very rare Packt books that is actually good? Most Packt books are poorly written (or at least poorly edited) and full of errors -- I generally recommend folks avoid Packt books altogether.

slk50022:03:19

Yes It's really good book. I haven't found any errors so far. But I'm not an expert. But looks very well organized & solid to me. Take a look & say what you think. few pages on amazon https://www.amazon.com/Clojure-Workshop-Interactive-Approach-Learning/dp/1838825487

2
seancorfield22:03:58

Good to know that sometimes Packt manages to release a good book, despite its awful editorial process and generally terrible track record with technical books. 🙂

Pagoda 5B23:03:43

I had the same direct experience myself, yet every once in a while they manage to get the lucky star out This is one of the rare pearls, nice and detail walkthrough of concurrency on the jvm and in scala: https://www.packtpub.com/product/learning-concurrent-programming-in-scala-second-edition/9781786466891#_ga=2.93099869.195211526.1679096467-216643390.1679096467

Pagoda 5B23:03:16

I don’t think I’ve found any other title there really worthwhile apart from this

Pagoda 5B23:03:51

I own the Clojure Workshop too, I’ll let you know once I’ve read it

Miguel14:03:13

Hi, we need to use some new features of a library, for that we need to update the library, but since this library has breaking changes and is heavily used across a big project, I would like to have both versions as dependencies at the same time, and use in the new namespaces the new version and then slowly migrate the old namespaces to use the new library. What is the best way to achieve this?

Alex Miller (Clojure team)14:03:24

use a new artifact name with new namespace names

Alex Miller (Clojure team)14:03:57

then there is no conflict and you can include both at the same time, migrating as needed

Alex Miller (Clojure team)14:03:54

the key piece of perspective is that if the changes are breaking, they are not "two versions", they should be two different things

Miguel15:03:18

After some reading feels like what I would need is aliases Something like this (from https://clojure.org/guides/deps_and_cli)

{:deps
 {org.clojure/core.async {:mvn/version "0.3.465"}}

 :aliases
 {:old-async {:override-deps {org.clojure/core.async {:mvn/version "0.3.426"}}}} 

Miguel15:03:53

Wouldn’t that be more fitting? For some reason I could not make that work, but I might be doing something wrong.

Alex Miller (Clojure team)15:03:47

you can only have one version in your classpath at the same time

Miguel15:03:35

How can I use new artifact names with new namespace names in a 3rd party lib?

Alex Miller (Clojure team)15:03:58

in that case, you can't, so there isn't really a way to do this in pieces

Alex Miller (Clojure team)15:03:45

unless you republish the lib yourself or something

Alex Miller (Clojure team)15:03:16

this is why breaking changes are bad - it breaks things :)

Miguel15:03:50

Turns out it was a case of RTFM. The lib provider does have different artifact names

woohoo 2
Jakub Šťastný16:03:38

What's the best way to create string like "------------"? I have (str/join "" (map (fn [_] "-") (take 10 (range)))), but that feels too complicated, surely there must be a simpler way?

hiredman16:03:38

repeat

👍 2
elken16:03:22

(apply str (take 10 (repeat "-")))

👍 2
hiredman16:03:56

you don't need take

☝️ 4
seancorfield16:03:33

FYI: with str/join you can omit that first argument because it defaults to "". So I'd do (str/join (repeat 10 "-")) for this.

👍 8
ghadi16:03:09

but as @U0NCTKEV8 mentions, apply+repeat is the clojure way

chucklehead20:03:30

row here is an ArrayList, what am I missing to invoke remove(int index) instead of remove(Object o) method?

(when (> (.size row) 4)
    (.set row 3 (str/join " " (.subList row 3 (.size row))))
    (doseq [i (range 4 (.size row))]
      (.remove row (int i)))
    (.trimToSize row))

dpsutton20:03:46

actions-test=> (do (def index 0)
                   (let [x (doto (java.util.ArrayList.)
                             (.add :a)
                             (.add :b)
                             (.add :c))]
                     (.remove x ^int index)
                     x))

#object[java.util.ArrayList "0x207866db" "[:b, :c]"]

dpsutton20:03:19

if you remove the ^int typehint, it sees it either as an object or an Integer and can hit the remove(Object) arity

ghadi20:03:37

before changing anything, turn on (set! **warn-on-reflection** true) so that you can understand why it's finding remove(Object) and not remove(int)

☝️ 2
ghadi20:03:55

only then should you even read any solutions.

dpsutton20:03:04

although i’m not sure this one triggers that? since one of the overloads is on Object I think it incorrectly assumes it is well specified?

ghadi20:03:48

it is unclear from the quoted context what row is

ghadi20:03:03

@U015879P2F8 (great name) knows it's an ArrayList, but does the compiler?

😆 2
chucklehead20:03:31

thanks, I think I see...the remove call itself actually doesn't trigger any reflection warnings, but the other methods do, and type-hinting the row leads to the right remove being called. So without the row hint, the compiler is assuming it's a Collection? which only has remove(Object o)?

ghadi20:03:04

without the row hint, you should see a warning describing exactly what the compiler thought

Cezary20:03:21

🧵Calling grpc with proto files compiled to POJOs (and pronto) 🪡

Cezary20:03:26

TLDR: How to set content type to proto message while using pronto?

Cezary20:03:12

I try to communicate with google using their native https://github.com/googleapis/googleapis. I have downloaded the proto files and using lein-protodeps generated java files from small subset of their api. With the use of pronto and some extra java classes I managed to reach the point in which the request is actually being sent but with a content type being text/html instead of application/x-protobuf which even https://googleapis.github.io/HowToRPC.html#requests suggest to set. My call flow:

(do 
   (pronto/defmapper selected-messages-mapper 
   ;; base for creating input messages from java classes generated from protobuf files using lein-protodeps 
     [com.google.cloud.resourcemanager.v3.ListProjectsRequest]) 
 
   (def list-projects-proto 
      (pronto/proto-map->proto 
       (pronto/proto-map selected-messages-mapper 
                         com.google.cloud.resourcemanager.v3.ListProjectsRequest 
                         :page_size 100))) 
   (def google-rm-api-address "") 
   (def channel-builder (ManagedChannelBuilder/forTarget google-rm-api-address)) 
   (def gcp-channel (.build (GcpManagedChannelBuilder/forDelegateBuilder channel-builder))) 
 
   (def stub (ProjectsGrpc/newBlockingStub gcp-channel)) 
 
   (def creds (GoogleCredentials/getApplicationDefault)) 
   (def more-creds (io.grpc.auth.MoreCallCredentials/from creds)) 
 
   (def stub-with-creds (.withCallCredentials stub more-creds)) 
 
   (.listProjects stub-with-creds list-projects-proto)) 
Results like so:
"UNIMPLEMENTED: HTTP status code 404\ninvalid content-type: text/html; charset=UTF-8\nheaders: Metadata(:status=404,content-type=text/html; charset=UTF-8,referrer-policy=no-referrer,content-length=1614,date=Wed, 15 Mar 2023 19:22:41 GMT,alt-svc=h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000)\nDATA--------------
---------------\n\n<html lang=en>\n <meta charset=utf-8>\n <meta name=viewport content=\"initial-scale=1, minimum-scale=1, width=device-width\">\n <title>Error 404 (Not Found)!!1</title>\n <style>\n   *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;
padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px)
{body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelo
go_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repe
at;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}\n </style>\n <a href=//www.google.com/><span id=logo aria-label=Google></span></a>\n <p><b>404.</b> <ins>That's an error.</ins>\n <p>The requested URL <code>/google.cloud.resourcemanager.v3.Projects/ListProjects</code> was
 not found on this server. <ins>That's all we know.</ins>\n" 
Naturally, given the first sentance of the error, the question I am bothered with now is how to set the content type correctly. I read that metadata is like proto headers.

Jakub Šťastný21:03:49

So isolating side effects to one main runner that calls onto pure fns is the way to go. How do I deal with exceptions in this context? There are some exceptions that I want to rescue in the main runner: like the user provided a wrong config and I want to tell him off 🙂 Something like this:

; runner.clj
(try (config :custom-user-fn args)
  (catch Exception e (println "Your config fn caused an error: ...)))
However the logic handling running the custom-user-fn has been extracted out of the main file. I would still like to keep the side-effects in runner.clj. In the OOP land you'd do custom exception classes, something like class UserError < Exception; end. Then you'd throw from anywhere and catch those in the runner.clj where you'd handle them best way possible, nice error and (System/exit 1) probably. Now what's the Clojure way to go about this? Exceptions don't actually sound like a particularly functional way to go about this, so I'm not sure I'm approaching it the right way, but...they're definitely handy. Definitely better custom errors than having print statements and System/exit all over the code base. Thank you 🙏:skin-tone-3:

seancorfield21:03:13

ex-info and ex-data may be helpful here.

seancorfield21:03:26

These let you throw an exception that includes Clojure data, and extract that data in the catch expression. So you can have the throwing context provide as much information as a caller might need in order to decide what to do / how to handle it.

Jakub Šťastný21:03:26

Right, that makes sense. I know what these are, just haven't used them in this context yet.

seancorfield21:03:17

Including something like https://github.com/cognitect-labs/anomalies in that data (hash map) can provide a simple, consistent, high-level way to indicate whether the operation is retryable or not and whether the failure is input-related or internal.

👍 4
phronmophobic21:03:25

> So isolating side effects to one main runner that calls onto pure fns is the way to go. That's one way to do it, but it's definitely not the only way to do it.

Jakub Šťastný21:03:39

Is there some good practice? I imagine a lot of it will simply be ex-data with type :custom-label and matching based on these.

Jakub Šťastný21:03:14

@U7RJTCH6J if you want to elaborate, I'll gladly learn 🙂 Although I suspect most of other approaches will be variations of this, you simply have to isolate side-effects somewhere.

seancorfield21:03:58

My experience has been that pushing all side-effects to the edge -- database reads and writes, sending email, logging things (to console, a file, or out to some third-party service), read/write operations over HTTP etc to other services -- is almost impossible if you want to maintain readable code. So a degree of pragmatism is useful here 🙂

clojure-spin 2
Jakub Šťastný21:03:16

Pretty cool actually, just data. (Why did I expect anything else I wonder 🙈😂). No need for empty bodies of custom classes that really serve only as a label.

Jakub Šťastný21:03:22

Definitely if you have a web app with DB, it's unlikely, I get it.

seancorfield21:03:30

Right, and all my experience is with relational database-backed services and apps, so I am possibly more cynical about such potential separation than others 🙂

phronmophobic21:03:43

Generally, I like to start with the problem and work towards the solution rather than the other way around. It also kind of depends on what you mean by "one main runner" and "isolate".

seancorfield21:03:12

You can pull it all apart by using, e.g., core.async and channels so all "effects" happen elsewhere (in go-loop's attached to output channels) but that pushes the inherent complexity elsewhere -- and now you've traded off impurity in the core for coupling and coordination, and also a somewhat artificial separation of "logic" which can potential make code harder to comprehend and maintain later on.

2
seancorfield21:03:07

You have to start "describing effects to be applied" and then have code that interprets those descriptions -- instead of just having code that applies those effects as needed, for example.

Jakub Šťastný22:03:47

Interesting use of core.async. Wouldn't go down that road, but an interesting idea.

skylize22:03:38

> Exceptions don't actually sound like a particularly functional way to go about this. > In case it wasn't clear already, I think it's worth drawing attention to the fact that creating Exceptions and throwing them does not need to be coupled. If your control flow is amenable to passing errors around by value, you can use ex-info to join a stack trace to an error message and just return that as a value. There is no requirement that you must also use throw to short-circuit with that Exception.

Jakub Šťastný22:03:24

@U90R0EPHA that's a very interesting concept. I like it a lot (on first thought), but the obvious thing is how do you do pattern matching? Because you'll want to end up with different behaviours based on if an expected value was returned vs. when not. Like you don't want to end up with code full of ifs, right? How would you handle that without introducing a lot of boilerplate code?

phronmophobic22:03:19

At the edges of your program where the side effects run, I think it's ok if your code is less functional. I think the functional version of that is usually some sort of "flow".

Jakub Šťastný22:03:49

I ended up with this (in case it's useful to anyone):

(defn handle-user-error
    "Abort on user error, re-throw otherwise."
    [error]
    (let [info (ex-data error)]
      (if (= (info :type) :user-error)
        (abort (str (ex-message error) "."))
        (throw error))))

  (defmacro abort-on-error
    [handler body]
    `(try ~body (catch Exception e# (~handler e#))))

  (defn run
    "Main entry fn for a custom runner.
     Takes command-line `custom-config` that it merges
     into the default one and command-line `args`."
    ([args] (run {} args))

    ([custom-config args]
     (abort-on-error
      handle-user-error
      (let [config (conj config/default-config custom-config)]
        (let [defs (abort-on-error handle-att-error ((config :args-to-tasks) args))]
          (doseq [def defs] (run-task def config)))))))
And:
(defn throw-user-error
    "..."
    [message]
    (throw (ex-info message {:type :user-error})))
So this way "expected" errors behave like :babashka/exit 1, print nice message and f-off. Errors that are because of my shitty code are rethrown so I can deal with them.

skylize02:03:26

> Like you don't want to end up with code full of ifs, right? > Are conditionals so terrible? You could use cond instead of if to also dispatch on different types of errors. Without careful design, you could obviously get an explosion of branching. But you get that same explosion with throwing, only hidden behind action at a distance. You end up with functions that might look pure and branchless, but have hidden dependencies that blow up without warning, forking off to a catch somewhere. It's not clear when you throw who is supposed to catch, nor clear when you catch who you expect to be the source of a throw. If you branch explicitly, then any resulting explosion of complexity is also explicit. When multiple levels of conditional starts to feel like a lot of boilerplate, you can likely use that as a beacon to follow (and as motivation) to help you reduce coupling and incidental complexity.

💯 2
Jakub Šťastný22:03:43

How can I work with re-thrown exceptions? I want to get to the original error, where it was re-thrown is relatively non-important. Something like error.previous-error kind of thing. Really have no idea how it works in Clojure, (prn error) is not showing anything I could use, as far as I can tell, it's really hard to read.

R.A. Porter22:03:37

ex-cause should work.

Jakub Šťastný22:03:38

That looks like it should, except it doesn't:

(try
  (try (+ 1 nil) (catch Exception e (throw e)))
  (catch Exception e (prn (ex-cause e))))
; nil

phill22:03:51

This path tends to lead logic into rough terrain. You know - how many levels do you want to look at?, what if they wrapped something in another exception for a reason, etc. Pursuing your other thread, it's usually better to recognize the kinds of faults that you'll detect on the back-end but solve on the front-end, and accommodate them in the API, I mean the interface. For example if back-end function A can fail halfway through due to a typical client error, then you could make it two steps, A1 and A2, or let its formal return value indicate that it got only halfway.... Exceptions "remote" poorly and seem to me to be useful only within a single thread of logic.

☝️ 2
Jakub Šťastný22:03:49

I'm only unwrapping my exceptions, it's 1 level.

R.A. Porter22:03:16

Also, your sample doesn't wrap the exception. It just re-throws it. It has no prior cause.

Jakub Šťastný22:03:49

@U0HG4EHMH OK so you're essentially advocating for another way of "make exception (or custom errors in your case), but don't throw them". Right?

Jakub Šťastný23:03:39

@U01GXCWSRMW Oh I thought rethrow would wrap it, since when I rethrow it shows the "new" location of where it happens. How do I wrap it then?

Jakub Šťastný23:03:54

Ah OK I see now: (ex-info msg map cause)