Fork me on GitHub
#clojure
<
2023-01-18
>
Dave Russell01:01:35

So an interesting thing that's popped up, anyone able to give some more details? 🙂 We have a set of stubs like this:

(def ^:macro foo #'other-ns/foo)
The idea being that it let us replace the implementation with another lib's macro definitions without having to swap all call-sites. This works fine. However, the during AOT compilation when the uberjar is built, these vars seem to lose their ^:macro metadata. The impact being that when running the uberjar and connecting via REPL, executing foo fails -- the var lacks the :macro metadata and tries to invoke the #'other-ns/foo macro as a function. I'm hoping someone can explain exactly why/how the AOT is causing this behaviour 🙂

hiredman01:01:09

If you look at how the defmacro macro works, it expands into code that creates the def and then as a side effect a call to setMacro (which alters the vars metadata)

hiredman01:01:28

What you have attaches {:macro true} to the symbol foo, then copies the metadata from the symbol to var when creating the var

hiredman01:01:37

But when aot compiled the copying of the metadata from the symbol isn't done (I believe because the vars are created in a static init block before they are given a value)

Dave Russell01:01:12

Whoa, thanks!! Do you have any links/source code where I can read more?

hiredman01:01:54

(source 'defmacro)

Dave Russell01:01:27

Thanks, I've read that -- I'm mostly curious about the lack of metadata copying on AOT compiling

hiredman02:01:29

That I am being more hand wavy about

hiredman02:01:17

I am not sure what mechanism is causing that behavior, you might see it if you aot compiled with elide metadata set

didibus05:01:05

I think that's a AOT optimization, there's a flag like Hiredman said, it's to make the bundle and runtime memory smaller by removing meta on vars which normally are useful only when developing

didibus05:01:25

Also might minimally speedup startup maybe

Dave Russell09:01:18

Thanks! We aren't using metadata eliding though.. :thinking_face:

Miro Bezjak18:01:48

Actually, I think AOT doesn't remove metadata. The problem is that only {:macro true} gets removed. Here is what I think I understood. If you have a namespace

(ns example.lib)

(defmacro inc-macro [x]
  `(+ 1 ~x))

(def ^{:macro true :additional true} inc-macro2 #'inc-macro)
And you create an uberjar out of it. The code that was generated is slightly different compared to running the clojure code. Take a look at the decompiled class lib__init.class . You'll see the following. The metadata is really here:
const__27 = (AFn)RT.map(new Object[] { RT.keyword((String)null, "macro"), Boolean.TRUE, RT.keyword((String)null, "additional"), Boolean.TRUE, RT.keyword((String)null, "line"), 13, RT.keyword((String)null, "column"), 1, RT.keyword((String)null, "file"), "example/lib.clj" });
But the way the binding of compiled code works is
final Var const__7 = lib__init.const__23;
        const__7.setMeta((IPersistentMap)lib__init.const__27);
        const__7.bindRoot((Object)lib__init.const__19);
(const__23 is const__23 = RT.var("example.lib", "inc-macro2");) So first .setMeta then .bindRoot. Let's see what bindRoot does: https://github.com/clojure/clojure/blob/e6fce5a42ba78fadcde00186c0b0c3cd00f45435/src/jvm/clojure/lang/Var.java#L277
alterMeta(dissoc, RT.list(macroKey));
Notice the comment on the method
//binding root always clears macro flag
Indeed. If you use the uberjar in a REPL. You'll get
{:additional true,
 :line 6,
 :column 1,
 :file "example/lib.clj",
 :name inc-macro2,
 :ns #namespace[example.lib]}
:macro is gone. But not the :additional. @U01BP1CB37B Unfortunately I don't think you can use your "trick" with AOT'ed code due to how clojure is implemented. But why does running in the REPL work (no AOT)? Because the order of those methods is reversed. https://github.com/clojure/clojure/blob/48e3292bb75015616ebce32de85428b71ac8dc1c/src/jvm/clojure/lang/Compiler.java#L446 First .bindRoot then .setMeta. At least I think that is correct. Someone with more experience (Alex?) can check my work. This might be a workaround
(defmacro inc-macro3 [& form]
  `(inc-macro ~@form))
Then with AOT'ed code it works ok
(lib/inc-macro3 1)
=> 2
vs.
(lib/inc-macro2 1)
=> clojure.lang.ArityException
   Wrong number of args (1) passed to: example.lib/inc-macro
Hope that helps.

hiredman19:01:16

or just explicitly set the macro flag like the defmacro macro does

Miro Bezjak19:01:29

Or that... 🙂

hiredman19:01:00

because the code that explicitly sets the flag instead of relying on metadata copying will just get run again in aot'ed form

👍 2
Miro Bezjak19:01:41

This works.

(ns example.lib)

(defmacro inc-macro [x]
  `(+ 1 ~x))

(def ^{:macro true :additional true} inc-macro2 #'inc-macro)
(.setMacro #'inc-macro2)
AOT then use
(lib/inc-macro2 1)
=> 2

Dave Russell19:01:20

Thanks all! Interesting read, @U04BP7T2GTA -- ended up going with that exact solution (just wrapping with a normal defmacro), which IMHO might be preferable for others' understandability since:

(def ^:macro foobar #'other-foobar)
(.setMacro #'foobar)
Might be more confusing than simply:
(defmacro foobar [& args] 
  `(other-foobar ~@args))
🙂

hiredman19:01:48

if you add macro falg after you don't need to also do ^:macro

Dave Russell19:01:18

Yep, but still slightly strange code to come-across, because then you might ask "why not add ^:macro metadata instead?" and the cycle continues! 😛

hiredman19:01:21

depends. if you have a call to setMacro it is less likely the automatic reflex will be to switch to ^:macro, if you have a call to alter-meta! then it is more likely

☝️ 2
Miro Bezjak20:01:19

Actually I came to a slightly different conclusion why clj code works in the REPL after I cleared my head. It's not because of the reverse of .setMeta , .bindRoot. @U0NCTKEV8 was right all along. It's all about the implementation of defmacro. https://github.com/clojure/clojure/blob/b2366fa5c748f9d600879c3e0b549e631a5b386f/src/clj/clojure/core.clj#L489 defmacro will first to defn (essentially def) which does execute DefExpr.eval with the order of .bindRoot and .setMeta. But immediately afterwards it will execute .setMacro that overrides macro's metadata. Plus it's not like .setMeta will have {:macro true} anyway. --- To confirm

(defmacro ^{:macro false} dec-macro [x]
  `(- ~x 1))

(meta #'dec-macro)
=>
{:macro true,
 :arglists ([x]),
 :line 10,
 :column 1,
 :file "/home/mbezjak/workspace/use-uberjar/src/using/core.clj",
 :name dec-macro,
 :ns #namespace[using.core]}

JoshLemer05:01:20

would it be good practice to include ^:private when declaring a var that will be private when it is defined?

(declare ^:private foo)

didibus05:01:07

In my opinion it's a bad practice to use declare altogether

JoshLemer05:01:19

Yes, though sometimes necessary

didibus05:01:21

(declare ^:private foo)
(meta #'foo)
;; => {:private true, :declared true, :line 270, :column 1, :name foo}
(def foo 10)
(meta #'foo)
;; => {:line 273, :column 1, :name foo}

didibus05:01:11

The meta is overwritten by the latter def evaluation. FYI.

didibus05:01:38

So I'd say it might be best to define the meta on the def, and not on the declare. Or make sure to have it on both.

❤️ 2
Dimitar Uzunov09:01:21

Hi folks, is it possible to constrain the resources a function or a multimethod like we can for OS processes? I.e. prevent a route handler from using more than one core and a gig of RAM and make it return nil if reaches a limit?

Ernesto Garcia09:01:36

You could delegate the task to a limited thread-pool.

👀 2
jumar10:01:05

> You could delegate the task to a limited thread-pool. That doesn't make it memory-constrained though. That task is also free to spin up it's own threads under the hood.

jumar10:01:11

@ULE3UT8Q5 If you really want that, I would suggest running separate processes instead for better isolation. That said, a https://clojurians.slack.com/archives/C03S1KBA2/p1614110292087900 I did an experiment - it's far from bulletproof but it sort of works: https://github.com/jumarko/clojure-experiments/blob/develop/src/clojure_experiments/performance/memory.clj#L224-L229

Dimitar Uzunov10:01:27

@U06BE1L6T running separate processes is my go-to approach, but it has its limits..

Dimitar Uzunov10:01:06

I’m mostly concerned about limiting CPU time

jumar10:01:24

You cannot do that easily. If you are trying to run untrusted code then you are asking for trouble. If you own the code and know what it's doing then the approaches suggested above might work.

orestis11:01:20

clojure.cache seems to be non thread-safe:

(let [thread-count 20
        cache-atom (-> {}
                       (cache/ttl-cache-factory :ttl 120000)
                       (cache/lu-cache-factory :threshold 100)
                       (atom))
        latch (java.util.concurrent.CountDownLatch. thread-count)
        invocations-counter (atom 0)]
    (doseq [i (range thread-count)]
      (println "starting thread" i)
      (.start (Thread. (fn []
                         (cache-wrapped/lookup-or-miss cache-atom "my-key"
                                                       (fn [k]
                                                         (println "GENERATING CACHE VALUE" i k)
                                                         (swap! invocations-counter inc)
                                                         (Thread/sleep 5000)
                                                         (println "GENERATED CACHE VALUE" i k)
                                                         "VALUE"))
                         (.countDown latch)))))

    (.await latch)
    (deref invocations-counter))

orestis11:01:19

I was expecting to see a single invocation (based on the API) but it seems I have to add a synchroniser or something myself. This is an issue with a web environment where you might get multiple requests for the same key.

phill11:01:32

Are you concerned about the anonymous function being called more often than you expected? It's probably used with swap! in the cache library, so keep in mind that swap! can run the updater function again (and again) if it hits a collision.

orestis11:01:37

Yes, the anonymous function should only be called once per key. I read the documentation in the core.cache usage, it mentions it's safe from a cache stampede, but it's not 100% accurate.

orestis11:01:04

The function is wrapped in a delay within core.cache so that swap! will not call it again. It's being called once per thread though.

jumar11:01:28

I think that's why people recommend core.memoize. FOund this article I read a couple of years ago: https://quanttype.net/posts/2020-10-25-caching-http-requests.html

orestis11:01:55

Ah, thanks @U06BE1L6T I will take a look there.

jpmonettas11:01:46

@U7PBP4UVA I'm not sure non thread safe are the words here, I guess look-up-or-miss is supposed to be about looking up data and not generating side effects?

orestis11:01:14

Well you have to somehow generate the data in the case of cache miss.

jpmonettas11:01:33

yeah, but if it is all about retrieving, the worst thing is some extra calls, so not as efficient as it can get, but nothing should be wrong, so it should be safe

orestis11:01:01

oh yeah in that sense it's thread safe as the result is eventually correct - but it's a version of cache stampede.

jpmonettas11:01:12

yeah, that I agree

jpmonettas11:01:05

but I think defining your own lookup-or-miss wrapper that locks on the cache object should do the trick

p-himik12:01:26

That's exactly what the article linked above mentions: > You could implement this yourself by doing some locking… but you could also use https://github.com/clojure/core.memoize, which does it for you.

p-himik12:01:45

Caches are notoriously hard to get right, so unless you're 100% certain in what you're doing and there's nothing available out there that suits your needs, you should definitely stick to the existing solutions, core.memoize in particular.

orestis12:01:49

Yep, same sentiment here @U2FRKM4TW

chrisn13:01:01

Maybe consider caffeine

chrisn13:01:07

The library :-)

😅 12
orestis13:01:50

core.memoize seems to do exactly what we want. Thanks everyone for the suggestions!

orestis13:01:56

(let [thread-count 20
        key-mod 3
        invocations-counter (atom 0)
        expensive-function (fn [k]
                             (swap! invocations-counter inc)
                             (Thread/sleep 3000)
                             (str "value-" k))
        cache (-> {}
                  (cache/ttl-cache-factory :ttl 120000)
                  (cache/lu-cache-factory :threshold 100))
        memoized-function (memoize/memoizer expensive-function cache)
        latch (java.util.concurrent.CountDownLatch. thread-count)]
    (doseq [i (range thread-count)]
      (println "starting thread" i)
      (.start (Thread. (fn []
                         (memoized-function (str "my-key" (mod i key-mod)))
                         (.countDown latch)))))

    (.await latch)
    [(memoize/snapshot memoized-function)
     (deref invocations-counter)]
    (assert (= key-mod (deref invocations-counter))))

seancorfield16:01:50

FWIW, it took me several rounds to get it as "safe" as it currently is but I'm still a bit surprised that you're seeing a sort of stampede with cache-wrapped/lookup-or-miss but not with memoize/memoizer -- can you put this example up on http://ask.clojure.org so I can take a closer look -- and I'll probably create a JIRA issue against core.cache for it...

jeroenvandijk16:01:07

I have been using it with a delay inside of the lookup-or-miss! and then deref the outcome. Think this works fine:

@(cache-wrapped/lookup-or-miss cache-atom "my-key" (fn [k] (delay ...)))

jeroenvandijk17:01:48

Ah I see the issue. I’m not using the wrapper and I see the wrapper is wrong.. It’s dereffing the delay inside the swap. It should happen outside of the swap! https://github.com/clojure/core.cache/blob/master/src/main/clojure/clojure/core/cache/wrapped.clj#L67 @U04V70XH6

jeroenvandijk17:01:15

With many threads having a cache miss at the same time could mean that they all deref their delay during the compare-and-set loop of swap!, causing more than one execution. The fix is simple I think. Instead of

(c/lookup (swap! cache-atom c/through-cache
                             e
                             default-wrapper-fn
                             (fn [_] @d-new-value))
I think it should be
@(c/lookup (swap! cache-atom c/through-cache
                             e
                             default-wrapper-fn
                             (fn [_] d-new-value))
This will guarantee only one delay value will be cached and executed

seancorfield17:01:41

If you can write this up on http://ask.clojure.org, I'll take a look at it when I can... probably in a week or two (I'm planning to schedule time to work on core.cache documentation early next month).

jeroenvandijk17:01:31

@U7PBP4UVA do you want to write the problem statement in http://ask.clojure.org? I can reply with a solution (I think)

seancorfield17:01:54

Sounds good. Thanks!

👍 2
orestis17:01:55

I had this in mind. I will do the writeup.

👌 2
2
orestis18:01:26

For now we fixed it by using core.memoize, but I recall a colleague fixing something like this by using a promise - it would be eagerly assoc'ed in the cache, so that subsequent requests would get a single promise, and the first thread would actually do the work and deliver the value (details are hazy - this was not core.cache).

seancorfield18:01:34

Yeah, ISTR core.memoize uses delayed values in the cache but core.cache deliberately does not -- because that would put the burden on the user to correctly deref things (and add confusion over what types of values were in the cache).

jeroenvandijk18:01:12

I added my answer with a potential workaround HTH

2
emccue14:01:28

Has anyone successfully set up Togglz with a ring app? I don't want to home-roll feature flags (again)

emccue14:01:38

I've made it about this far

emccue14:01:57

(ns togglz
  (:import (org.togglz.core Feature)
           (org.togglz.core.manager FeatureManagerBuilder)
           (org.togglz.core.repository.jdbc JDBCStateRepository$Builder)
           (org.togglz.core.spi FeatureProvider)
           (org.togglz.core.user SimpleFeatureUser UserProvider)
           (org.togglz.core.metadata FeatureMetaData)
           (org.togglz.core.repository FeatureState)))

(def features
  {::new-home-screen {}})

(defrecord KeywordFeature
  [kw]
  Feature
  (name [_]
    (name kw)))

(defn map->metadata
  [feature feature-metadata-map]
  (when (seq feature-metadata-map)
    (reify FeatureMetaData
      (getLabel [_]
        (:label feature-metadata-map))
      (getDefaultFeatureState [_]
        (FeatureState. feature (:enabled-by-default feature-metadata-map)))
      (getGroups [_]
        #{})
      (getAttributes [_]
        {}))))


(defn keyword-feature-provider
  [features]
  (reify FeatureProvider
    (getFeatures [_]
      (-> (keys features)
          (map ->KeywordFeature)
          (into #{})))

    (getMetaData [_ feature]
      (some->> (:kw feature)
               (get features)
               (map->metadata feature)))))

(defn user-provider
  []
  (reify UserProvider
    (getCurrentUser [_]
      (SimpleFeatureUser. "ethan" true))))

(defn start-feature-manager
  [db]
  (-> (FeatureManagerBuilder.)
      (.featureProvider (keyword-feature-provider features))
      (.stateRepository (-> (JDBCStateRepository$Builder. db)
                            (.tableName "togglz")
                            (.createTable true)
                            (.build)))
      (.userProvider (user-provider))
      (.build)))

emccue15:01:57

where im getting stuck, conceptually, is the wiring up of the admin panel - how tf. do i put servlet handler under ring middleware?

emccue15:01:17

oh...amazing > Caused by: java.lang.NoClassDefFoundError: javax/servlet/Filter Love the jakarta rename

robert-stuttaford15:01:51

honestly i would rather just hand roll flags :thinking_face:

Noah Bogart16:01:15

in the implementation of clojure.core/for, the #_"inner-most loop" does some odd stuff with looping over the chunked buffer and appending to a new chunked buffer. does anyone know why it's written that way?

Alex Miller (Clojure team)16:01:11

the intent is to kind of keep the chunk structure (maybe that's what you mean)?

Noah Bogart16:01:37

i think so. looking at the commit that added it, it says "add support for chunked seqs to 'for'". i think I just don't understand enough about the chunking system to get why this is needed.

didibus17:01:25

I think it's so that it returns a chunked sequence as well, otherwise it would have the behavior of unchunking

mars0i17:01:12

I've been using Leiningen for a long time, and previous brief forays into deps.edn were frustrating (because I didn't want to go through a long process of figuring out how to do what I already knew how to do while I was busy :grinning:).  Now finally, I have a new simple project that I can use to gradually explore CLI and deps.edn.  So far so good, except for one oddity with starting nrepl to use with Conjure.  I have two computers.  On one, I can leave the nrepl dependence out of my :deps map, but start nrepl using this alias:

seancorfield17:01:39

Run clojure -Sdescribe on both machines and check that they are finding the deps.edn file(s) you expect.

seancorfield17:01:24

(perhaps one machine is XDG and the other isn't and you have the dot-file in a place it is not expecting @mars0i?)

Alex Miller (Clojure team)17:01:30

also, is Clojure CLI version the same? (`clj --version`)

seancorfield17:01:34

@mars0i wrote:

:aliases {:nrepl-server {:extra-deps {nrepl/nrepl {:mvn/version "1.0.0"}}
                          :main-opts  ["-m" "nrepl.cmdline"]}}
Then I can start nrepl with clj -M:nrepl-server. On the other computer, if I do that, I get an error:
Execution error (FileNotFoundException) at clojure.main/main (main.java:40).
Could not locate nrepl/cmdline__init.class, nrepl/cmdline.clj or nrepl/cmdline.cljc on classpath.
If I move the nrepl dependency into :deps and just leave the :main-opts in the alias, the problem goes away. So that's a solution, and I don't mind requiring nrepl every time. But it's puzzling. What's different between the machines? Both are using the same deps.edn, with clojure.1.11.1. Machine A (the one with no error) is using Java 17 (because I got errors with another project with Java 1.8). So I tried using Java 1.8 on machine B (the one with the error), but that didn't fix the problem. I downloaded Java 19, but that doesn't fix the problem either. I tried completely removing .m2 so that all of the libs were downloaded again. Still no change: error with nrepl in the :extra-deps, no error in :deps. Any thoughts about what I should investigate? (I will say that there have been weird differences between these machines in the past. I think that has to do with machine A having stuff left around from very long ago.)

mars0i17:01:47

Interesting. Thanks @U04V70XH6! I just realized that I should have waited to ask when I had both machines present. At work and I only have machine A. -Sdescribe does say that I have a deps.edn in ~/.clojure, but it's just an empty map. I'll check machine B when I get home.

mars0i17:01:57

What is XDG?

seancorfield17:01:26

@mars0i I think Alex might be right that it's a CLI version issue -- -M has changed its meaning over time.

mars0i18:01:04

@U064X3EF3 Oh, didn't think of that. i.e. clj could be one version, but it loads another version of Clojure that's listed in deps.edn? I'll check that.

seancorfield18:01:49

The -M option has changed its meaning. With an old CLI it would not pick up extra-deps, just main-opts.

seancorfield18:01:11

clojure -Sdescribe will show the version (as well as file paths)

mars0i18:01:31

I see about -M. OK, thanks. I'll check that stuff. It would be nice to understand what's happening. Great! Thanks.

mars0i18:01:42

@U04V70XH6 about using a thread--thanks. I was just trying to include the code blocks in the original question and couldn't remember how to do that in Slack without making a new post.

mars0i18:01:06

In the future I'll post a short description of the question and then add details in a thread.

seancorfield18:01:09

Depends on your preference settings in Slack but ctrl-enter should allow you to enter newlines without sending the message I think...?

mars0i18:01:18

OK, will try that.

seancorfield18:01:31

But, yeah, short problem statement in the main channel, details in a 🧵 is best.

👍 2
seancorfield18:01:09

When you're back at the non-working machine, let us know what version the CLI was and if updating it solved your problem.

mars0i18:01:35

OK, will do--thanks!

mars0i22:01:10

Interesting @U0K064KQV. I'll try that. Thanks.

mars0i02:01:08

@U04V70XH6, @U064X3EF3 Yes--it was the CLI version. The machine with the error had 1.10.1.478. The one without the error has 1.11.1.1208. Upgrading CLI fixed the problem. Thank you.

2
seancorfield17:01:37

@mars0i I've replied in a thread -- please use threads for extra details after posting a question instead of multiple messages in the main channel. [edit: I copied your messages into the thread and deleted them from the main channel]

Asher Serling19:01:40

Hey everyone. I'm trying to use Drift for migrations in my app but I'm running into an issue. Running my app in local it works fine, but it dev it's not finding the migrations. I find that this bit of code which it runs as part of the process of finding migrations

(loading-utils/classpath-directories)
returns an empty seq when running my app as a .jar file, as it is in production. Can someone please explain to me why this is? Sorry for my ignorance, this seems to be quite basic

hiredman19:01:03

just looking at the examples in the readme for drift it doesn't look like it is intended to run from a jar with migrations as resources in the jar

Asher Serling19:01:12

it sounds like you're saying that within a jar files are accessible as 'resources'. what does this mean?

hiredman19:01:25

Files are things that exist in a filesystem, once you put things in a jar, they aren't files anymore, but the jar is

Jakub HolĂ˝ (HolyJak)21:01:38

look at the http://java.io/resource fn. Many java / clj libs can work with resources that are on the classpath via this fn, but some simply expect a physical file on the FS

Asher Serling21:01:24

thanks @U0522TWDA I'll give it a look

Asher Serling21:01:57

can you reccomend any material for understanding what a resource is?

Jakub HolĂ˝ (HolyJak)22:01:59

But if drift only supports file system and not classpath resources then you are out of luck. Personally I use https://github.com/weavejester/ragtime, which is maintained. There is also https://github.com/yogthos/migratus, which I haven’t tried

Asher Serling23:01:16

i took a look at ragtime before but i got the impression that they leave a lot more work to the client. i'm going to switch to migratus

valerauko03:01:19

a jar file is a glorified zip. resources = files inside the zip (in a specific location). files = files outside the zip.

hiredman04:01:23

Resources are steams of data you retrieve via a class loader, entries in a jar file is just the most common place a class loader will retrieve them from

Asher Serling07:01:14

i'm finding the lions share of what's challenging about learning Clojure being not Clojure itself, but what Java I need to learn in order to use Clojure. especially all of the stream stuff, this type of stream and that type of stream. coming from a JavaScript and PHP background this stuff is mostly unfamiliar. is this an area of Java which is really complicated, or is it really simple and justs needs to be gotten the hang of?

Ben Lieberman20:01:03

Is it a bad idea (or an anti-pattern etc) to describe business logic with a protocol if the implementation merely computes new data from existing data in a map? Should I instead define functions that do the same thing, sans protocols?

nwjsmith20:01:18

Yeah, if you're not looking to do anything polymorphic there isn't much reason to use a protocol.

💯 2
Ben Lieberman20:01:32

that was my intuition that I was nevertheless in the process of ignoring, so wanted to check 😅 thanks @U04V32P6U

Dane Filipczak21:01:00

I agree with @U04V32P6U but want to add that polymorphism is a spectrum and, if experimentation is cheap, exploring the implementation space could yield unexpected results 👍

nwjsmith21:01:58

What do you mean by "polymorphism is a spectrum"?

nwjsmith21:01:38

It's monomorphic -- operating on a single data type -- or it's polymorphic, no?

didibus21:01:04

The features are upgradable, so there's no reason to start with a protocol just because you're not sure if you might need one in the future. You can have functions, then feel one of them would benefit being polymorphic, switch to a multi-method, then realize a set of them would benefit being polymorphic together, now switch to a protocol, all without ever breaking the call-sites

👍 4
2
didibus22:01:55

The function call would still always just be:

(fn-name operand paramerters...)

Dane Filipczak22:01:30

@U04V32P6U Sort of - in the case of concrete types and the usual usage of protocols, you’re correct. What I meant by “polymorphism is a spectrum” is that there’s a practical continuum between reaching for conditional logic on a map, multi-methods/hierarchies, and type-based dispatch. I apologize for the ambiguity and my comment’s possible irrelevance to the original question.

nwjsmith14:01:12

Oh no need for an apology! I was curious to understand more, thanks