Fork me on GitHub
#beginners
<
2023-03-04
>
Louis Gariépy04:03:47

Hello. I am not a clojure programmer, and I'm not (yet) trying to learn it, but I'm reading some clojure code and I'd like to understand what's happening. Specifically, this: `

#schema.core.OptionalKey{:k :something}
` What does the :k refer to here? I think I understand the gist that this represents an optional key in a map, and :something would be the name of that particular key, but it's unclear to me what the purpose of :k would be. Thanks 🙂

rolt10:03:15

k is the the name of the field of the class OptionalKey (with the value being :something)

rolt10:03:46

#my.package.Point{:x 1, :y 2} would be the usual notation for a point in a 2 dimensionnal space for instance

rolt10:03:07

(OptionalKey only has one field so it looks a bit stupid, but you still need a name for it)

delaguardo10:03:47

small addition - #C1923ED97.core.OptionalKey{,,,} is a datified representation of the record schema.core/OptionalKey

delaguardo10:03:42

it might feel a bit stupid to have just one field in a record but there might be some protocols implemented for the same record that doesn't have data representation.

Louis Gariépy20:03:12

I see, thanks 😄

Frederic Latour14:03:05

Hi, Has anyone any pointers to some article or whatever that would clarify the project management tooling situation. Is Leiningen still a thing ? From my reading, it sounds like clj + deps.edn (tools.dep) is a viable alternative. Is it easy to migrate a project from one to the other ? (It reminds me of the cabal + stack situation but I may be wrong) Thanks in advance

phill14:03:34

Rule of thumb: "never use a build tool that anyone is still working on". By this standard, Leiningen is 4 stars (and the Maven Clojure Plug-in is 9 stars).

2
pavlosmelissinos14:03:33

> Is Leiningen still a thing ? Yes, it is, some people even say that lein is easier for beginners. Leiningen is what I used up until 2019 (when we switched to tools.deps). Unfortunately I never got to learn it well enough to have a definitive opinion. Clojure CLI seems to be simple and good enough for what I've done so far, I wouldn't go back. > Is it easy to migrate a project from one to the other ? https://github.com/borkdude/lein2deps Not sure about the other way around

pavlosmelissinos14:03:13

(I'll use CLI to refer to Clojure CLI, for brevity) > Has anyone any pointers to some article or whatever that would clarify the project management tooling situation. I'm not aware of a single thing that compares them but: • https://clojure.org/guides/deps_and_cli is essential in understanding it • https://codeberg.org/leiningen/leiningen/src/branch/stable/doc/TUTORIAL.md is very comprehensive If I had to describe both in a sentence I'd say that lein is easier but the Clojure CLI is simpler My perceived Pros/Cons of each: Lein Pros: batteries included, user-friendly, one tool for all project management Cons: slow start up, does some magic to bring it all together, which could lead to unpredictable situations if you deviate from the happy path CLI Pros: simple, bundled with Clojure, quick start up Cons: awkward/unintuitive commands, you need third party tooling to approach lein's ease-of-use And an example of awkward syntax: lein test doesn't have an out-of-the-box equivalent in CLI world, you have to add a new alias to your deps.edn and then run clojure -M:test or bring in an external tool like neil or deps-new

practicalli-johnny15:03:50

Leiningen and Clojure CLI are very different tools and not directly comparable . Use which ever approach fits your preferred workflow Leiningen is a project automation tool with a large amount of functionality built in, with additional Leiningen plugins available Clojure CLI is a tool to run Clojure (and manage the download of dependences from maven repos & Git repos) Clojure CLI can be used with tools.build and community libraries to create a custom project management workflow. Aliases in project and user deps.edn configuration define specific tasks/ tools https://practical.li/clojure/clojure-cli/ shows example of managing projects with Clojure CLI, tools.build and community libraries and provides a user deps.edn with a wide range of tools

👍 2
seancorfield19:03:12

A bit of historical context also helps here: lein was the only real choice for many years, so that's why a lot of books and tutorials still focus on it. Boot was an alternative that appeared (and worked very well) in the mid-2010's. The official Clojure CLI appeared in 2018 and is designed in keeping with Clojure's principles: simple and composable. At work, we started with Clojure in 2011 and had to use Leiningen. We switched to Boot in 2015 because we needed more "programmability" in our builds and we had a large, complex monorepo that was just hard to manage with Leiningen (we had grown all sorts of custom shell scripts and ant stuff around it). Boot let us throw all that away and write all our build tasks in "plain Clojure". But Boot had some interesting abstractions and complexity of its own that ultimately began to break down for us. We embraced the CLI and deps.edn pretty much as soon as it appeared and then added tools.build and build.clj when that appeared. Now we have everything we had with Boot -- everything is "just" Clojure functions -- but with the simplicity and careful design that the core team bring to Clojure itself. We're very happy with the official tools. Bottom lines: • If you're following a book or tutorial that uses lein, then don't worry, just keeping using lein. The same goes for any project you create from a template that has project.clj. Leiningen still works just fine. • If you're following a book or tutorial that uses clojure/`clj` or working with a project that has deps.edn, just keep using the CLI. • If you're starting something from scratch, I'd recommend learning about the CLI, deps.edn, and build.clj because that's where the community is going overall. According to https://clojure.org/news/2022/06/02/state-of-clojure-2022 Leiningen usage has dropped from 90% in 2018, to around 70% now, while deps.edn and the CLI has climbed from just a few percent in 2018 to around 65% now. I would expect this year's survey -- open now, go fill it out! -- to show the CLI slightly ahead of Leiningen. 2023 survey: https://www.surveymonkey.com/r/clojure2023

💡 2
Frederic Latour22:03:13

Thanks to all of you for taking the time to give a detailed answer. @U04V70XH6 Your Bottom lines was my exact feeling after a couple of readings but then I've been watching some video and they were using Lein. This decided me to try to get some clarification directly from clojurians. I will start my Clojure journey with cli / deps.edn with a little help of deps-new (saw a convincing video from a guy named Kozieiev). Thanks again.

🙂 2
seancorfield22:03:30

There's a #C019ZQSPYG6 channel if you need assistance with that and #C6QH853H8 and #C02B5GHQWP4 for deeper help on the machinery behind the CLI / deps.edn and the build.clj files respectively.

👍 2
Jakub Šťastný17:03:55

What's the best way to parse strings like ":task yes :shebang \"#!/usr/bin/env clojure -M\""? It's nearly EDN, but not really, some of these aren't escaped (they come from Org mode source blocks). I want to get a map of {:task "yes" :shebang "#!/usr/bin/env clojure -M"}. (or true for task, even better, but doesn't matter).

Jakub Šťastný17:03:06

My idea was to make it a valid EDN and read it as an EDN string, but having quoted strings there complicates things as I can just str/split by " " and treat these chunks as a whole thing, as I might get "clojure" there which I'd quote since it looks unquoted, but it's quoted by previous chunk "\"#!/usr/bin/env".

Jakub Šťastný17:03:57

So I'm looking for an alternative 🙂

Bob B17:03:28

What's the actual string? You said some things aren't escaped.. if you took that as a string literal, it parses as valid EDN

Jakub Šťastný17:03:28

The thing that isn't escaped here is yes.

Jakub Šťastný17:03:49

If I try to read the whole string (see the first post), I only get :task back. I assume it's because it fails on yes as it's not a string.

Bob B17:03:52

so, that'd be a symbol... if you wanted to, you could take the name of any symbols that come through (although 'strings' that don't read as symbols could be problematic if they're in the domain

Bob B17:03:15

it doesn't fail... read returns the next object from stream

Jakub Šťastný17:03:55

OK interesting. I'm using edn/read-string.

Jakub Šťastný17:03:28

(prn (edn/read-string ":task yes :shebang \"#!/usr/bin/env clojure -M\""))
; => :task

Jakub Šťastný17:03:58

Ah nope I'm forgetting to wrap it in {}.

Bob B17:03:06

; this is thrown together... I wouldn't trust it to be robust...
(defn edn-read-all [s]
  (let [r (PushbackReader. (StringReader. s))]
    (loop [o []]
      (if-let [x (clojure.edn/read {:eof nil} r)]
        (recur (conj o x))
        o))))

(edn-read-all ":task yes :shebang \"#!/usr/bin/env clojure -M\"")
  ;; => [:task yes :shebang "#!/usr/bin/env clojure -M"]

🙏 2
skylize03:03:36

I'm guessing the whole PushbackReader thing above is likely helpful. But if you already have the string in hand, you could wrap your 4 expressions into a pair of parens/braces/brackets so you have a single expression to read.

(prn (edn/read-string 
      (str "{"
           ":task yes :shebang \"#!/usr/bin/env clojure -M\""
           "}")))
; =>
{:task yes, :shebang "#!/usr/bin/env clojure -M"}
nil

Frederic Latour23:03:42

Hi guys, I've been reading a couple of things about types / refactoring and so on. I must admit that I haven't really been able to completely understand how one can maintain large clojure codebases without types. Obviously, most clojurians seems to be perfectly happy the way it is. Therefore, there must be a combination of things that I can't seem to get. I'm coming from the typescript world (mostly node and some front-end). I have embraced typescript from the very beginning. Besides intellisense and the prevention of many runtime type errors, the key point to me is refactoring. I'm refactoring all the time. Even if some piece of code is working, I will refactor it if it does not feel right or simplifies things up. The nicer it is the easier to re-read and to understand. Would you guy share any clue on why you do not feel types would help in Clojure ? How do you approach refactoring ? I've read about protocols. Do they help for intellisense ? Is there some checking based on protocols. I've also heard of type hint but as far as I could read, that's not really a thing that is embraced widely. Am I correct? By the way, when I write some python, I tend to use type hint with the help of mypy. So, maybe it is just me 🙂

jgood23:03:27

Two concepts that the Clojure community embraces that I think mitigate the need for types: • Spec the edges of the system • Accrete changes Specs define the shape of data and, imo, are absolutely needed for a large scale application. The art of it is deciding where edges exist. Lately I've been leaning with frontend, backend, and the database. Any information being passed along those boundaries should have a spec. I could see a really large codebase putting specs between layers of the backend or front end. I also like to spec the my front end re-frame app-db (big client state map). The à la carte nature of spec is fulfilling one of the goals of typed languages -- defining shapes of data moving through a system but it doesn't have as much friction as having the definitions on every function at all times. I think a philosophical goal of many Clojure developers is to not break backwards compatibility. If the system needs to change dramatically I normally try to just add new functions and slowly start using them across the system rather than refactor existing functions.

seancorfield23:03:23

There's a lot to unpack here but I'll cover a couple of things: • Clojurians typically work with raw data and rely on abstractions (sequences, associative data, etc) so a lot of things that would be represented by typed data in other languages are just plain hash maps in Clojure -- so functions take and return plain hash maps a lot and maybe some vectors. • A lot of Clojurians are relying on LSP or similar in their editors these days which can catch arity errors and provide access to docstrings directly in the "intellisense" offered in the editors -- so you don't even need to run code to spot arity errors, and it's pretty easy to check a function signature and docstring inline when writing a function call. • And then we use the REPL a lot: we eval small expressions to verify our understanding of behavior, and we're able to work very interactively in a way that isn't possible in other languages -- your app is usually running in your REPL, connected to your editor, and you evolve the code "live". That said, I did a large refactoring last year, working in a section of the codebase I did not know very well. I started by writing Clojure Spec definitions for functions -- describing the shape of the data I thought they expected, and working iteratively via my REPL until I had accurate Specs in place for several of the key functions I wanted to refactor. That left me move things around pretty easily -- and of course I was able to validate each small change as I made it, directly in the REPL connected to my editor.

👍 4
seancorfield23:03:43

(some background: we have a monorepo with 136k lines of Clojure, spread across about 160 subprojects, that builds almost two dozen deployable services, running around 40 online dating sites -- and in any other language it would be a substantially larger codebase, and was much larger before we rewrote it in Clojure!)

👍 2
Frederic Latour00:03:09

Just reading the specs stuff. I had seen the word in a place or another but had made the assumption that it was unit/integration testing related. Basically, one could validate each parameter with specs. But I suppose that it not what is normally done. If I'm reading both of you well (@UE0HLL163 @U04V70XH6) it will mostly being used for the most important structure or maybe the key functions. Is there anything in terms of intellisense for Map ? Well I suppose that without a way to say that some def implements this specific map, there is no way for the editor to help by proposing available keys.

Frederic Latour00:03:45

@U04V70XH6 Not yet Google monorepo but still enough code to appreciate strength and weaknesses of the language and its ecosystem. Do you deploy using docker image ? Kubernetes infrastructure ?

seancorfield00:03:51

@U04S6P8TL78 This article from 2019 talks about the various ways we use Spec https://corfield.org/blog/2019/09/13/using-spec/ As for deployment, we deploy JAR files to private cloud servers in a data center -- we haven't moved to containers yet 🙂

dgb2311:03:52

I'm personally neither in neither "camp" when it comes to static vs dynamic typing. I also like languages like TS! But I care about a couple of things: 1. For certain types of code I need__ strong validation and consistency. Especially say at the edges of a program to make decisions about input provided from elsewhere. 2. Library/foundational code is much better if it automatically explains exactly the shapes of data and the function signatures etc. 3. I like having my code to be validated while I'm coding in some way. 4. I sometimes want to be able to tell the compiler/runtime exactly what the types/data is in order to gain better performance characteristics (latency/resource usage etc.) --- Typescript is a very nice language with a great type system. I really like structural typing and the ability to narrow down types with control structures etc. Tagged unions are very easy to encode in Typescript and are one of my favorite ways of structuring things. However TS only gives you point 2 and point 3: - On 1: There are libraries that allow you to infer types from validation/schema-like constructs, but they usually do this in a non-data driven fashion. Typescript itself has no notion of types at runtime, no reflection etc. You often have to do things twice__ in order to get runtime validation. - On 4: It has no direct impact on how your code is compiled (JIT) that you don't already get in JS. --- Clojure gives me all of these 4 things, in some cases with certain caveats and in some cases with more__ power than a typical typesystem, even a very expressive one like TS has, gives you: 1. Spec and malli give you powerful, re-usable and composable schema descriptions for data and function signatures that you can use for runtime validation, generative tests and so on. You can encode arbitrary predicative rules with them (as opposed to just the shape), so you can express more things than say in TS because of this. 2. If you describe function signatures with these libs, you can leave out stuff that is not important and say much more about the things that are. You can encode contracts between the input (assumptions), output (guarantees) and additionally also the dependency between them! 3. If you are using a linter like clj-kondo (You get that automatically with Calva/clojure-lsp). Then it will catch a whole lot of things by default, additionally you can automatically generate linting rules from schemas (malli and I think also spec?). 4. Clojure core gives you a lot of tools like type hinting, deftype, to-array and so on if you really care about avoiding reflection, memory layout and so on. Additionally there are libraries that will even let you emit bytecode directly and use native data structures and so on. There is nothing__ standing in your way to get very, very fast code with Clojure if you need to. Additionally you get even more things that are specifically related to refactoring, changing and understanding code: - The REPL just generally gives you a much nicer approach to write, understand and change code. It's like hot reloading but on steroids and more. You can easily navigate into an expression that is inside a function and just evaluate isolated things. Always keep it running! - You can just generate or write data directly into your REPL, get the printed output, do something with it etc. With that you have a very direct and immersive pathway to re-structure your code and to understand all the bits and pieces that each step does. - You should absolutely check out structural editing and what you can do with it. Since Clojure is a Lisp it has very fluent navigation and it is very easy to extract functions, let bindings and move things around in a structured fashion. - With tap> and tools like portal you get even more insights into your code that you can display and navigate. If you like console.log in the browser (which is very powerful!), you'll love__ this. When I do need to refactor, which I very often do, I much rather have a tool like Clojure.

👀 2
phill13:03:06

I too sometimes got the feeling that someone had removed the arms from my chair. One gets over it. But as regards refactoring, it turns out that basic idiomatic Clojure innately supports refactoring because code is pure-functional and data is pure-data, and persistent to boot... not objects. When you refactor and replace code/function/module/subsystem A with B, you begin with the given that neither A nor B can break anything, nor produce any effects besides their return value. You can more easily tell what's going on (what it's fashionable to call "reasoning about" your code). There is a big mechanical benefit too: you can make a Clojure program's replacement for A execute both A and B and compare their results with = or show you the differences with data.diff until you're satisfied. Bottom line: nothing is perfect, but it's simpler to need no solution to a non-problem, than to have a half-baked solution to a problem of mad chaos.

☝️ 4
dvingo15:03:27

I think there are large number of clojure developers who wish there was some form of static typing in clojure (myself included). e.g. https://app.slack.com/client/T03RZGPFR/C8NUSGWG6/thread/C053AK3F9-1677971622.356619 After working on two large commercial clojure codebases the biggest problems I've experienced are that because almost every argument to every function is a hashmap you cannot understand a piece of clojure code in isolation - you have to understand large parts of the surrounding system it operates within to understand the type/shape of the parameters. In some instances you just give up and may never know the true set of all possible parameters. The dual issue to this is that on a team of devs with varying years of experience you have no way to enforce contracts in your code anywhere nearly as strongly as you could with something like typescript or haskell. Wherein more experienced devs could design/architect the types of the system and then have those be used by the team to ensure some safety and reliability guarantees. What I've concluded from this is that like all tools clojure comes with a set of tradeoffs and it has a sweet spot for exploratory/data analysis coding and prototyping, potentially useful as a production tool for small teams of highly skilled people. If you are planning to scale your team and codebase I would stick with typescript or something similar. Said another way, technology is a social endeavor and the people using a piece of technology and their culture must be taken into consideration when choosing a tool. For problems where I choose clojure, the way I deal with the lack of a compiler-enforced type system is by using malli and its inline function schema syntax to approximate a type system https://github.com/metosin/malli/blob/master/docs/function-schemas.md#function-inline-schemas I do think there's a lot of innovation and leverage to be gained by a static type system being added to clojure and while I am perplexed by the lack of attention and effort paid to typed clojure by the core team, I think it makes sense when seen from the point of view that clojure was created to solve particular design problems by its creator and a static type system is not perceived to be in that solution space.

kennytilton18:03:17

I started on Common Lisp thirty years ago, a couple of 100kloc apps, and Clojure ten years ago. I refactor as a way of coding life. I see types as an obstacle to productivity and especially refactoring, since I have to fully transform the source before trying a change. Type systems often do not think I know what I am doing, and I have to jump through hoops to make them accept perfectly good code. I do appreciate compilers catching mistakes, and all the things Clojure does catch at run time, but static typing goes too far for too little gain. I recently built a BPA system entirely in pl/sql, which is maddeningly strongly static typed. It is true: by the time I got it to compile, it was correct. Not worth it if we already know how to code well. But programming is intense. It involves the right side of the brain as well as the left, and the amygdalla , and deep reptilian carryovers. It is not a rational activity, by the time we are done. So mileage varies on these things. And consider this. I hate coding, so I strive for table-driven design. That means I write 1/N lines of code that runs N times more often. It gets exercised out the wazoo, and in short order is something for which I do not need artificial reassurance. The BPA system was a perfect example, but I still had to fight pl/SQL. Clojure, OTOH, is all over data-driven. Something like that.

Frederic Latour23:03:59

@U01EFUL1A8M On 1. I'm not sure what you mean by libraries that allow you to infer typs in a non-data driven fashion. Can you clarify ? I am mostly using [https://gcanti.github.io/fp-ts/] those days. You can use [https://gcanti.github.io/io-ts/] to to validate and decode data into valid type. [https://github.com/colinhacks/zod] is a popular solution that is not specific to fp-ts. Why would one have to do things twice considering you can get the typescript type from the "schema" ?

const user = z.object({
  username: z.string().optional(),
})
type C = z.infer<typeof user> // { username?: string | undefined };
On 4. You are right. It would be great if typescript types were being used for additional optimization. That being said, javascript has very good performance (though not really capable of taking advangate of multiple cores). I looked (superficially) at the different tools you mentioned. It seems there are many different approaches to compensate in some way for the lack of types. It's a bit overwhelming: spec, spec-tools, malli, clj-kondo, typed-clojure, typed-malli, type hints, etc. Malli (a tool also mentioned by @U051V5LLP) looks very interesting. I have to admit that I like this notation a la Haskell: What does the :cat stands for ?

Frederic Latour09:03:51

@U0PUGPSFR It's always interesting to debate/discuss about types because we often get many different reasoning approaches (even opposite sometimes) from a wide range of people who are obviously knowledgeable developers/thinkers. A couple of remarks though. A type-hint type system (which is what typescript is) can be as flexible as you like. Not sure this is true for a type system that is part of the language. Having to completely transform the source when refactoring with types on the way seems somewhat hyperbolic to me. After all, you already have some unit/integration tests in place and you certainly can't change everything all over the place without revisiting them. As far as typescript is concerned, it seems quite easy to me to do some refactoring/experimentation:

// optional fields make it easy to experiment
type Profile = {name: string, age: int, phone?: string}

// starting from an existing type
type testProfile = Omit<Profile, 'age'> & {birthdate: Date} 
	| Profile

// can't be more flexible than that
type flexibleProfile = any
PL/SQL is really one of a kind. Its procedural nature and Its tight integration on top of the database makes it very specific. It may not be the best example for evaluating the merits of a type system. For instance, it's easy to hate types when having to deal with a deep java/c# class hierarchy. However, it has more to do with inheritance, tight-coupling, verbose class definitions and an inflexible type system. You are right, mileages may vary, even though I think programming is a complex rational activity (doesn't mean it does not involve intuition and creativity) that does not always end in a rational solution :) That being said, in the end, we are all striving to write quality code (easy to read, to refactor) in the shortest possible time. After all, a sophisticated and flexible optional type-hint system on top of a dynamic language (and backed up by the core team) may represent the best of both worlds, the solution for being productive in a fast-pace changing world. Python is now completely embracing type hinting in latest versions (PHP is going that route as well if I'm not mistaken).

dgb2310:03:47

@U04S6P8TL78 "cat" in malli is conceptually the same as "cat" in spec: https://clojuredocs.org/clojure.spec.alpha/cat You have a sequence (for example a parameter list) that you want to tag with keys if the predicate for each key matches. Say you have an argument list like so: ["Hello" 42] and you want to specify that s is a string and n is an integer for a parameter list like so: [s n] In spec: (s/cat :s string? :n int?) In malli: [:cat :s string? :n int?] The sequence can be thought of as a tuple that you want to tag for further processing! Note that it doesn't matter how you name the tags (here the keywords :s and :n . Their relationship to the sequence is positional.

👍 2
kennytilton02:03:08

This is good, @U04S6P8TL78. I feel we are close to settling the strong vs dynamic typing question. But first, a POI: how much Lisp coding have you done? I am feeling bad that I did not simply say, "Give it a month."

seancorfield03:03:19

I think the workflow with Lisps - very interactive, working within a live, running program - may also play into the mindset in Clojure that types aren't as important as in many other languages.

Frederic Latour07:03:55

@U0PUGPSFR Touché 🙂 I'm just participating to the discussion as a veteran and transposing some of my experience. In particular, I witnessed the "I don't need your types" era of Javascript when first versions of typescript were released. However, it doesn't mean that this experience is applicable here. In Summary, I get from this discussion: • Some people are not only fine without types but consider this to be a strength (balance Pro/Cons) • There are however some people in the community that would like a type system. One of the reason I was asking the question initially, is that I was wondering if Clojure had some kind of magic trick that was making the "type" not even a thing in the community. • You are not completely on your own (as Clojure dev). There are several tools that makes it possible to shape your data in a meaningful way without the constraints of types. • There are some experimentation with type-hinting but that's not mainstream. • There is a tool like malli that is trying to bring in a coherent way both specs and type hinting (my understanding may not be completely accurate here) When I have the chance to get more direct experience with Clojure, I will not hesitate to participate to this kind of discussion and provide my own experience. > A word of caution: I'm not a native English speaker as you have certainly guessed. Even if I try to pay attention to it, sometimes my answers or expressions may come as too strong for someone mastering the subtleties of the language. This is unintentional.

👍 4
seancorfield19:03:54

Malli is like Clojure Spec (and Prismatic/Plumatic Schema) -- they provide additional runtime checking of data structures and function arguments/results. Not a type system, but a runtime specification system. In some ways it's more powerful than a type system (but it isn't static analysis at compile time). Editors can have a Clojure integration that provides static analysis to some degree, e.g., LSP with clj-kondo, and that tends to be augmented by what the REPL provides (via evaluation of code that gets "metadata" about code). There is also Typed Clojure which is a static analysis system for "type checking" source code with annotations -- it can't do much with raw Clojure source -- but it is more of an experimental/academic project at this point.

Frederic Latour19:03:14

@U04V70XH6 Thanks for clarifying. I was indeed thinking that this kind of syntax was checked at compile time / edit time (LSP).

;; int -> int
[:=> [:cat :int] :int]

dgb2322:03:59

To add how it works specifically with clj-kondo: https://github.com/metosin/malli#clj-kondo It is an extensible, data driven linter that does edit time checking of many things. Malli can integrate with it to generate linting rules. Custom linting extenstions are also often used for macros, because they typically introduce their own syntax. It's not a static type system obviously. But many of the practical affordances that one would give you, you get on demand, if/when you want it in Clojure.

👍 2