def foo(bar, baz):
bar(baz)
...
What the heck is 'bar' and 'baz'? I deduce no more than 'bar' can be called with a single 'baz'. I can't use my editor/IDE to "go to definition" of bar/baz to figure out what is going on because everything is dynamically determined at runtime, and even grep -ri '\(foo\|bar\|baz\)' --include \*.py
Won't tell me much about foo/bar/baz, it will only start a hound dog on a long and windy scent trail.You just make it way harder for people to understand your code and contribute to it.
Though in my experience, sane code structure and informative comments trump everything else when it comes to understanding big and unknown codebase. I still shudder when I think about working years ago on various Java codebases (mostly business IT systems). What a convoluted mess of n-levels deep interface hierarchies. Types? Yeah, but good luck unraveling what exactly is happening in the runtime.
I agree that on balance type signatures are better -- and that's why modern Python has evolved to incorporate them. But they aren't a magic cure-all, and do they impose a significant tax of their own.
Python is easy to grok, and if you have programmers writing code like bar(foo,baz) then the problem is not Python. You can write crap in any language.
Unit tests do much of what typing checks anyway ... and here's the thing ... you NEED unit tests no matter what. No typing system can tell you that you wrote > when you should have written <.
(defn advisories
[config]
{:pre [
(map? config)
(:download-advisories-dir config)
]
:post [
(map? %)
]
}
(let [
dir (:download-advisories-dir config)
]
;; more code here> The claim is simple: in a static type system, you must declare the shape of data ahead of time, but in a dynamic type system, the type can be, well, dynamic! It sounds self-evident, so much so that Rich Hickey has practically built a speaking career upon its emotional appeal. The only problem is it isn’t true.
Is this somehow supposed to relevant to the posted article or did you just want to start a tangentially related dynamic-vs-static flame war here in the comments section?
While you still can make a static language that is confusing, it's a lot harder... I challenge you to write a function signature in Rust that is both:
1) Useful
2) As opaque as the python signature above.
> You find automatically inferred types
A minority of static languages do type inference in function signatures. I think it's a bad idea for exactly the same reason the python code is bad. On the other hand, every dynamic language allows you to omit any information about a type signature.
Static vs dynamic makes for such difference in the detailed workflow, both in terms of changing existing code & in terms of writing new/(more) from scratch code, yet they can both be quite fruitful, and can both be abused in absurdum.
It seems like people naturally fall into one of the two camps (either by personality or by training), and the other side just seems kind of insane: "how can you even work that way!?". Then culture and idioms emerge over time and strengthen the tribalism.
I've gone back and forth between the two over the course of my career, and it's quite a mind-shift when switching, with a fair bit of pain involved ("but it would be so easy to do this in [old language]", or "what the hell is this garbage anyway!?") and then eventually it settles in and it's not all painful, all the time ;)
(Going back and forth between Scala and Python right now, so this hit a bit of a nerve)
Oh yes, they do. Even inferred, the types are there and pretty easy to locate, even if you're not using an IDE.
And traits! "Oh look, this functionality is implemented in a trait implemented by a trait implemented by a trait implemented by what you're looking at. Maybe"
const foo = JSON.parse(arbitraryJsonString);
and not having to worry about the structure up front.That's not power, that's a shotgun aimed at your crotch whose trigger is connected to a cosmic ray detector.
The API then becomes a runtime fallible one, which is perfectly sound.
let foo: serde_json::Value = serde_json::from_str(arbitraryJsonString)?;
There, just as powerful [1]. But you know what's even more powerful? After you've done your dynamic checks, you can do this on the entire JSON tree, or on a subtree: let bar: MyStaticType = serde_json::from_value(foo)?;
and you get a fully parsed instance of a static type, with all the guarantees and performance benefits that entails.[1] Value represents a JSON tree: https://docs.serde.rs/serde_json/enum.Value.html
Now you get type checking on JSON at compile time :)
As soon as you try to do anything useful to foo it's not arbitrary anymore. You have to make some kind of an assumption on the underlying type, check for keys, nulls, maybe it's a number (the right number?), maybe it's a list. So now you have to scatter some boilerplate checks everywhere you touch a part of foo.
If you could parse it into a typed structure up front, you'd only have to deal with this in one spot, and have guarantees for everything else that follows.
Bonus: if your typed language has good support for records, you can even do this in a way that only provides structure to the parts you care about, and is robust to changes to any other parts of the json.
eval('alert("hello, ' + userInput.name + '!")')So there's no docstring? And the actual variables are that random and indecipherable?
Sounds like the problem is that you're tasked with looking at code written by someone who is either inexperienced or fundamentally careless. When dealing with reasonably maintained codebases, this kind of situation would seem pretty rare. In modern python we now have type hints of course, which have helped quite a lot.
In static languages this simply isn't a problem. Types are checked for consistency at compile time and you don't have to rely on people toiling on this busy work.
Not to say documentation isn't necessary, or good, but isn't something you need to create working programs because otherwise no one knows wtf any variable is without running the program.
I had to laugh, hard.
If your Python program uses any library whatsoever, chances are that library won't have types, so you can't really use them.
Even super widely used libraries like numpy don't have good support for types, much less any library that consumes numpy for obvious reasons.
Function names and doc comments describe behavior, not argument and return types.
What I like about Rust is that it even checks code in your 'docstring'. So it is easier to keep it maintained.
I use "Find Usages" on foo to see where it is used. Once you see where it is used, you know what the types can be. It's not great, but it's also something that can be progressively remedied.
In the event that the function is not as trivial as your example suggests, the author should have written a docstring to help you understand what it is trying to do, in addition to type annotations that will make it more readable.
In this example, bar can either be a function, a class, or any object with __call__(), so the type information is less important in this case, than actual docstrings that express intent.
I just gave up and introduced globals and used as flags for some places in the code.
To make things even more fun, big parts of the system was written in 2.7 called from runtime generated bat files from 3.4 as remnants of a rewrite and the consultant that had his funding cut.
But also, there’s nothing stopping the code from being much clearer about its intention than this weirdly contrived example (I have a lot of code and most the function names are pretty unique). And surely you want to search for ‘foo(‘ to find invocations.
Most of the problems that cause non-trivial bugs come from invariant violations. At point A, there's some assumption, and way over there at point B, that assumption is violated. That's an invariant violation.
Type systems prevent some invariant violations. Because that works, there are ongoing attempts to extend type systems to prevent still more invariant violations. That creates another layer of confusing abstraction. Some invariants are not well represented as types, and trying makes for a bad fit. What you're really trying to do is to substitute manual specification of attributes for global analysis.
The Rust borrow checker is an invariant enforcer. It explicitly does automatic global analysis, and reports explicitly that what's going on at point B is inconsistent with what point A needs. This is real progress in programming language design, and is Rust's main contribution.
That's the direction to go. Other things might be dealt with by global analysis, Deadlock detection is a good example. If P is locked before Q on one path, P must be locked before Q on all paths. There must be no path that leads to P being locked twice. That sort of thing. Rust has a related problem with borrows of reference counted items, which are checked at run time and work a lot like locks. Those potentially have a double-borrow problem related to program flow. I've heard that someone is working on that for Rust.
> ...
> The Rust borrow checker is an invariant enforcer. [...] This is real progress in programming language design, and is Rust's main contribution.
I'm so confused by your stance here. You essentially say "type systems are not useful" and then "oh but this most recent advance in type systems — that one is useful." Do you find type systems useful or not?
There are a lot of properties we can analyze statically, and practically all of them essentially amount to extensions of type systems. Any of them increases our ability to rule out undesirable programs from every beginning execution. Some of them have unintuitive syntax, but many of them are no more syntactically burdensome than most other type systems. This is especially true if you consider how far we've come with type inference, so we no longer have to write code with the verbosity of Java just to get some meager guarantees. It's still a very active area of research, but we're clearly making progress in useful ways (which you even highlight), so I don't really know what point it is you've set out to make.
> It explicitly does automatic global analysis
They appear to think that the borrow checker isn't achieved with type theory, but with some other technique ("global analysis").
Although, to be fair, my understanding of making a practical affine type checker is that things get kind of wonky if you do it purely logically. So practically you do some data flow analysis. Which is, I believe, what rust is doing. This also explains why MIR was such a big deal for certain issues with borrow checking. They ended up with a format that was easier to run a data flow analysis on, and that allowed the borrow checker to handle things like non-lexical lifetimes, etc.
[I've only read about such things. So I might have mis-remembered some of the details. However, this is my take on why someone might not call rust's advances purely type theoretic (even if they can be handwaved as type theory at a high level).]
Not the original author but it seems like they're saying that type-systems are non-specific invariant enforcers and so have costs without necessarily having benefits whereas a user-specifiable invariant enforcer is more guaranteed to have the benefits.
Two examples from the top of my head:
1. Encoding matrix sizes into the data- and function-types, so that you can safely have a function `mat[c,b] mat_mult(mat[a,b] a, mat[c,d] b)` or even `mat[w-2,h-2] convolve(mat[w,h] input, mat[3,3] kernel)` and have the compiler check that you never use a matrix of the wrong size.
2. Actually checking the correctness of your implementation.
There is a very nice online demo of Liquid Haskell [1], where they defined the properties of ordered lists (each element has to be smaller or equal to the one before, line 119). Then they define a function that takes an (unordered) list and spits out a ordered one by applying a simple quicksort.
Now, if you break the algorithm (e.g. flip the < in line 193) and run Check, the compiler will tell you that you messed up your sorting algorithm.
Pretty neat.
edit: I just realized that LiquidHaskell is almost 10 years old. Sad to see that basically nothing made it into "production".
And yet type theory is an excellent way to express all kinds of invariants. The more rich the type system the more you can express. If you get to dependent types you essentially have all of mathematics at your disposal. This is the basis of some of the most advance proof automation available.
What is super cool is that proofs are programs. You can write your programs and use the same language to prove theorems about them.
This is still fairly advanced stuff for most programming activities but the languages have been steadily getting better and the automation faster and I think the worlds will eventually collide in some area of industrial programming. We're already seeing it happen in security, privacy, and networking.
I don't think type systems suffer from complexity. They merely reveal the inherent complexity. You can use languages that hide it from you but you pay a price: errors only become obvious at run time when the program fails. For small programs that's easy enough to tolerate but for large ones? Ones where certain properties cannot fail? Not so much in my experience.
update: clarified wording of "proofs are programs"
Why is this interesting? You pay an extremely heavy price in terms of language complexity. In practise, you almost never have the invarants at all or correct when you begin programming, and your programs evolve very rapidly. Since with dependent types you loose type-inference, you now what to evolve two programs rather than one. Moreover proofs are non-compositional: you make a tiny change somewhere and you might have to change all proofs. In addition we don't have full dependent types for any advanced programming language features, we have them only for pure functions that terminate.
> the same language to prove theorems about them
That sounds like a disadvantage. Ultimately verification is about comparing two 'implementations' against each other (in a very general sense of implementations where logical specs and tests also count as implementations). And the more similar the two implementations, the more likely you are to make the same mistake in both. After all, your specification is just as likely to be buggy as your implementation code.
> type systems suffer from complexity.
This is clearly false for just about any reasonable notion of complexity. For a start pretty much as soon as you go beyond let-polymorphism in terms of typing system expressivity, you looks type-inference. Even type-checking can easily become undecidable when the ambient typing system is too expressive.
There is no free lunch.
The only difference is: instead of brittle hierarchies, we get ossified compositions (depending on how much nominal vs structural typing happens).
We, of course, agree that we are quite some distance from having advanced type systems brought to day-to-day industrial programming.
Didn't Kurt Gödel and Alan Turing do some work on proving statements within a system?
A success story in this regard is the async keyword. Very quickly you can get used to it and it feels like any other imperative programming.
In C# if I can add assertions and have C# compile time check the source that the assertion will not be violated. This would be great. I know they do this for null checking.
Ah yes. And then you end up writing entire prgrams in types. So the next logical setep would be to start unit- and integration tests for these types, and then invent types for those types to more easily check them...
> you essentially have all of mathematics at your disposal.
Most of the stuff we do has nothing to do with mathematics.
And yet Go is adding generics in 1.8. And I'm sure its type system in another 5 years will be much more expressive than 1.8's. The community has long been saying that the minimal type system isn't enough.
Isn't it stil mostly Java and C++? That's what I hear all the time here.
Also, I'm not sure what point you're trying to make. You start by saying that fascination with types systems is not useful in practice, and end with an example where it is useful (Rust). While Go can stick a GC to avoid most of the issues that Rust is trying to solve, it stil has to ship with a defer mechanism (no linear/affine types/RAII) and a data race detector.
I'm out of touch, but I would expect that there is a lot more Go code by now, and it also didn't catch up with C++ or Java.
Go's type system is much weaker and less expressive than either Java's or C++'s. C++ in particular has parametric polymorphism, type constructors, and dependent types. Go has none of those.
Which is exactly what a type error is!
> The Rust borrow checker is an invariant enforcer. It explicitly does automatic global analysis, and reports explicitly that what's going on at point B is inconsistent with what point A needs. This is real progress in programming language design, and is Rust's main contribution.
> That's the direction to go.
The borrow checker is an ad-hoc informally specified implementation of half of an affine type system. Having to switch programming languages every time you want to add a new invariant is a poor paradigm. What we need is a generic framework that allows you to express invariants that are relevant to your program - but again, that's exactly what a type system is.
Rust has done a great thing in showing that this is possible, but linear Haskell or Idris - where borrow checking is not an ad-hoc language feature that works by gnarly compiler internals, but just a normal part of the everyday type system that you can use and customize like any other library feature - are the approach that represents a viable future for software engineering.
In principle you could implement a form of borrow check with linear types (I don’t think affine is good enough), but the ergonomics would be horrible.
I think typescript with gradual and structural typing and similar like mypy or sorbet are making real difference.
Type systems provide multiple benefits, performance, self-documentation, better tooling and more explicit data model.
Rust has automatic memory management.
> or even explicit multithreading.
You don't need explicit multithreading to run into data races. Languages that allow any kind of unchecked mutable state sharing and allow any form of concurrency (explicit or hidden) are prone to that problem.
Even single-threaded programs with aliasing of mutable variables are hard to reason about and Rust improves on that considerably by not allowing accidental aliasing.
You mean, one of the companies with the largest number of developers on the world, paying one of the highest average salaries for them is able to use the language?
That means absolutely nothing.
At least so far. Rust might change that in the future.
For example, perhaps my `calculate_price` function only depends on 2 attributes of the order which has 65 attributes. Am I creating a 2-element data type for that function to process? no! I'm specifying that it processes an Order data type, with all its 65 elements. But implicitly then I'm saying the function has 65 input parameters of all these specific types and nobody can call it now without providing them all. What a pain! Huge amount of extra code, refactoring, unit testing, because of this.
So either you end up with a cambrian explosion of micro-types or you have these way overspecified interfaces everywhere.
Compare with dynamic languages (or structural typing, Go etc) that only care that things "quack like a duck". The calculate_price function doesn't care what object you give it, as long as it has the two attributes it needs. Now I can unit test `calculate_price` with a 2-element object rather than needlessly creating the 23 irrelevant required elements of a valid Order.
I think a lot could be solved with culture shift. Where data types are really known and locked in, use the crap out of them. As soon as things get ambiguous or flexible, go right ahead and specify that your function takes a Map<String,Object>. If a useful concrete interface emerges at some point factor it out then. The problem is that this is really frowned upon in a lot of places.
Go is a statically typed language. Unless you're referring to interfaces at run time?
> I'm saying the function has 65 input parameters of all these specific types and nobody can call it now without providing them all. What a pain! Huge amount of extra code, refactoring, unit testing, because of this.
I've seen this "cambrian explosion of micro-types" argument before on HN but I think it comes from a misunderstanding about what you actually do in a static language. No one's creating types for every combination of parameters.
Either you'd pass the two arguments directly (`a, b: int` or whatnot), or you'd pass the Order type and just use the bits you need.
If you have multiple Order types, you'd use generics or something like it to get duck typing. If you used a field that wasn't there, you'd get a compile error.
The reality is the code would look pretty much the same in both static or dynamic languages.
Dear god, please don't. Some of the worst spaghetti code I have disentangled used this pattern. Typos in the string literals used as keys, object type mismatches, etc.
If you only want some of the fields from another struct, you have a few options
* Define another struct `FooArgs`. This is easy, and I (respectfully) reject the claim that nobody does it.
* Just define your function to take those two fields directly.
[0] https://insights.stackoverflow.com/survey/2021#technology-mo...
The issue with this is it makes it difficult to understand code. If anything works potentially anywhere then the flip side is you have no idea what works anywhere without running the code.
Running code is slower iteration cycles than a type checker. Also you don’t actually know what it returned. So it was able to produce an output, was it what you expected? Or was it subtly different in ways that will break your code downstream.
I work at a company with a large untyped code base and the product is constantly breaking in these ways.
Python can do the same using what they call Protocol. Here protocols need to be defined upfront.
This is usually called Structural typing, as opposed to nominal typing where classes inherit from a base.
Currently working my way through some complex Python code written in that style and it's completely impossible to understand it. In fact, the only way I can actually do it is transforming all these ad hoc data structures into proper types so I can make sense of it.
As for how you know what’s in there - you should only know whether what’s relevant to your function is in there and not care about the rest of the world. For the former, tools like clojure.spec are helpful but ultimately good design helps the most (something that typed languages can often obscure).
How?
> Sometimes, software engineers find their languages too primitive to express their ideas even in dynamic code. But they do not give up . . .
Is this a failure of the language, or a failure of the engineer?
> If we make our languages fully dynamic, we will win biformity and inconsistency,[^] but will imminently lose the pleasure of compile-time validation and will end up debugging our programs at mid-nights . . . One possible solution I have seen is dependent types. With dependent types, we can parameterise types not only with other types but with values, too.
Types are a productive abstraction/model in programming languages. One of many. Each has its strengths and weaknesses; each is appropriate in some circumstances and not in others. Types are not the solution to all problems, any more than currying or OOP or whatever else is.
Production languages (like prolog or make) don’t need an if statement or operator as selection is implicit when a production matches.
It seems that fixing this is a research problem, which would lead to the holy grail of programming languages, i.e. an ultimate language that is as expressive as Idris and as efficient as Rust, and is thus essentially perfect.
Programming languages are not theoretical things. They're concrete, practical tools that _enable_ other stuff. Engineering, not science.
>Programming languages are not theoretical things. They're concrete, practical tools that _enable_ other stuff. Engineering, not science.
You can't escape theory, engineering is applied science.
Depends on what you mean. Idris's notion of multiplicities essentially subsumes Rust's borrowing (there's some differences with affine vs linear types), so I can't think off the top of my head of things that you can ensure with Rust that you can't with Idris, but Rust has a lot more quality of life improvements that make things less clunky (also having a GC, Idris can get away with a lot less need for borrowing in the first place).
Are both Idris's expressiveness and Rust's efficiency (given stronger guarantees) perfect? Aren't theese languages really complex both to learn and to write? There are poblems without a solution, perfect and unique to all of them.
We can get a more direct Idris implementation by inlining the parser (toFmt) into the interpreter (PrintfType). That lets us throw away `Fmt`, `toFmt`, etc. to just get:
PrintfType : (fmt : List Char) -> Type
PrintfType ('*' :: xs) = ({ty : Type} -> Show ty => (obj : ty) -> PrintfType xs)
PrintfType ( x :: xs) = PrintfType xs
PrintfType [] = String
printf : (fmt : String) -> PrintfType (unpack fmt)
printf fmt = printfAux (unpack fmt) [] where
printfAux : (fmt : List Char) -> List Char -> PrintfType fmt
printfAux ('*' :: fmt) acc = \obj => printfAux fmt (acc ++ unpack (show obj))
printfAux ( c :: fmt) acc = printfAux fmt (acc ++ [c])
printfAux [] acc = pack acc f : Char -> Type
f '0' = Int
f _ = Char
g : (c : Char) -> (f c)
g '0' = 0
g c = cprintf is what you call when you want to print X in hexadecimal with at least two digits, left justified on an eight-character wide field. I don't see how the sanity of whatever string representation the programming language uses is relevant here.
In theory this is true. If the compiler is decent, compile times and analysis shouldn't really be affected. Maybe libraries will use X but otherwise they would use a manual implementation of X anyways.
But in practice developers misuse features, so adding a feature actually leads to worse code. It also creates a higher learning curve, since you have to decide whether to use a new feature or just re-implement it via old features. See: C++ and over-engineered Haskell. So each feature has a "learnability cost", and only add features which are useful enough to outweigh the cost.
But most features actually are useful, at least for particular types of programs. It's much harder to write an asynchronous program without some form of async; it's much harder to write a program like a video game without objects. This may be controversial, but I really don't like Go and Elm (very simple languages) because I feel like have to write so much boilerplate vs. other languages where I could just use an advanced feature. And this boilerplate isn't just hard to create, it's hard to maintain because small changes require rewriting a lot of code.
So ultimately language designers need to balance number of features with expressiveness: the goal is to use as few simple but powerful features to make your language simple but really expressive. And different languages for different people. Personally I like working with Java and Kotlin and Swift (the middle languages in the author's meme) because I can establish coding conventions and stick to them, C++ and Haskell are too complicated and it's harder to figure out and stick to the "ideal" conventions.
All features are useful. That's table stakes. But usefulness is insufficient to warrant inclusion. How does a feature interact with all existing features? Are there ambiguities? Are there conflicts? A language is not a grab-bag of capabilities, it's a single cohesive thing that requires design and thought.
Is that really a problem on the language's side, though? Devs are capable of mis-using any feature, even extremely basic ones that almost every language has (variable names, for instance (although I'm laughing in FORTH)). Code standards and code reviews are necessary tools in the first place because it doesn't matter what language you give a programmer - they're perfectly capable of constructing a monstrosity in it.
I argue that preventing programmers from doing dumb things with well-designed language features (so, hygenic Scheme macros, and not raw C pointers) is a social and/or organizational problem, and it's better to solve that at that level than to try to solve it (inadequately) at a technical level.
("I keep dereferencing null pointers", on the other hand, is an example of a technical problem that can be solved on the technical level with better language design)
Yes, for a language to be good in practice you need to look at what developers actually do and not how a perfectly rational developer would use the language.
I have found the opposite to be true. Missing features often leads to what one would call "design patterns". When the language adds official support to solve the problem you're trying to solve with that pattern, the code becomes clearer.
But dynamically typed languages produce at least the same amount of accidental complexity, just in different ways.
Complexity in the types happens when the type system isn't expressive enough. Or when you're trying to do something that would make the compiler try to solve the halting problem.
To that last point, this is why the PLT community has pushed in the direction that Agda / Idris has. Kind of like how we realized years (decades?) ago that we didn't need pointer arithmetic, there's been a realization that "total" isn't actually that helpful, and it's okay if we didn't have languages that could express the halting problem.
Maybe we should judge compile-time constraint systems by how easy it is for the library author to add good error messages for misuse?
who's "we" here? pointer arithmetic is useful for all kinds of things.
Rather "I believe there is a silver bullet, but I don't know where yet". Probably I am too naive!
I guess the author just hasn't encountered Nim before, where anything becomes compile time by just assigning to a const, and macros have access to the real AST without substitution. Macros also allow compile time type inspection, as they are a first class citizen rather than tacked on.
The compile time print, AFAICT, already exists in Nim as the `&` macro in strformat. That lets you interpolate what you like at compile time, and supports run time values too.
I think the message is more nuanced than that (otherwise wouldn't lisp with its homoiconicity and compile time macros fit the bill perfectly?). Idris uses the same language, but is still too complex. And Zig not general purpose enough. I don't want to put words in the author's mouth but I think the implication is that this is a large space to explore and we don't have a solution yet; there's nothing like "just make your language like this and it'll be good." They're just pointing out the problem they see, and some (non-)solutions to it.
I thought it was more nuanced too as they were explaining how integer types can be derived, until I finished the article, and they really did just seem to be complaining that there's a mismatch between compile time and run time.
Dynamic types don't really solve the problems they mention as far as I can tell either (perhaps I am misunderstanding), they just don't provide any guarantees at all and so "work" in the loosest sense.
> otherwise wouldn't lisp with its homoiconicity and compile time macros fit the bill perfectly?
That's a good point, I do wonder why they didn't mention Lisp at all.
> we don't have a solution yet
What they want to do with print can, as far as I can see, be implemented in Nim easily in a standard, imperative form, without any declarative shenanigans. Indeed, it is implemented as part of the stdlib here: https://github.com/nim-lang/Nim/blob/ce44cf03cc4a78741c423b2...
Of course, that implementation is more complex than the one in the article because it handles a lot more (e.g., formatting and so on).
At the end of the day, it's really a capability mismatch at the language level and the author even states this:
> Programming languages ought to be rethought.
I'd argue that Nim has been 'rethought' specifically to address the issues they mention. The language was built with extension in mind, and whilst the author states that macros are a bad thing, I get the impression this is because most languages implement them as tacked on substitution mechanisms (C/C++/Rust/D), and/or are declarative rather than "simple" imperative processes. IMHO, most people want to write general code for compile time work (like Zig), not learn a new sub-language. The author states this as well.
Nim has a VM for running the language at compile time so you can do whatever you want, including the recursive type decomposition (this lib isn't implementing Peano arithmetic but multiprecision stack based bignums): https://github.com/status-im/nim-stint and specifically here: https://github.com/status-im/nim-stint/blob/ddfa6c608a6c2a84...
func zero*[bits: static[int]](T: typedesc[Stuint[bits] or Stint[bits]]): T {.inline.} =
## Returns the zero of the input type
discard
func one*[bits: static[int]](T: typedesc[Stuint[bits]]): T {.inline.} =
## Returns the one of the input type
result.data = one(type result.data)
It also has 'real' macros that aren't substitutions but work on the core AST directly, can inspect types at compile time, and is a system language but also high level. It seems to solve their problems, but of course, they simply might not have used or even heard of it.> Yes, you can write virtually any software in Zig, but should you? My experience in maintaining high-level code in Rust and C99 says NO.
Maybe gain some experience with Zig in order to draw this conclusion about Zig?
This was indeed weird to read, given that only Zig (and soon the JVM) solves this problem, and is well known for the fact. Especially when language design and type theory are an area of interest.
But hey, silver lining: Zig still kind of came out on top.
But anyway, thanks for your work on Zig. Your metaprogramming concepts were heavily influential for some of the ideas in my own language, Empirical.
It's amazing what you can do when you have compiler transformations and targets always available.
Suddenly, "little DSLs" (MLIR dialects) don't seem so bad, since they are defined the same way and map in semantically-sound ways to lower-level dialects. You can have dedicated dialects, like Halide, for doing something as concrete as image processing kernels.
Oh, and you can output those kernels to both the CPU and GPU, including automatically introducing async functions, host-side sync barriers, etc. Good luck doing that automatically with a general purpose programming language and a combination of macros, AST manipulations, and derived types! You really need a compiler to stay sane.
> "Programming languages ought to be rethought."
Indeed.
Also, most of our code needs to support suspend/resume on another machine, either in the middle of an action or more often between actions. So, a "behavior" might begin on machine A and then migrate to machine B to do more work, then on to machine C. While doing work, its execution state might be serialized to Postgres while some dependency is waited on—say, a human task that doesn't get done until the following Monday. It's then resumed in the same execution state, potentially on an entirely different worker/machine, and continues executing.
The suspend/resume stuff completely destroys the code if you're writing it by hand, as does moving from machine to machine.
So we write the core logic in our own internal MLIR dialect and then output code that has the suspend/resume semantics automatically (i.e. literal compiler transformations, plus our own "interpreter" (which is just JavaScript/v8 with all of the extra suspend/resume cruft added in).
We don't translate out of SSA form at all, our codegen can execute it directly. We also insert debug hooks so when there's an error, you can map the execution state to the original code.
Most of the cool machine learning stuff MLIR can do, we're not even doing yet outside of some internal prototypes. So far, just the methodology of MLIR has made a huge impact—it gives really nice structure (read: tooling) for the kinds of code transforms we've needed to do.
HTH
Also, the main argument is that separating features into those used at compile-time (AKA static) and run-time (AKA dynamic) is necessarily creating separate languages (i.e. a "static language", which may involve types, macros, preprocessors, etc.; and a "dynamic language", which may involve memory allocation, branching, I/O, etc.)
https://github.com/fsharp/fslang-suggestions/issues/243#issu...
https://old.reddit.com/r/ProgrammingLanguages/comments/placo...
F# designer Don Syme is making the "biformity" argument, e.g. needing a debugger for compile time as well as runtime.
and
Syme & Matsakis: F# in the Static v. Dynamic divide https://old.reddit.com/r/ProgrammingLanguages/comments/rpcm6...
I still think something an application language with something like Zig's comptime would fill a big niche. (As opposed to a systems language.)
> In this "1ML", functions, functors, and even type constructors are one and the same construct; likewise, no distinction is made between structures, records, or tuples. Or viewed the other way round, everything is just ("a mode of use of") modules. Yet, 1ML does not require dependent types, and its type structure is expressible in terms of plain System Fω, in a minor variation of our F-ing modules approach.
> An alternative view is that 1ML is a user-friendly surface syntax for System Fω that allows combining term and type abstraction in a more compositional manner than the bare calculus.
On the other hand, from the "engineer" point of view, all abstractions melting into one may not be desirable. It's nice to be able to use weak abstractions for simple stuff and powerful abstractions for more powerful stuff. Being exposed to the full complexity of your language all the time sounds like a recipe for disaster.
We do or we don't. There is no "might". Spending money on "might" has been the death of many projects.
If we didn't, and now we do, we could write a fn to map the car to parts, or we could define the car struct in terms of its parts, or we could just do away with the car altogether.
But far more valuable would be an analysis of what changed about the requirements that the model no longer works.
Now, don't get me wrong: I'd love a better language, and by better I mean "as fast as assembly but 'dynamic'". The problem is that, at the end of the day, all compilers are just "premature optimizations" or perhaps "willing premature optimizations". We could all be happily programming in smalltalk or build a runtime using predicate logic, but a) the number of people who could program in it is vanishingly small and b) it would be fucking slow. These languages don't solve a problem that I have, or rather they don't solve a problem that I don't already have a far better solution for. They solve a problem that academics have.
For example in Typescript, I use tsc-macro to run "*.macro.ts", they can import any functions and modules just like normal source code. And their evaluated result are saved as "*.ts"
The generated ts are then compiled alone with other hand-written typical source files into js for deployment and execution.
Hoist the "Final Words" section to the top and make it a "tldr" introduction, that way your reader can begin with a high level understanding of your argument, which you can hone and refine as you progress.