See also Jeaye's post from yesterday...
I guess Clojure / JVM is looking more and more attractive, unless you want to rewrite to Rust? ;)
Under the recent https://www.youtube.com/watch?v=P5eO7BNyJao talk there's a comment that says: > typescript (structural typing) or trait based languages (swift, rust) with easily extensible conformance are better option than this I barely know the first and don't know the other two, so I'm curious - if anyone's here well-versed in one of those, how would you compare what they have to what will land in Clojure 1.13, as we know it now?
What we'll have is only at call site and at run time. The structure information does not propagate in either direction.
The lack of propagation is probably a necessary outcome of it being at run time only. But it feels like a not so big of a deal - I wouldn't expect for it to be different, anyway.
It's a question of when you'll catch the error, "compile" time or run time. Would be interesting to see if kondo can propagate this information and make use of it statically
Yeah, I get that. But is it really all there is to the "better option than this" bit?
Typescript has anonymous structural typing, so in a function arg, you can define the type with
const func = (x: {customer: {name: string, id: number}}) => {
console.log(x.customer.name, x.customer.id)
}
and get a compile time error if your callers don't provide a type that matches the required type. It also has type aliases so you can reference these things less anonymously. I think by "better" the commentor means "more familiar".With true REPL languages like Clojure, I've been realizing we've muddied the waters by contrasting compile-time and run-time.
Given that development-time is supposedly the only moment code changes are made, one could argue that if checking is done at development-time (thanks to a REPL or tests), it's equivalent to checking done at compile-time (given sufficient coverage from REPL or test cases).
Of course, in the destructuring example, this checking only happens if we develop in the REPL and/or at test-time (which is part of development-time) if there is coverage.
Ed's answer is much more useful to the OP question. π
in localized situations (unit tests, repl sessions), it's very easy to make sure callers are conforming to the spec (as determined by :keys!). this is lessened when the call stacks are quite deep, and maps are updated in many locations, which is when structural typing or traits really shine
the issues of Maybe Not still exist, but i've always felt Rich overstated them
> which is when structural typing or traits really shine I kinda feel like the ability to reify and name the destructuring functions will help with that in the same way type aliases do. You can always refer to a single implementation when you need to share it.
the changes in 1.13 are going to raise the floor of clojure dramatically, and especially paired with spec or malli, you'll get a ton of assurance in a way that's been challenging before. but it still won't meet the bar of "you are guaranteed that every single caller has this shape, and there are no possible scenarios where something slips past"
@ben.sless I think the new elixir type checker has been working on something similar with propagating type information from pattern matches.
Can anyone here tell whether (Clojure-)LSP can be made to capture the structure and accommodate (validation of) propagation across a call chain? Along the lines of what @ben.sless mentioned for kondo.
Though invocation may be dynamic, so not always possible to verify.
you said you're not familiar with the languages, phimik, so i'll state it a little plainly. typescript's type system is unsound so you can miss stuff if someone writes "foo as any" or "foo as unknown as Bar", but you can look for those with a linter or runtime checks. this can mimic :keys!. rust and other ml-inspired languages can actually guarantee a baseline safety for what shape a given object will be, and like :keys! yelling at you in the repl, the compiler will tell you when you've made a mistake or gotten an assumption wrong, but that happens at compile time
as i understand it, rust's traits are kind of like clojure protocols, where you can implement a trait for a struct, but then you can type a function parameter to an object that implements that trait instead of a concrete type
Yeah, that bit of TS I do know. I also know that it enables and maybe even induces programs that are 80% types, where you spend most of your time solving a type puzzle that's also very much TS-specific. But maybe it's mostly due to how people are taught in TS, no clue - just something I noticed almost every time I have to work with TS. So kinda hard to judge from the sidelines what parts are "better" than the new destructuring bits. :)
> Typescript has anonymous structural typing, so in a function arg, you can define the type with > >
const func = (x: {customer: {name: string, id: number}}) => {
> console.log(x.customer.name, x.customer.id)
> }
>
> and get a compile time error if your callers don't provide a type that matches the required type. It also has type aliases so you can reference these things less anonymously. I think by "better" the commentor means "more familiar".
>
Can you express nested structures here like you can with :keys! ?that code example has nested structures
Yeah, but x is the outer structure. Can you get to the id without an explicit x.customer.id?
> Can you express nested structures here like you can with :keys! ? Yes, that is a nested example. > Can you get to the id without an explicit x.customer.id? Yes, it also supports destructuring like:
const func = ({{name, id}: customer}: {customer: {name: string, id: number}}) => {
console.log({ name, id})
}oh, yes, but it looks silly in this example. i'm on my phone or i'd type it up for you
it's not as powerful as the dsl being introduced in clojure, but it's "a type system" and therefore "better". I think that's sort of line of reasoning that Rich is trying to head off with that talk
> rust's traits are kind of like clojure protocols I'm not sure I totally agree with that. I think at an implementation level, they're really similar, but Clojure protocols are switch statements that you extend from the outside, but Rust traits are a common behaviour that types of things can have. It's an intentional mindset difference, right?
"common behavior" is up to each implementer, kind of like protocols, as i understand it. i think with protocols you can extend a type you don't know with a protocol you don't own, but in rust you have to own at least one of them
but yeah there's a ton of differences lol. but idk if a thread is the right place for inserting a chapter from The Rust Book π
i'd highly recommend experimenting with rust to anyone who hasn't used it, it's very neat. you'll probably come out liking clojure more lol but you might learn something too
Yeah, Rust was what I had hoped Go would be, back when I learned that. I didn't like Go, but I liked Rust.
I stopped at "I didn't like Go".
One thing to keep in mind is that we don't build type systems in Clojure but data shapes and schemas. That's a fundamental difference and I personally don't want to go back to type systems.
It would seem to me that the specific distinctions depends on whom you ask. :) Not in the know enough to guarantee it, but I'd think that proper type theorists would tell that shapes and schemas are also types.
Shape and Schema type, as in Shape.class and Schema.class?
The main distinction for me is, that I don't build micro languages, as you often do with types. I care about the shape of my data and the data has to be validated and conformed, especially if it comes from untrustworthy sources. But being able to treat it as plain data and not as formal types inside my code is an enabler for me.
What do you mean by that? What do you do with plain data that a type system would prevent you from doing?
lol uh oh
In nominative type systems like Java, you often create a different class for each context you want to talk about each entity in. Like HttpCustomer, PdfInvoiceCustomer, DbCustomer, etc. Whereas plain data lets you talk about each of those things anonymously in each content. I think that the people pushing structural typing (like the TS example above) are trying to argue that these anonymous types that can be compared by a type system are a way to get away from the proliferation of context specific classes.
A type system does not prevent stuff (in the definition of the type, of course), but it often makes things more cumbersome. And I'm with Rich, that it's not worth it most of the time, especially in information systems. I love the fact, that maps are open, for example. I don't care if they contain more stuff than needed in the current context, most of the time. It's like the difference between an RPC API and a document oriented API.
> I stopped at "I didn't like Go". @lsolbach Perhaps a better characterization is "I was disappointed in Go". I've designed programming languages and built compilers, interpreters, and runtime systems, so I have "opinions" about programming languages π That's why I'm here and why I love Clojure so much!
(I've also learned and used dozens of programming languages over the past 45+ years in IT π )
static is really the difference here. the cost of compile time safety in agentic feedback loops is real. so the split Rich provides between mechanistic and live/situated checks matters. the cost of dynamic mistakes only caught at run time is real, too. the tradeoffs matter, good to see this anchored back. static guarantees become less and less valuable the more your program gets away from inputs and interactions you fully control. remember clojure is built for βsituatedβ systems, so it makes tradeoffs re: what you actually get over the wire being the source of truth, not some static snapshot of a spec someone made about what you get over the wire.
I saw a team of 20 devs waste months of effort on typescript mismatches in private types in microservices. this was βfixedβ by then generating all the request/response surface types from a common json schema source. Which then blocked several teams b/c the types specified re: inputs & outputs actually didnβt give them everything they needed to do the work their microservice was supposed to do, etc. many things went wrong along the way, and the types could easily have been the basis for fast feedback & reconciliation. maybe an issue that βcircumventing typesβ was against the rules for the teamβs process, so you couldnβt necessarily use language constructs for type flexibility to handle the mismatches.
Exactly like something that I had to observe more than once, only it was actually with static types and not a JSON schema. :D Untold amount of hours wasted. But maybe they held the types wrong - hard to tell when everything is already FUBAR.
no it was static types and json schema both! b/c typescript is so cool
but anyway letβs just say itβs a special level of hell when youβre literally not able to compile a working program b/c of how your organization mandates (1) how you use static types & (2) what the source of truth for those static types is.
The lesson I learned working in Clojure for a long time is that sometimes compile time safety is a luxury π in TS, you can always fall back to dynamic, which gets you as good as Clojure. But in a lot of other places, you can get a lot of compile time safety for not a lot of effort. Similar to tests. Thatβs one of the reasons that I would use it again.
In the end, bytes in memory are dynamic π But for all the talk regarding fast feedback - something telling you you got it wrong before you run it is faster than running it. hopefully.
but that something might as well be clj-kondo instead of the compiler.
shakes the Borkdude tree
Michiel, come on - where's our type inference with 100% of coverage, including dynamic call sites? (And while you're at it, could you please add a check for whether the program of interest halts at some point?)
I myself don't need it, but I could see it might be desirable to be able to def a destructuring form to some symbol and then provide that symbol in places where a destructuring form is allowed. And also to be able to compose those things.
The new (in alpha 9) selector macro might be what you want there @michaeljweaver? It takes a destructuring form and returns a function that destructures its single map input and produces a map of :select/`:missing`/`:excess`/`:all` data as indicated in the form.
I don't think it quite does that.
(defn my-func [<destruct-form> input] ... )
I might want to provide destruct-form with a symbol or name, something I can define once and then use in more than one place, and not a literal destructuring form every time. I don't think I can provide a function there.Oh, no, that can't work: it's syntax, for read time, no runtime values.
And I maybe also want to do
(defn my-func [(deep-merge <destruct1> <destruct2>) input] ...)For read time, so what reader tags are available? This can be Rich's next project...
(btw, it's merge-deep, not deep-merge)
That's right, Rich explicitly said they chose a name to avoid collisions. I haven't used any of this stuff yet so I have no muscle memory. But I'm excited to find places where I can use it.
> something telling you you got it wrong before you run it is faster than running it. hopefully. lotta hope there
And we all know hope is the first step on the road to disappointment
@bkamphaus may I just interject and say that is a sick keytar!
@lsolbach never played or read any 40k material? That IS a motivational aphorism
What is 40k material? Never heard of it.
Warhammer 40k. Tabletop wargame with lots of rich surrounding material. Source of the quote I used there.
Nope, I have the computer game, but never really tried it. I'm the SupCom type.
WH40k is a motherlode when it comes to depressing quotes. > Happiness is a delusion of the weak.
you can always fall back to dynamic, which gets you as good as Clojure> @lilactown how can we fall back to dynamic in TS? using
any ?use "any", use "as any", use "as unknown as foo", etc
TypeScript is statically typing dynamic world really well.
Better than rest of the statically typed languages at least.
... at which point you can't trust anything that's near those waived types.
I think there is a trend across all programming languages that they move towards more guarantees, more typing, more ceremony. At first people say, it is just optional, but slowly it creeps into style guides and becomes de facto practice. Dynamic languages initially become popular because of their lean coding experience and then they evolve into professional languages by giving up what made them popular.
I find Clojure one of the most professional languages, if not the most professional language, I've ever used. Clojure is the language, where I get things done. I'm saying this as a 15+ year 2nd chair of the Java User Group Stuttgart.
I was going to say, I think it's weird to imply dynamic languages are somehow "not professional"...
I rarely ever have type/shape problems in Clojure, and if so, I can just look at the data and see what is wrong. Maybe in a big team with less experienced developers, type safety buys some safety, but the complexity of at least OO type systems like those of C++ and Java add so much (accidental) complexity. They're not worth the hassle, IMHO.
There are professional languages and then there are professional languages. :D Clojure doesn't even have a standard!! There's no committee!!
Design by Committee has a good reputation.
π
@seancorfield there is certain snobbery among users of statically typed languages where they consider dynamic languages I serious, toys, scripting languages, poorly designed, etc I've seen it. They consider Clojure an offensive parasite on Java.
As someone who spent eight years on the ANSI C++ Standards Committee, I am glad Clojure has no committee π€£
Some of them are even in this slack, I could tag them but I don't want to put them on the spot. But their main gripe is not lack of standardized, that one mystefies me.
I'm reminded of the sketch where the guy goes into a fancy restaurant and orders a steak "well done" and the waiter says "perhaps sir would prefer the chicken?" π
something telling you you got it wrong before you run it is faster than running it. hopefully.This is the intuition that a lot of people have. But I think it can go the other way (at least for me). I am an empiricist, not a rationalist, deep, deep in my bones. When the compiler tells me "oops,
:foo is not guaranteed to be present at this call site," I have to do a lot of deductive reasoning to try to understand how and why, with no example in front of me.
When the runtime check fails, I can reason inductively. I can use context clues! I have an example of the thing that is missing :foo. Usually it is immediately obvious when I look at it where it comes from and then I can go there and see why :foo is missing.
For this reason I find it much more natural to investigate and resolve a runtime failure than a compile-time type error. Yes, this means you need reasonable tests. (You need them anyway! but not trying to die on that hill right now.)Static typing is good to avoid stupid bugs. But what about the hard bugs?
Depends on how you define those. And those aren't necessarily disjoint sets.
I'll try. Definition: Stupid mistakes are those mistakes that occur due to accidentially applying insufficient cognitive resources.