Fork me on GitHub
#clojure
<
2023-08-31
>
emccue10:08:22

Last night to deal with sleep deprivation I translated https://github.com/weavejester/progrock into Java https://github.com/bowbahdoe/progrock/ and I have some...lingering thoughts

💡 6
emccue10:08:58

There are a lot of ways that I did things differently that I think give way to probably a larger comparison of "the way clojure works" to "the way java works"

emccue10:08:13

1. "The progress bar is just a map of data:" >

{:progress 0, :total 100, :done? false, :creation-time 1439141590081}
In clojure, the progress bar's data is a data structure. It has a definite shape, but that shape is growable and is directly exposed

emccue10:08:41

I could have done something similar

public record ProgressBar(
   int progress,
   int total,
   boolean done,
   long creationTime
) {}

emccue10:08:44

but I didn't

emccue10:08:09

public final class ProgressBar {
    final int progress;
    final int total;
    final boolean isDone;
    final long creationTime;

emccue10:08:42

because, specifically, I thought I might want to refactor to java.time.Instant in the future

emccue10:08:25

whereas in Clojure you can just expand the contract of the function to include both, the long creationTime in a record is a contract that a long is going to be stored

emccue10:08:04

so the hypothetical cost of that commitment is larger than with clojure.

emccue10:08:02

but also, for reasons I can't articulate too too well, I thought of a "java consumer" in a different way than a "clojure consumer"

emccue10:08:27

like...i'm playing the "minimize commitments" game a lot harder than in Clojure world

emccue11:08:40

maybe in part because by making a nominal type, I think about that differently?

emccue11:08:53

this also came into play with the rendering options

emccue11:08:58

in clojure it looks like this

emccue11:08:16

> "Render a progress bar as a string. Takes an optional map of profiles, which > connects state keywords to maps of display options. The following display > options are allowed: > > :format - a format string for the progress bar > :length - the length of the bar > :complete - the character to use for a completed chunk > :incomplete - the character to use for an incomplete chunk

emccue11:08:31

> (def default-render-options > "A map of default options for the render function." > {:length 50 > :format ":progress/:total :percent% [:bar] ETA: :remaining" > :complete \= > :incomplete \space})

emccue11:08:44

In Java I did not do the equivalent

emccue11:08:07

because in my brain I was thinking "well, complete could be a String if its only one displayable character"

emccue11:08:16

but didn't want to "make that commitment" now

emccue11:08:39

public final class RenderOptions {
    public static final RenderOptions DEFAULT = new RenderOptions(
            50,
            ":progress/:total   :percent% [:bar]  ETA: :remaining",
            '=',
            ' '
    );

    final int length;
    final String format;
    final char complete;
    final char incomplete;

emccue11:08:45

so I make everything nonaccessable

emccue11:08:14

and did the whole dance with a builder...just so I could change char complete to String complete (maybe to put an ascii escape sequence in there...but it should stay a single displayable character - but thats a property of the output format, which is hard to determine)

emccue11:08:27

maybe, later

emccue11:08:05

and all these choices I made the Java version strictly less flexible - I hide the data

emccue11:08:43

and the thing that drove me to do that was the nominal type system

emccue11:08:52

not a lack of immutability

emccue11:08:10

so idk @U05291MNLRH if this is relevant to you at all

emccue11:08:22

but these sorts of subtle differences compound.

emccue11:08:36

its not as simple as "oh dynamic typing good" - progrock can definitely be a core.typed library or even have been a typescript library without changing much at all about its construction

emccue11:08:09

and "what dynamic typing gets us" is a whole other dimension to clojure

emccue11:08:59

but this exact scenario might be indicative of why we tend to end up with one library for a task that's "complete"

Alex Miller (Clojure team)11:08:31

As Rich has said, when you move from Java to Clojure, it’s like someone has been standing on your foot for years and they just stepped off it

clj 4
Alex Miller (Clojure team)11:08:05

I find using Datomic vs sql to be similar in terms of freeing you up in making commitments about the data model

💯 2
lread11:08:15

Cool and thoughtful experiment! Thanks for sharing it! (And hope you finally got some sleep!).

emccue11:08:41

I didn't!

😢 2
jpmonettas11:08:22

I guess in Java instead of hiding you could add interfaces when you don't want to commit to any specific type? Or just Object?

Alex Miller (Clojure team)13:08:23

you could use generic collections (like HashMap) or Object but everything in the lang drives you towards making types, which are closed and fixed at the point of conception (unless you change all your code)

jpmonettas13:08:15

I think using HashMap and generic collections yes, java will drive you off that and into closed types with class and records for entities, but for the problem of comiting to specific types for fields (what I think this was about) interfaces are natural in java

Alex Miller (Clojure team)13:08:26

the problem with interfaces is - they DO add abstraction, but they also increase code size. the abstraction affordances in Clojure instead tend to decrease code size. this makes all the difference over time

jpmonettas13:08:58

interesting, can you expand a little more on that?

Alex Miller (Clojure team)14:08:11

if you make an interface in Java, you have to take the method(s) you have and package them up into a new interface class (100s more lines of code), then create multiple instances which extend the interface (100s more lines of code), etc. Combine that with factories to make the instances ... more code. vs Clojure you first should be working with the data directly, you may need no "interface" at all, it's just keys in a map that return different things. if you're moving up to functions as a point of abstraction, then function invocation (particularly with map opts) doesn't need anything "extra" - you might just take a function with a known shape. If you want open extension, you do have a bit more work with either multimethods or protocols, but (thanks to macros), that can also be made smaller, if it's worth the time to do it.

👍 2
Alex Miller (Clojure team)14:08:53

You can write some standard Clojure code and get things working and then if you really want to work on it, making it more robust, more generic, more reusable - I find that process usually results in code that is smaller. Doing the same process in Java usually tends to make things bigger, often much bigger. It is not uncommon to see 10:1 or even 100:1 Java to Clojure code comparisons. Its astonishing to me how much (by volume) of Java code is just "maps of attributes" once you can see it that way, and it would be better that way.

👍 2
jpmonettas14:08:05

yeah that I agree and I'm all in for open map entities for information systems, I was focusing on this particular example and the problem of having to commit too early to some concrete type for a field. I don't think you should put interfaces everywhere in java either, but when you don't feel commiting to a concrete type for a field, that is pretty straight forward with interfaces and not much verbose if you do small interfaces

jpmonettas14:08:48

in Clojure if you are returning a map with a key pointing to something you are not sure you wan't to change tomorrow you have the option of creating a protocol and a type for it, or a set of functions to make working with that key opaque so you can change it tomorrow without changing all users of that data, which I think is kind of the same as interfaces in Java unless I'm missing something

Noah Bogart15:08:37

thanks for the thoughts, @U3JH98J4R! this is really insightful

emccue15:08:55

@U0739PUFQ interfaces would not allow promotion from char -> String no matter how I did it

emccue15:08:42

interface RenderOptions {
    char complete();
    char incomplete();
}
Committing to the contents of the field isn't really the issue

emccue15:08:47

And I think this is descriptive - there are value judgements to make, but they are distinct from the existence of the tension

jpmonettas16:08:38

I was talking more in general, for this specific example I think for complete and incomplete I would choose something that can be stringified (that is the user interface). Since anything can overwrite .toString(), I would just use Object complete; where you can put Char, String or anything else, and you can change it later to anything that overwrites toString(), which is probably what the Clojure implementation will do with str to support char and String

emccue16:08:58

Choosing Object also presupposes foresight

jpmonettas16:08:32

how is that? I mean, how using Object there is inferior to Clojure maps from keywords to Objects for this case? I think just because Clojure maps gives you Object everywhere by default it doesn't mean you don't need to foresight fields possibly changing. You can't return a user entity like {:name "John" :birthdate (Date. ..)} and then one day expect to change :birthdate to a string, or a long, without it breaking

emccue16:08:54

For return types, yes. This is an "input type" though. Broadening the contract of what's allowed is always okay

emccue16:08:50

The problem is that I need to dictate the vocabulary of allowed interactions for that input type. This means I need to say "you get a char" out or know to resist saying that and instead say "it's an object"

jpmonettas16:08:53

I think what you need to make explicit is again the interface you are going to use on some input field, which is important since you need to use that input somehow. In Clojure if you are doing arithmetic on some input and someone now sends you a string it is going to break

jpmonettas16:08:26

so here by declaring complete as "something stringifiable" you just commit to that, to something that can have a string representation because you need strings in the end

emccue16:08:57

if i was to translate it to java-like syntax, the clojure equiv is like this

emccue16:08:48

interface RenderOptions<Length, Format, Complete, Incomplete> {
    Length length();
    Format format();
    Complete complete();
    Incomplete incomplete();
}

void render(RenderOptions<int, String, char, char> opts) {
    // ...
}

emccue16:08:10

and then the expansion would be

emccue16:08:21

interface RenderOptions<Length, Format, Complete, Incomplete> {
    Length length();
    Format format();
    Complete complete();
    Incomplete incomplete();
}

void render(RenderOptions<int, String, char | String, char | String> opts) {
    // ...
}

emccue16:08:40

(imagine ? extends everywhere)

emccue16:08:17

so there isn't a reason to care if someone makes a RenderOptions<boolean, boolean, List, Void>

jpmonettas16:08:38

but why not just?

public record RenderOptions {
   final int length;
   final String format;
   final Object complete;
   final Object incomplete;
}

emccue16:08:47

if you pretend interfaces are structural, thats ~= to clojure

emccue16:08:59

because that, again, cannot be an evolution of

emccue16:08:08

public record RenderOptions {
   final int length;
   final String format;
   final char complete;
   final char incomplete;
}

emccue16:08:37

whereas with clojure you can start with a narrow input contract and expand it later without breaking consumers

emccue16:08:50

(or making RenderOptions2)

jpmonettas16:08:35

sorry not sure I follow, maybe we are just talking about different things, but is ok, we can disagree :hugging_face:

emccue16:08:09

I think you are talking about what the design could be - yes I could take a record with two Object fields and that fits the requirement of Character | String. Probably I would just take String, but I get the idea.

emccue16:08:37

I am talking about how unless I "hide" things, I cannot evolve the design from an initial design that only wanted char

emccue16:08:56

whereas in clojure you don't need to "hide" things in order to evolve the design

jpmonettas16:08:26

as I see it if you want to move from Object to char on input it is "requiring more" in Rich terms, and you are going to break, no matter if Java or Clojure. In clojure if you have a fn that accepts a map of keys -> objects, but suddenly your code needs to only accept a map of keys -> Longs, then you are going to break some users

emccue16:08:57

its the other way around

emccue16:08:46

we have a proverbial map of keys -> Longs and we want to accept a map of key -> Object

emccue16:08:04

its char -> Object

emccue16:08:09

we are requiring less

emccue16:08:37

and that breaks because requiring less for input means we provide less for people who introspect the input object

jpmonettas16:08:38

oh sorry, I thought you said the other way around, but that case you can evolve from char to Object with no problem in Java or Clojure

emccue16:08:52

its fine if its

void f(char c) {}
to
void f(char c) {
  f((Object) c);
}

void f(Object c) {}

emccue16:08:55

but not fine if its

emccue16:08:22

record In(char c) {}

void f(In in) {}
to
record In(Object c) {}

void f(In in) {}

emccue16:08:29

because code that can exist like

emccue16:08:51

var in = new In('a');
String s = String.valueOf(in.c());
will break

jpmonettas16:08:49

and how does Clojure scapes that? if there is that exact same code? I mean, if there is code already doing (String/valueOf((:c m)))

emccue16:08:19

(let [in {:c \c}]
  (String/valueOf (:c in))

emccue16:08:11

won't break if you make

emccue16:08:24

(let [in {:c "ABC"}]
  (f in))

jpmonettas16:08:58

sorry not sure I follow, kind of confused now tbh XD and have to go back to work. Always fun to explore this kind of things!

jpmonettas16:08:28

oh I think now I understand what you meant by String s = String.valueOf(in.c()); breaking, you will need to add a cast there, I see, interesting

Nikolas Pafitis14:08:39

Is it correct to assume that future is better suited for short lived tasks, and core.async/go for longer running tasks? (Assuming no other CSP/core.async features are used)

dpsutton14:08:44

future will use a dedicated thread in a caching thread pool. It doesn’t really have constraints on short vs long running. go is for coordination. “long running” can be a bit ambiguous. an infinite loop pulling from channels, a bit of data manip, and then sending to other channels is long running but a great use case for go. A single execution that selects from a database, does data manip and then puts back into a db is a terrible scenario for a go block

Jan K14:08:27

In the core.async world you'd normally use core.async/thread instead of a future. Using go vs thread isn't about long running or not, but more about whether the task is CPU or IO bound.

quoll17:08:12

Is there some way to use case with constants that are Java enums?

quoll17:08:35

I’ve never tried to do this before, and I just assumed it would work. I can always dispatch on the names of the enums, but I thought I might be able to do so on the objects themselves

hiredman17:08:27

enums are not handled any differently by clojure than other classes and methods and fields, so same evaluation behavior, etc, and the same caveats with literals apply to case

quoll17:08:28

Hmmm. OK. That’s what I expected. It didn’t work though 😕

hiredman17:08:32

there are weird hacks and stuff you can do to make either the reader eval the read of the enum object from the static field on the enum class

hiredman17:08:58

right because what you have is a symbol that evals to an enum instance

hiredman17:08:10

and case needs literal contants

quoll17:08:21

Ah, thank you!

quoll17:08:24

That makes sense!

hiredman17:08:15

you can also do things like write you own macro that embeds the enum objects in the expansion to case, but that depends on the compilers ability to "decompile" arbitrary objects into bytecode and then reconstruct them when running the bytecode, which can be spotty

quoll18:08:20

My brain was elsewhere, and I was thinking that something like jakarta.json.JsonValue$ValueType/STRING was a constant. But of course it’s a symbol. I’m switching languages too much and my brain isn’t keeping up.

quoll18:08:32

Thanks for the course correction

Alex Miller (Clojure team)18:08:15

this is something I feel bad doesn't work :( I do think we have a ticket about it somewhere

hiredman18:08:39

java's switch does let you use enums, but I think it is a little tricky then you might expect, because there is nothing stopping you from compiling against one version of enum E and then having a different version of enum E at runtime

quoll18:08:28

In other contexts that can cause runtime errors. I don’t know what it would do for a different version of an Enum in a comparison like that :face_with_spiral_eyes:

quoll18:08:55

But, to be honest, if you’re compiling with one class, and then replace it with another class at runtime, you’re asking for whatever happens to you. (and yes, I’ve been the bunny who has tried to do this in a Tomcat service in the past. Classpaths can be painful. But it was my own fault)

hiredman18:08:06

I think it is more an area of concern with static languages where you want some completeness guarantees

Alex Miller (Clojure team)18:08:12

case won't check completeness like switch so wouldn't matter from clojure perspective

hiredman18:08:47

https://clojure.atlassian.net/browse/CLJ-1368 has a link to a google groups discussion as well that mostly has a survey of some different hacks to make enums work in case

hiredman18:08:33

oh just the macro version, not the other one

Noah Bogart18:08:59

is it consistency and limiting complexity that case doesn't work with enums like this? or are there technical limitations too?

Ben Sless18:08:00

I wrote a ecase macro once for this use case that dispatches on the enum value, you could probably extend it to check for completeness at compile time

emccue18:08:30

(condp identical? might be my choice

emccue18:08:24

also, curious what you are doing with jakarta json?

Alex Miller (Clojure team)18:08:50

pretty sure enums didn't exist yet when case was created, so that's why they were not considered, but don't know

👍 2
Noah Bogart18:08:42

of course, completely forgot that enums were a "recent" addition to java

Alex Miller (Clojure team)18:08:47

enums were Java 5, case was Clojure 1.2

hiredman18:08:10

I imagine it was the completeness thing

hiredman18:08:54

java's switch I believe requires you to add a default case with enums, incase the code is used at runtime with a version of the enum with more values

quoll18:08:28

@U3JH98J4R oh, just a small utility in some code that needs to parse JSON-LD. I haven’t had time to write a JSON-LD parser for Clojure, so I’m using a Java one called Titanium. (I’m not a fan) Then I discovered that I need to parse some JSON, and rather than bring in yet another external lib, I wondered if I could just use what had already been loaded for the JSON-LD parser. Again, I’m not a fan. 🙂 But it was very little code to convert it to a Clojure object, so it didn’t bother me. I just found myself frustrated that I couldn’t use case on the enum for the JsonValue type

hiredman18:08:37

with clojure's case as is, you would get an exception at runtime without a default case if an enum was added to

Ben Sless18:08:56

(defmacro ecase
  {:style/indent 1}
  [e & clauses]
  (let [t (:tag (meta e))
        clazz (resolve t)
        values (Reflector/invokeStaticMethod ^Class clazz "values" (object-array []))
        syms (map (comp symbol str) values)
        ords (map #(.ordinal ^Enum %) values)
        mapping (zipmap syms ords)
        lookup (fn [x] (or (mapping x)
                          (throw (ex-info "Value not in enumeration" {:value x
                                                                      :syms syms}))))
        [clauses else] (if (odd? (count clauses))
                         [(butlast clauses) [(last clauses)]]
                         [clauses []])
        clauses (->> clauses
                     (partition 2)
                     (map (fn [[clause expr]]
                            [(if (sequential? clause)
                               (map lookup clause)
                               (lookup clause))
                             expr]))
                     (apply concat))]
    `(case (.ordinal ~e)
       ~@clauses
       ~@else)))
you can add a check that all enum cases are covered, too

hiredman18:08:33

you can check that that all the values of the enum you are linked against at compile time are handled

hiredman18:08:42

but the jvm is linked by name at runtime

😅 2
emccue18:08:39

I would be a bit hesitant to use ordinal

emccue18:08:46

if we have this java code

emccue18:08:57

enum Thing {
    A,
    B
}

....

void f(Thing thing) {
   switch (thing) {
      case A -> System.out.println("A");
      case B -> System.out.println("B");
   }
}

Ben Sless18:08:39

They'll have the same ordinal value?

emccue18:08:03

and we compiled the two things seperately

emccue18:08:15

and changed Thing's definition to

emccue18:08:22

enum Thing {
    B,
    A
}

emccue18:08:45

the ordinal values will be swapped, but I would be surprised if the hypothetical f method would change its behavior

emccue18:08:27

googling how javac actually does it now

Ben Sless18:08:06

At least in the code I wrote, the programmer specifies the cases as enum symbols, so if you link against different orders it doesn't really matter as long as you compile your ns after the java enum was compiled, no? The ordinals will be resolved at macro expansion time

hiredman18:08:52

it is the same thing, the version you inspect at macro expansion time could end up different from the version at runtime

emccue18:08:14

well if there is a new version of the java enum that links against AOT compiled clojure code there could be an issue, right?

hiredman19:08:06

that kind of thing is somewhat less of an issue with clojure because we tend to always build from source and mostly don't distribute aot'ed code, but some people do that

emccue19:08:37

im super curious now but working with javac is a bit painful - i'll try the hypothetical and report back later

emccue19:08:10

okay fascinating

emccue19:08:15

class Main {
  Main();
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return


  static void f(Thing);
       0: getstatic     #7                  // Field Main$1.$SwitchMap$Thing:[I
       3: aload_0
       4: invokevirtual #13                 // Method Thing.ordinal:()I
       7: iaload
       8: lookupswitch  { // 2
                     1: 36
                     2: 47
               default: 55
          }
      36: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      39: ldc           #25                 // String Hello
      41: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      44: goto          55
      47: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      50: ldc           #33                 // String World
      52: invokevirtual #27                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      55: return

}


class Main$1 {
  static final int[] $SwitchMap$Thing;

  static {};
       0: invokestatic  #1                  // Method Thing.values:()[LThing;
       3: arraylength
       4: newarray       int
       6: putstatic     #7                  // Field $SwitchMap$Thing:[I
       9: getstatic     #7                  // Field $SwitchMap$Thing:[I
      12: getstatic     #13                 // Field Thing.A:LThing;
      15: invokevirtual #17                 // Method Thing.ordinal:()I
      18: iconst_1
      19: iastore
      20: goto          24
      23: astore_0
      24: getstatic     #7                  // Field $SwitchMap$Thing:[I
      27: getstatic     #23                 // Field Thing.B:LThing;
      30: invokevirtual #17                 // Method Thing.ordinal:()I
      33: iconst_2
      34: iastore
      35: goto          39
      38: astore_0
      39: return

}

emccue19:08:22

so it does dispatch on ordinal

emccue19:08:42

but it loads the ordinals at runtime into an anonymous inner class

emccue19:08:13

$SwitchMap$Thing and thats how they avoid the issue I think

emccue19:08:31

or maybe i'm reading it wrong

hiredman19:08:22

Yeah, it really is a map thing, mapping from ordinal to position in array, where position in array is fixed at compile time

hiredman19:08:40

Very interesting, thanks for bothering with javac

hiredman20:08:16

Your can't really do that with a macro on top of case, because you likely cannot get the compiler to hoist the map creation into a static context

hiredman20:08:04

(I think spectre has a similar issue with the inline caches its macros expand to use, and does some shenanigans)

agorgl18:08:36

What do you usually use when trying to transform heterogeneous maps? E.g. given some input data like this:

(def sample-colors
  {:black "#000"
   :white "#fff"
   :lime
   {:50  "#f7fee7"
    :100 "#ecfccb"
    :200 "#d9f99d"
    :300 "#bef264"
    :400 "#a3e635"
    :500 "#84cc16"
    :600 "#65a30d"
    :700 "#4d7c0f"
    :800 "#3f6212"
    :900 "#365314"
    :950 "#1a2e05"}
   :orange
   {:50  "#fff7ed"
    :100 "#ffedd5"
    :200 "#fed7aa"
    :300 "#fdba74"
    :400 "#fb923c"
    :500 "#f97316"
    :600 "#ea580c"
    :700 "#c2410c"
    :800 "#9a3412"
    :900 "#7c2d12"
    :950 "#431407"}
   ...})
that you want to transform it to something like:
{:black "#000"
 :white "#FFF"
 :lime-50 "#f7fee7"
 :lime-100 "#ecfccb"
 :lime-200 "#d9f99d"
 ...
 :orange-50 "#fff7ed"
 :orange-100 "#ffedd5"
 ...
 :smth-else-50 "fefefe"
 ...}

markaddleman18:08:01

My “go to” library for transformations is #CFFTD7R6Z but the transformation that you are talking about probably doesn’t warrant meander’s power. Instead, I think this could be handled in clojure straighforwardly.

p-himik18:08:09

I'd probably use into with mapcat.

hiredman18:08:37

(fn f [p m]
  (if (string? m)
    [[(keyword (apply str (interpose \-) p)) m]]
    (for [[k v] m
          i (f (conj p (name k)) v)]
      i)))

hiredman18:08:19

that will result in a seq of pairs, you'll need to pour that into a map at the end

agorgl18:08:20

ah so the trick is to make a list with a single element in case of string values in order to make them kinda homogeneous with the others

hiredman18:08:47

the for does a concat

hiredman18:08:07

because that code works for arbitrary nesting levels

agorgl18:08:55

nice one, thank you!

telekid19:08:25

I'm building an app for which I'd like to delegate authz/n to an OIDC identity provider. That is, it is my goal that my users can use whatever OIDC provider that they want (AWS IAM, Keycloak, Okta, etc.), provided they launch my app with a config file pointing to their identity provider. I've spent some time looking for a nice little clj/s library that bundles up the various steps of the standard OIDC connection flow into a few functions that I can wire up to my frontend / ring / my database, but haven't found anything to that effect. I built an OIDC integration at my last company so I know the steps involved (it isn't tooooo bad,) but I'd like to avoid having to get too deep into it if I can. Any thoughts? I can't be the only one who's run into this.

telekid19:08:59

(Basically I want my user's experience to be more or less what Tailscale provides.)

p-himik19:08:39

Perhaps something for Java/JS exists, then you could use it via interop.

telekid21:08:12

Yeah that may be where I end up going

lukasz22:08:10

FusionAuth is open source, so with some pain you could potentially build on top of its API https://github.com/fusionauth Metabase's SSO stuff also might be open sourced (I think)

Steven Lombardi23:08:10

I'm looking for a way to get a consistent reload experience regardless of IDE. Reloading with tools.namespace was okay but if I goofed or made an error, the reload would blow up, forcing me to lose all of my context/vars and require a REPL restart. Previously, using Cursive, if I made an error and reloaded my old state was preserved and I could fix the error and continue on. Is there a way to mimic my Cursive reload experience with tools.namespace?

hiredman23:08:13

cursive is mostly likely not actually reloading, just re-evaluating the contents of the current file

hiredman23:08:20

most editor integrations have something like that, alternatively you can call load-file on the file, or just copy and paste the file contents into the repl

hiredman23:08:03

actually cursive may be doing something between what tools.namespace calls reloading, and just loading the file contents

hiredman23:08:48

cursive is maybe doing something more akin to (require 'the-namespace :reload-all)

👍 2
jpmonettas01:09:27

when reloading with tools.namespace, if you hit a compilation error, you will get only half your namespaces (or less) reloaded and you would feel like you lost everything and tend to restart the repl. When this happens, instead reloading the "main" namespace (the one that pulls everything else) with whatever editor is enough to bring everything back and start fixing the compilation errors

jpmonettas01:09:57

you still need a way of stopping and restarting resources like threads, db connections etc, which you can accomplish with any of the "reloaded workflows"

thumbnail05:09:08

I usually have a snippet in my ide which stops the system if tools.namespace reloading fails. Making sure to grab the reference beforehand

vemv06:09:16

> Reloading with tools.namespace was okay but if I goofed or made an error, the reload would blow up, forcing me to lose all of my context/vars and require a REPL restart. An intentful setup can avoid this. For instance, require and refer key tools.namespace stuff in user.clj, and disable reloading for user.clj https://github.com/clojure/tools.namespace#disabling-refresh-in-a-namespace Leave user.clj thin, just for that and a few other things that should be able to salvage a failed refresh. And use dev.clj as your 'real' user.clj with app-specific helpers, etc. This way, if a (refresh) fails, user.clj is still defined as-is, so you can repair your code, hit (in-ns 'user) and refresh again from there

Steven Lombardi05:09:34

Thanks for the feedback everyone. A few things I'm going to try. • Re: hiredman - Been having some luck with (require 'the-namespace :reload-all) so I'll keep playing around with that. And possibly make my own helper func to do it since I'm working in a Polylith context. • Re: jpmonettas - I'll have to double check but I'm fairly certain Calva's reload capabilities cannot recover your repl when tools.namespace borks it. But I'll double check that and try some of your suggestions. I'm using Stuart Sierra's component library so I've got myself a nice reloaded workflow.

Steven Lombardi05:09:02

Re: vemv : > For instance, require and refer key tools.namespace stuff in user.clj, and disable reloading for user.clj That's a neat trick. I'll give that a shot.

hiredman05:09:25

Having mentioned it, reload-all is kind of terrible, reloading code without tracking transitive dependencies can really break things in confusing ways, which is why the tools.namespace is the way it is, and why it is so scorched earth about it

hiredman05:09:03

Some people completely eschew that kind of reloading tooling in favor of being disciplined about repl usage because of the trade offs involved https://youtu.be/gIoadGfm5T8 (you can find other videos of Sean talking about repl usage searching on yt as well)

mauricio.szabo14:09:09

I also have a custom snippet in user.clj that does stop the system before the reload.

mauricio.szabo14:09:41

There's also a trick - sometimes, when something breaks the reload, it can leave things un-reloadable. tools.namespace have a way to "clear" the incremental reload state and reload everything

mauricio.szabo14:09:02

My snippet is basically keeping a state if the last reload was an error, and if it was, clear the whole thing before trying to reload again