Looking for opinions on what you think are the actual issues with languages. Ive heard toolchains and documentation are a pretty big part. If say, there was a language like rust + go + scala, with an exceptional package manager and documentation, would you feel compelled to try it out?
Python, but with static typing and compiled to native code.
I love Python's syntax. It's usage of whitespace forces people to have some semblance of proper formatting. Overall, I find Python code extremely easy to read.
But my god is it slow, and I've always assumed a lot of its slowness is due to being interpreted and the duck typing.
I've worked with Python for years, and Go (golang) turned out to be everything I wanted Python to be.
Admittedly, Go doesn't have enforced whitespace, but it has standardized code format, enforced by the `gofmt` tool, so in 99% of cases it looks as good as Python. The syntax is somewhat similar - there is a lot of type inference and duck typing, yet the type system is still static (which removes a majority of type errors), with opt-in dynamic typing through `interface{}` (the empty interface, which is implicitly implemented by every type) and type switches.
Go is compiled to native code, and can be statically linked, making it perfect for containerization, since you don't need any dependencies whatsoever - just copy the executable into a scratch container and be done with it.
It's also very fast, and incorporates an async-by-default runtime which is scalable to an arbitrary number of CPU cores out of the box. Imagine asyncio, but multithreaded, and without the async/await keyword littering - that's Go runtime.
My favorite feature of all - the `select` keyword, which waits for a fixed set of channels (similar to queues, but lightweight), and receives/sends a value from/to the first channel that has another goroutine (similar to thread, but lightweight) sending/receiving a value to/from it. It's a very useful feature for building event-driven systems.
Sorry for the sales pitch. I just really like Go :)
> I've always assumed a lot of its slowness is due to being interpreted and the duck typing
Being interpreted definitely, even though there are some rarely-used methods that can improve that. Additionally, there are more than one implementations of Python, and pypy is much faster than the standard CPython.
As for duck typing, why would that make a language slower exactly?
> As for duck typing, why would that make a language slower exactly?
I assumed it would result in basically every operation to require multiple lookups to figure out how types would interact at runtime. Like a simple "foo.bar(biz + baz)" would have to check the types of biz and baz, figure out how to add them, then look up the foo object and make sure it has a function named baz, and then call it.
I assumed all these lookups and verifications would add significant overhead.
‘Growing a Language’ and the ‘Are We There Yet’ presentations changed the way I look at languages, so right now I would say my “perfect” high-level PL would be a Java-like language (possibly running on the JVM due to ecosystem-reuse and the killer GCs and performance) where immutable “objects”/values are the first-class citizens, but method implementations can use local, mutable semantics. I would also very much add a ‘pure’ keyword marking side-effectless functions/methods.
It sort of sounds like Scala though, but I would probably settle for a weaker type system but with constraints (like JML or Clojure’s spec) that are tried to be proven at compile time but will fall-back to be checked at runtime in debug mode (and if marked so, even in prod). Also, would probably make Clojure’s atom/ref/etc concurrency primitives the “most first-class”, so other than the possibly mutable method implementations, everything would be safe+ concurrency-wise.
It is pretty much a mix between Scala and Clojure, or a non-lisp clojure with some types, so not really original..
Oh, and Haskell’s target typing for number literals, I honestly don’t understand why don’t we use that everywhere.
Guy Steele was in the office next to mine at Oracle. We had a lot of long chats about language design, and I've tried to keep him up to date on the work we've done with Ecstasy including
* Java-like language
* immutable “objects”/values are the first-class citizens
* method implementations can use local, mutable semantics (including mutable captures)
Well, it would have been probably more clear to say I wouldn’t go all-in on the type system and if something would have to get a tradeoff I’m okay with the type system being weakened for that.
I’m quite okay with e.g. Haskell’s type system up to a point (including monads), but some even more exotic feature and borderline dependent types are not something I would put into the language (I will let other languages be the testing grounds for these ideas!), as I think that most interesting properties will not be provable either way and that a constrain-based approach may scale better.
Some middle way between F#/Kotlin and Rust. Sum types, generics etc are completely non-negotiable. Something where I can use a GC for 90% and build regions of high perf or low level when needed. I want to say “in this region I want zero allocations” and the compiler will warn if an iterator or closure or something ergonomic-but-expensive allocates in that region. This is where CLR/Jvm lands often fail. I even want to be able to get rid of virtual dispatch and all pointer chasing but only for select regions. In some high level languages I can’t tell whether an abstraction comes at a cost.
In the bulk of the code I want the syntax to be as terse as possible so the business logic is as clear as possible even if it comes at a perf cost. This is where Rust often fails. The logic of something where I don’t care about perf is obscured by token salad for Box’es and similar.
To me what matters the most isn't the language but the ecosystem:
- versatility: create desktop apps, mobile apps, websites, CLI
- quality of IDE / tooling
- lots of libraries to deal with all sort of corner problems
- lots of knowledge I can google
- if used professionally: lots of talents available
For the language itself, I only really care about one thing: statically typed, just because it eliminates all sort of silly bugs and makes refactoring and navigating the code so much easier. But I am not dealing with hard computer science problems.
But the most important thing is muscle memory: not having to even think of how to solve something, just do it. We can probably adapt to any major language, but you aren't productive until you develop muscle memory. That's why I am not looking to switch to any shiny new thing unless I have a really compelling reason to do so.
- This is not yet possible, but a good optimiser which can turn an abstract representation of the code into optimal code for a given hardware (CPU or GPU). Currently we are far from this, there is probably a 10x improvement that current compilers can't achieve. See for example a FizzBuzz implementation achieving ~10x throughput improvement after months of optimisation work:
An actual functional lang server that doesnt randomly die on you and tries its best to infer your intent would be nice.
I like that last point too, as copilot I think is going in the right direction with "step 1" of full optimisation. Maybe step 2 would be some AI based optimiser scheme to transform IR into optimal cpu/gpu ISA. Though maybe stability would be a problem
Syntax like F# but with the ability to get performance like C++. Linear types seem to offer some hope of being able to have manual / non GC memory management in a functional programming language but I haven't really seen this done in a mature functional language.
To really get good performance you probably need the ability to drop down and write more procedural style code in hot code paths and expose it with a functional interface. The details of what exactly that would look like in a way that integrated nicely would be important. I'd also want to be able to easily move code to the GPU or incrementally vectorize it.
I'd also like to see the one language / runtime support everything from C++ constexpr style compile time programming through native compilation, JIT runtime and an interpreted mode for scripting with it being easy to interoperate between levels and reuse the same code across contexts.
A good ecosystem of tools and libraries with a sane package manager and build system would also be important. F# is decent there but could still use improvement.
I'd also like to see less impedance mismatch between serialization and runtime data structures and with database / relational data. Hoon is pretty interesting in how there is no real distinction between runtime and serialized data in many contexts but its harder to see how to do something like that and meet the other goals above.
Much of the above is driven by the needs of games and VR. I'm interested to see what comes out of Simon Peyton Jones working at Epic as it seems it may lead to something along these lines.
Have you tried Rust? It checks a great deal of your boxes, don't be misled by the reputation of a "systems programming language"!
I made a video explaining why rust is different here, if you're interested https://youtu.be/4YU_r70yGjQ
I haven't yet, I've read a bit about it and it's on my list to try and learn it properly at some point but I haven't yet prioritized it as being pretty experienced with C++ (and liking it more than a lot of people seem to) I haven't seen a really compelling reason to pick Rust over C++ for my use cases.
F# is the closest I've found. A pro/con of F# is the .NET ecosystem which has many C#-isms which both accelerate library development and detract from functional styles.
- functional
- type inference (but not so much to slow down compilation)
- not too heavy on type abstraction
- multi-threaded
- local heaps for actors
- cross-platform
I'm also disappointed that Pony doesn't have as wide adoption as Rust or Zig.
I would want something like C++/Typescript/C#/Python, but only the good parts.
- Multiparadigm, that means traditional OOP, but also first class functions (well first class everything)
- Compiles to native code, runs relatively fast
- Designed from the ground up for async/await
- No undefined behavior
- Statically typed, but with the feeling that the compiler is helping and not punishing you. I get that feeling from Typescript, Python+MyPy, C++ on a good day, Kotlin.
I would also love to experiment with a few ideas of mine:
- Voluntary checked exceptions (You mark a block specifically and it is a syntax error if an exception could possibly leak that block).
- First class observables for easy GUI binding
- You can lift the action of a piece of code into a variable. Then you can reason about this action. For example, you would write imperative looking code to modify a document, and it would generate objects for the "command pattern" so you'd have undo functionality. Or you could lift a longer running action and have it running on a thread and cancellable. I imagine this to be closures on steroids.
Why async/await? As go and soon java with loom shows it is quite a misfeature, if anything.
Otherwise, I like your idea on checked exceptions, I think they are a very good feature, but somehow got a bad name and thus not experimented with as often.
Your last point sounds quite macro-ish, so I guess the LISP-fanatics will come in in droves and tell you that they already have that in their language :D But relatedly, transactions sound similar in a way.
I compare async/await with manual threading, or with callback hell, and in both cases it is a great improvement. I haven't tried Java with Loom and haven't done much in Go, so maybe that style is indeed better. Right now it feels spooky if my lightweight thread can suspend at any time, deep down in some function call. And what if you accidentially don't call a "loomified" IO function, but a plain blocking one? I like the explicitness of "cooperative" await but agree it can be a bit tedious. Maybe there is a better way of integrating "asyncness" in the type system?
Yeah the last idea came when I added undo/redo to an app. I had to go mechanically through everything and make every change to my model go through a "Command" subclass. When I encounter something like this I think "why can't the compiler do this for me?". Probably solvable by LISP macros, or transactions as you said.
Loom also comes with something called structured concurrency and many other improvements to make taming lightweight threads easier.
Nonetheless, Java is a very “pure” ecosystem in that basically 99.9% of the ecosystem is written in Java itself — so besides the tiny amount of FFI, every method you might call is ready to be magically unblocking.
Nim is pretty cool, and I always look for a way to use it. Started a couple of side projects in Nim and it is a great balance of close to metal like C and expressive like Python, but with a proper type system.
My main criticism is that it feels like a large language with all kinds of weird features and you can write very clever and dangerous code. A tiny bit Go-like restraint and it would be perfect.
I personally hate lisp syntax, maybe im just no used to it or something but the (brackets (and (stuff))) feels a bit weird. Is there a reason why you want lisp syntax specifically?
TypeScript, but the only supported module system is ESM (including extension being required like it is in the browser), there is no interface and enum (types and type unions are used instead), there are nameof and typeof operators in the language and there is syntax for getting a runtime value representing a type (for example string union into a string array) or even straight up option/syntax to selectively (up until absolutely) enforce type shapes at runtime.
Naming: linters deal with much more than formatting, and users tend to create linters even for languages which enforce canonical formatting.
Linters check for the patterns which are allowed by the compiler, but disallowed by a particular project/team/org. Example: disallow functions longer than four statements, disallow repeated strings that could be replaced by a constant, etc.
You don't solve the halting problem. You don't introduce it in the first place by making a Turing complete language.
Datalog is terminating. Programming languages don't have to allow infinite loops and recursion. There are plenty of ways to ensure recursion is terminating, like structural recursion or only allowing recursing finite data structures.
Can you give an example of why you need non-terminating semantics? Programs can still be run "forever", if they are run for each input while maintaining terminating semantics for deriving output. Abstractly the Turing machine is infinite, but in the real world input and output is almost always finite and discrete.
I guess you wouldn't be able to use it to build a machine that is supposed to display Pi or Fibinocci?
Here's a CPU that controls an automobile engine. Inputs are the position of the throttle, and the rotational position of the crankshaft. Outputs are instructions to the fuel injector and the timing of spark plugs firing.
You can terminate that program when the user turns the engine off.
Or, you can say the program should consist of "read the stored state, re-configure the fuel injectors, fire a spark plug if one is supposed to fire now, update the stored state, and terminate". You could say that it should be written that way. But when you do, you've got a bunch of cynical old embedded software people saying, "Explain to me how that is better in any way? Does it make the program easier to write? No, it doesn't. Does it make it less error prone? More reliable? No and no. Does it make it use fewer resources? Also no. So why in the world do we want to write it that way?"
The diamond dependency problem is easy to avoid: Only allow namespaced/qualified includes, no shared libraries. There is no need for a package manager if the runtime does the work of resolving all includes, and these references have some kind of cryptographic integrity.
This can be completely transparent to the user. There's no need to have a separate program do dependency resolution when dependencies are referenced in source code. Instead we have the complete waste of life that is package manifests and shared libraries.
To be clear the lack of “shared” dependencies does not necessitate code duplication. By shared dependencies I’m talking about languages (Haskell for example) where only one version of a library can be used, which results in the diamond dependency problem (predictably so!).
Let’s say file A references file B and C, and files B and C both reference D. D doesn’t need to be duplicated.
If all includes are qualified/namespaced there is no diamond dependency problem, and the compiler/runtime can reuse the same code for multiple references to the same file.
> Let’s say file A references file B and C, and files B and C both reference D. D doesn’t need to be duplicated.
Yes, if an ecosystem does not attempt to ensure that D is at a single version, which is both B-compatible and C-compatible, it moves a mountain of complexity onto A.
It might not be apparent for a small program that only uses standard library and never experiences diamond dependencies at all. But the complexity here is that
1. A might get a D1 object/structure/result from B, and later
2. A might get a D2 object/structure/result from C, and then
3. Some code may be needed to ensure D1 compatibility with D2, if they interact. This problem is better to be resolved in an ecosystem than in A.
Without loops or general recursion, how would you implement binary search of a list? How would you implement list sorting algorithms like merge sort, bubble sort, or quicksort?
Databases are useful. Look at Datalog and SQL and you're looking at a language that does very useful stuff without user-defined types, higher-order functions, recursion, etc.
No higher order functions means that you cannot make a function that takes another function as input or returns a function as output. I would prefer a programming language to be simple, only allowing functions which transform finite input to finite output.
No modules / namespaced includes means that to include other code, you would write something like `include as Foo './other-source-code.file'` or `include as Bar 'github.com/foo/bar/some/source.file@commit`. All the names in the included file (functions, names of data, etc.) would be referenced by prefixing the namespace, like `Foo.baz` or `Bar.baz`. Combine that with the ability to include code via URL, and there would be need for a package manager or separate package manifest. Wherever you want to use the new version/code you simply update the include statement at the top of the file.
I've actually been having a lot of fun building stuff using OpenAI Codex. I suppose something where I could write the specs and the AI spits out something resembling HTML/JS/CSS so that I can verify that I communicated correctly.
- create a dark mode palette for the page based on this image: {file path}
- make a form with a single name (not first and last), email, country code, phone
-- this form should allow a document upload when clicking
-- allow drag and drap to upload documents
-- add a tick box that says "I agree with terms and conditions"
- this form should have input validation
-- all fields should be secure from injection
-- name can be anything except number
-- file type is documents (not images but including PDF)
-- absolutely ban exe files
- upon valid input, send this form to {URL}
- if the response from {URL} is valid, redirect to {success page}
-- else show the error message if it's type 200
I'm afraid that a language that was an agglutination of features of those three languages would be horrible to work with. Omitting features that aren't pulling their weight is key in making a good language. My instinct is to prefer a language with a minimal and orthogonal feature set. Tastes differ and some languages have a good reputation despite putting in all the bells and whistles (C# goes against all my minimalist impulses in language design, yet I find it very pleasant to work with).
Minimalist languages face their own challenges, the Lua mailing list is besieged by people who love the minimalism of it and only want to put in just one little feature that would make it perfect for them. If there wasn't a strong filter, it would have hundreds of those little features and not be so minimal anymore.
Anyway, tooling is great, but what I want to see most of all is simplicity, and not simplicity in the sense of Go, with masochistic manual error handling and all that imperative boilerplate. I'd like simplicity like Scheme or SML, omitting what isn't needed or can be relegated to a library. With great tooling, of course.
So, what I most of all want is for SML to become popular. Which isn't going to happen. So all I can do is to argue for replacing C++ with Rust at work (there's not much resistance actually, more than finding resources to do the actual work, which is problematic).
Anyway, I have fooled around with making my own language, but I don't think it's a good idea anymore (but damn would it be good if there was a modern SASL, think Scheme with ML syntax[0]). Because you correctly identified that it's tooling that is the decider these days. So if you want to make a difference, find an underappreciated language that is close enough to your ideal (there will be several, unless you have really wild ideas) and put in work on tooling for it instead.
Have you looked at F# at all? OCaml like syntax in the .NET ecosystem with great tooling and good interoperability with C#. It's pretty easy to work in a C# /.NET environment and introduce F# for some parts of the codebase.
> I believe that the purpose of life is, at least in part, to be happy. Based on this belief, Ruby is designed to make programming not only easy but also fun.
Writing Ruby code feels natural, from creating classes, all the way to its basic types. Knowing everything is an Object makes it easy to work with.
The tooling is amazing as well, from managing gems through bundler, and running simple scripts such as rake tasks.
Now, if it's performance can be further improved, say like Go-like speeds then it would be further used.
The one that would allow a domain specialist to skip all the dev/build-ops and wheel-research into a project structure and jump straight into a business logic. Akin to what excel is to users, ignoring its clumsy parts in this analogy.
To name a few details:
Rich but plain standard library with convenience features. E.g. if you have to read json file, you just readJsonFile(filename), not sys.fs.readFile(fn, “a”, {encoding:”utf-8”}).pipe(new JsonReader({streaming:true}).inPipe()).collect(pipeUtils.buildArray()). If you have a date, it should be easy to modify, format or extract any of its parts, and it must be a built-in standard type. If you work with http, it must be result = http.postJson(data), easily findable in a reference. If you want to just communicate with another instance, it’s result = myInstance.<method>(<args>), not http.
Good data extraction, transferring and restructuring features, e.g. var data1 = obj.{name, id, contacts[].value}; obj.{a, b} = data2.
Development environment is the same as runtime, no explicit build-deploy phase. See a bug or require a new feature? Click “Menu - Edit module” and edit it right there with live data and live tests. When you save, your temporary db instance/transaction/backup/whatever is applied to the main branch, after it gets its own backup.
I have much more bullet points than time rn, so I’ll stop here.
This is not exactly a “language”, but that would be perfect.
A lot of langs have a std lib namespace (c++, rust for example) where you could just do `use std::fs::*` and write `read_to_string(path)`. Even better if those convienience functions were placed in `std::prelude` so that it would be immediately known to your project namespace.
I do like the idea of a better dev ops system. Maybe with better github workflow integration or a custom git frontend? Then everything would be coupled together a bit tighter with the advantage of an ergonomic, uniform interface to do your devops
- No undefined behavior. All behavior is defined even if the ramifications of that behavior are not. Use after free? The data in the memory location is written-to or read-from, not omitted by compiler, and programmer is responsible for the second- and third-order effects thereof. Signed integer overflow? You get the assembly ADD instruction for your platform/data width and see what happens.
- Fixed width integers. int8_t == short short int. int16_t == short int. int32_t == int. int64_t == long int. int128_t == long long int.
- char is by default always unsigned.
- byte type similar to unsigned char but requires explicit cast.
- Anonymous functions well-supported.
- A method keyword inside a struct defines a function pointer to an anonymous function with the signature and definition that follows.
- New operator --> such that x-->y(...) is equivalent to y(x, ...) (perhaps with additional error checks)
- Arrays no longer degrade to pointers but to fat pointers with an aligned prefix sds-style. Argument syntax for an array/fat pointer is arg[..]
- char type used alone is deprecated
- string type is an array/fat pointer of type char[..]
- str* functions deprecated; now that unique identifiers can use more than the first six characters, use string* functions instead
- "C" locale defined as using UTF-8 normalized to NFC
- Conforming change: allowable main() signatures include int main(string argv[..])
Well, let's baseline with Zig. Because that adds build dependency management. Building code has got to work like GO: it needs to be built into the compiler. Let's also add formal methods that's bound up with the implementation language even if it starts 1-way from FM to code. Correctness in any of its several dimensions would be better with pragmatic FM together with the code itself.
Zig, which deals with C/C++ code, has the ultimately eco-system. That's a big plus.
As a non professional programmer the thing that i don't like about zig is it tries to make you do the right thing.
I like playing around with the side effects of things, abusing pointers etc. It isn't safe, it isn't sensible but I'm not aiming to do safe sensible things.
A bunch of them already show up in languages like rust. Generics are the more ergnomic impl of HKTs and with ownership after you pass an entity it off to another context, you cant use it again, which I guess isnt exactly linear per se
Interesting. Given the comments from some others [0] when I asked about HKTs in Rust, they said something quite the opposite from you. Curious to hear your thoughts on those comments.
Since I'm not doing lowlevel stuff, for me it would be a statically typed language that has a typesystem so powerful that it never gets in my way and lets me express all constraints in types that I need for my domain (so it must be a value-dependent typesystem or similiar). In addition, the syntax must be terse so that it's easy to define (business) datatypes and mappings easily, so that someone can come in and read those definitions quickly and understand the domain.
And then one feature that I like which few languages support are some kind of contexts. Scala does this really well for example, but even in Scala it could be improved.
What I mean is that code that looks the same can mean different things in different contexts. The typesystem then ensures that it cannot be used wrongly though.
An example would be how "5 ± 3" can mean different things depending on the context, such as "1 or 8" or "mean of 5 with variance of 3" (in statistics).
Without such a feature it's hard to translate thoughts into code as easy in certain domains.
Standard ML, but unfrozen and most of the "successorML" proposals integrated into the standard. And it has somehow sprung a large healthy library ecosystem. And the MLton toolchain has gained a REPL.
A C++ dialect that dropped exceptions, headers and backwards compatibility (especially backwards compatibility with plain C). Everything that can be is statically linked, so no fussing about ABI. Explicitly no support for dynamically linking the std lib, or passing std lib objects across ABI barriers. One big build in a compiler that runs as a daemon, no messing about with recompiling the world in each translation unit. One implementation that works the same everywhere because it's the same code. No committee, no standard. The implementation is the standard and there can be only one.
Have thought about this a lot, I really want it. I even started messing around with implementing it, but it's just a toy (an abandoned toy, even) for now.
TypeScript without the BC shit (and a native JIT compiler). Basically I wish for a TypeScript that’s maintained like Swift where the primary goal of the latest version of the language is for it to be the best possible language ever.
I want to write a program that has no mention of concurrency and runs efficiently on clusters anyway. So no threads, fibres, processes, actors, atomic, mutex, channels, queues, coroutines. Not in the language nor in a library.
Push the concurrency and distribution machinery into the language runtime. Developer can neither see nor influence it. Program just runs faster when put on more parallel hardware.
The price appears to be dropping mutation, severely limiting I/O and coming up with a domain independent scheduler for the runtime. So it's expensive but not impossible.
> Push the concurrency and distribution machinery into the language runtime. Developer can neither see nor influence it.
This is completely unworkable. It's based on the assumption that nodes have zero failure rate and networks have zero latency and packet loss and infinite bandwidth.
Distributed system programming is hard because you want to control the failure modes of your system as closely as possible. The opposite of hiding it.
It's based on the assumption that node and network failures can be detected (with a performance cost) and unobservably healed around.
It's also assuming that the runtime can make a good enough guess at when the ratio of data size to compute time for a task makes distribution profitable.
Distributed systems programming is indeed hard. That's why I want it pushed out of my applications.
> It's based on the assumption that node and network failures can be detected (with a performance cost) and unobservably healed around.
...which is false, especially on large scale systems. Unmanaged cascading failures are the kind of thing that caused big downtimes in various FAANGs many times.
> Distributed systems programming is indeed hard. That's why I want it pushed out of my applications.
Companies hire good software and system engineers to do this stuff.
NodeJS sounds pretty close to that. Just ‘await’ anything in parallel and your code appears synchronous. Parallel code would need to be a new node process, so it’s not quite so amazing, but pretty close.
Lots of things claim to be implicitly parallel where they mean 'annotate the parallel parts yourself'. Parallel for, await, callbacks, futures and so forth. I don't want any of that annotation.
Wasn't that the way OOP was supposed to work, back before people who didn't get it got their hands on it ? Isolated objects, messaging only, no notion of where the object resided. Just add idempotency guarantees and implicit parallelism for loops, and off you go. I can't think of any language that's actually like this though, so maybe it's harder than it looks.
C++ without the old bad things it carries for the sake of backwards compatibility.
Maybe a simpler subset of the language with it's most common usage, like STL containers, string, etc.
And a similar syntax, I don't want want exotic things like in rust.
By the way I wonder why it's not possible to deprecate things in C++, and compile different part of a code base with either an old or a new part of the language. C++ really needs to break off backward compatibility.
TypeScript free from the historical baggage of JavaScript. With some runtime type checking, autogenerated typeguards from types, allowing things like checked de/serialization.
A TypeScript where (for example) string methods make sense, with all the number types and not just floats, with only null or only undefined and a lot of other things I can't think of right now.
Maybe add some pattern matching on top.
TypeScript's type system is so nice and ergonomic to use, it's a shame the basic objects of JavaScript (Object & co) are so "fickle" to work with.
To answer OP's question, I don't think there could be a "perfect" programming language.
This would be nice. I'd also like more traits/protocols/interfaces. I think deno is going in this direction but there is a lot more to do to get there.
Similar features of C probably, but with many differences (and probably different syntax too). Specifically, I would want to have such things as:
- Good documentation.
- Always twos complement. (For one thing I would have -fwrapv being the default setting.)
- Reduce some undefined behaviour, so that more of the behaviour is defined or partially defined.
- Types specifying how many bits, or can specify in terms of other features (e.g. pointer size).
- Not Unicode.
- Avoid the confusing syntax for types that C has.
- The C comment syntax has /* even though / followed by * would be a valid sequence in C, so, such a conflict should be avoided in a better programming language.
- Parameterized types.
- Must still have the goto command.
- More powerful macros and preprocessing (including auto-generation files too), including a superset of the capabilities of C, but with possibility of hygienic macros, too.
- Better namespacing possibility than C.
- Does not need too many extra libraries than C, and can be used with any libraries for C programs (including possibility to be called from C codes, too).
- Many of the GNU C features.
- Associated data with types (which may include functions, constants, numbers, strings, other types, etc).
- You can still use pointer arithmetic, etc like is also possible with C.
- String type being the length and pointer to beginning of data; this way you can easily make slices. For compatibility with C codes, they will by default also always having the null terminator after the data, too.
I'd start with most of the best parts of Delphi/Free Pascal
Insanely fast recursive descent single pass compilation.
Assignment and equality test have different tokens := vs =
Var parameters instead of messing with pointers
Functions return values, procedures don't
Strings that are automatically reference counted (no allocation or garbage collection to worry about), counted (so you always know how many bytes of text you have), and just work, yet can hold at least a gigabyte
Units for separate compilation
Strong typing
Then I'd add some things from elsewhere
pre and post increment/decrement from C
declarative programming, using a "magical assignment operator" <<==<< or something like that to indicate that the left is always made to equal the expression on the right, and updated any time the expression changes. -- from an experiment called Metamine that was here a few years ago
Id be fine with delphi if their IDE was available on other systems like linux or mac. I also kinda find fpc a bit verbose and clunky. Something like an elm interface would make sense I think + the points you laid out
Firstly it would be persistent - so you'd never need to write SQL or file handling code again. Want to save a map or list or object of some kind? Just reference it from the root object (can be indirectly) and it is magically there the next time you start your program.
Secondly, it would be as easy to program in as Python but have some way to be more efficient when that was strictly necessary. Might be JIT or the ability to embed a lower level language or some kind of type hints that made it more efficient or who knows. One wouldn't need to spend too much time on crap like "build systems" or package management or whatever to make it work.
Thirdly: It would work very well with concurrent/asynchronous situations and have powerful abstractions for them like channels, continuations etc that would make them as simple as they could be.
Very abstract question, but the code is compiling, I'll bite right in.
I think the 'ingredients' for my ideal language are more or less the following, in decreasing order of importance:
- Quality of the ecosystem. You can't buy or engineer this, it kind of has to happen naturally. Good FFI and trying not to be a "let's rewrite the world" language help immensely. It's controversial to put this as the most important bullet point, but the world we have to program in is much more complicated than it was even just 20 years ago. Programs live in an environment and have to interact with databases, networks, etc. I just don't see myself programming in a language where I have to rewrite my own data structures or network code or postgres integration, it's way too much work for way too little gain.
- Tooling. Just look at Java and Go (also Python and JS, albeit to a lesser extent). It's a massive, massive productivity gain to have modern, effective tooling that supports modern workflows.
- Language features. I'm partial to the ML family, but I'm not a radical in this respect, I'll program in almost anything as long as it's sufficiently expressive and doesn't have insane semantics.
A few _very wild_ ideas:
- A configurable type system. I'm sold that stronger and more articulate type systems are the future, but sometimes, especially in smaller projects, it's incredibly annoying to have to prove to the compiler that the code you wrote is actually allowed. There's an inherent expressivity vs. correctness tradeoff that it would be interesting to have "different settings" for.
- A dependency system based on functional semantics rather than names. Imagine to reduce each function to a canonical representation (for example: its De Brujin encoding). Now you can design a dependency system where you can import a function based on its representation rather than its name.
- First-class algebras. Imagine being able to program user-defined structured sets. Strings as monoids, Vec3 as vector spaces, calendar as affine spaces, etc.
Pretty much just Java but a few more affordances for flat memory layouts. I know value types are in the works and the new memory api is in preview so we're getting there.
Oh, and I think every language should come with an AST parser in python. We need to really lower the barrier to manipulating codebases with code. And before any one mentions Lisp, we know — and we'll never learn. Blub blub blub.
I'd like to be able to @Annotate a class with @Localized and enforce just that it'll never get assigned to a field or passed to public functions — only local variables and private functions. For anything mutable, it'll buy you 80% of what a borrower checker would.
There is no perfect answer, or at least no universal answer, and there never will be.
The perfect programming language fits the way I think. But other people think differently, so my perfect language won't be perfect for them, and vice versa. There is no answer that is perfect for everyone. There can't be.
The perfect programming language would handle all the incidental complexity of the problem I'm trying to solve. But different people are trying to solve different problems, and what is incidental complexity to me is essential to someone else. Again, there cannot be a perfect answer.
In my opinion, Rust is what Scala would be if ownership was flagship feature and everything was built around that. Certain combinations just don't match.
Let's take an example of Scala. Great language for most business domain problems: easy to write expressive, bug-free and concurrent code. What enables those strengths also enables its weaknesses: steep learning curve, a lot of ways to reduce boilerplate which increase complexity of codebase, powerful type-system to prevent bugs/invalid states - you have to deal with "type gymnastics" and slower compilation times
I m a frontend Developer, So for me React and JavaScript is the perfect programming language. JavaScript makes it easy to develop a responsive design. JavaScript has become integral part of Internet experience because it increased interaction and complexity in website or application. It would not be possible to have responsive website, Content management systems, social media, Mobile friendly ad SEO friendly website/application with JS.
* Seamless integration/interfacing to C/C++ libs/toolchains/etc. and ecosystems.
* C/C++ core made more multi-paradigm in the spirit of Mozart/Oz.
* The runtime semantics of Erlang and BEAM VM.
So it would be a set of DSLs (one per paradigm but the ability to work together) with a common core of modified C/C++ like syntax and the ability to run on top Erlang BEAM VM with its guarantees. I think it is fully doable today and will eat the World.
I was going to say "a better version of C#" but maybe an entirely different language would be better.
C# uses stack and heap for memory management, which is to say this is an entire language designed around memory management. Should memory management be a core of a modern programming language? Available memory has expanded like 1000x in the lifetime of this language.
Fixity and associativity control at least at the haskell level.
Notation comparable to Coq level.
Generics without jvm limitations.
Subtyping or subsets of inductive types.
Rich signature overloading.
No need for general recursion, loops etc.
Pretty printing out of the box.
Static literals (from the binary directly to memory).
Good question! From the top of my head: non-opionated memory management, nice syntax (and meta-semantics), strongly statically typed, "interpreted with possibility of AOT compilation" execution model and easy and performant native interop. Let me expand the points in order.
Non-opionated memory management means that I am in the ultimate control of each allocation it does. Even if it requires GC it should allow me to redefine malloc/free and sandbox it into a memory region I choose (like Lua). It should not allocate memory willy-nilly and free it as it chooses, especially if it aims for a "system language" role.
Nice syntax is the most opionated point of mine. It should definitely be C-like and not, for example, Python-like or Lisp-like. After all, everyone can read JavaScript. I like the idea of expanding C syntax with first-class blocks (like Ruby) and semantic macros and quoting like Lisp. C has a tradition to make language constructs first-class - that was the motivation for varargs, but it's ability to create new language constructs is flawed. You can #define a "foreach" with "for"-like syntax, but it still falls short compared to what proper semantic macros can achieve. It would be nice to be able to define a "foreach" as a hygienic macro with a quote.
Typing is a non-question - I don't want to have to deal with dynamically-typed languages anymore. Extra points for local type inference. Strong static typing also enables a lot of performance enhancements down the line.
The execution model is the next salient point. My perfect language should have REPL (I'm that spoiled by Ruby and RoR) and also should be performant enough in release/production builds. Which is why it should allow both live coding and ultra-optimized AOT builds right down to native machine code. In my opinion, the best way of AOT compilation is targeting C or C++ - the AOT compiler shouldn't have to implement any optimizations Clang or GCC already have. Maybe it could target LLVM IR instead.
Native interop should be zero-cost and ideally based on LLVM tooling. The performance cost of calling a C or C++ function should be negligible. Regarding the tooling, I don't actually want to write FFI interfacing code by hand, it can easily be generated automatically.
The only language I know of that actually ticks most of these boxes is daScript, but it is still in the early phase of development and I don't particularly like the syntax of it.
I just want something that will allow me to have what FlatAssembler does but in something higher level like C. I just want a flat binary, only to use the stack, no relocations, no data, rdata etc. Strings on the stack or heap.
Been trying to hack Zig a little bit to get what I want but still needs some post-processing.
Rust with a GC (and therefore without borrow checking). I'll copy the stuff I wrote on a twitter thread recently below:
Rust's borrow checker is the most innovative but also most inconvenient bit, and GCs are certainly more convenient.
But Rust is much more than that:
- Rust traits are like typeclasses, but you can still use dot notation for better discoverability, which IMO makes them even beter (no, hoogle is no substitute for automatic, instant inline editor documentation).
- Macros in Rust are very powerful: not only can you control what they generate, but you can also control error messages via `compile_error!` and the exciting upcoming compiler diagnostic API. This means great DX is possible, even when macros are used!
- Rust lets you write code that uses Result<T, Err> that is almost as compact as exceptions in other languages. The `?` (previously try! macro) operator implicitly early-returns if there is an error. With a bit of effort, its usable even in functions such as `map` and `flat_map` via `collect`
- Traits are really helpful here in two ways: You can implement `From` for standard error types to ensure they're converted to your error type (Or you can use an awesome ready-made crate that lets you add context such as https://docs.rs/anyhow/latest/anyhow/). Traits also help .collect() work with Result regardless of whether the iterable is an iterator, vector or some other data structure.
- rust-analyzer and how well it works in vscode.
- Cargo, the Rust package manager, just works. No fuss. If you want to setup a monorepo of multiple crates, binaries, libraries, no problem - it can do that too and it will know how to handle it. Contrast with JS/TypeScript ecosystem, where monorepo setup is the topic of blogposts.
- Rust documentation culture is crazy good. Crates go as far to add a copy-pastable example to many of the functions. Thanks to python style doctests, those examples are also automatically tested. Almost all crates have an excellent "README" page that introduces the library properly. Contrast with Haskell where there's none of that.
- Some things available are just plain cool. For example, just add the Rayon crate https://docs.rs/rayon/1.5.3/rayon/ to your project and replace a few key `iter()`s with `par_iter()` and you have a multi-core program that is 4-8 times faster on your average machine.
- One of my favorite macro packages is peg (https://docs.rs/peg/latest/peg/) - it uses the compiler_error api to tell you if you've written a left-recursive definition (not good for PEG). Its super easy to use and as is Rust tradition, produces excellent parse errors with locations.
- All those C library bindings available. Enough said.
- Its really cool and convenient that you can produce a single binary. Thanks to rust-embed (https://crates.io/crates/rust-embed) its also really easy to keep it a single binary in many situations.
- Last but not least, the community goes above and beyond to make people feel welcome.
Mind explaining why algebraic types are better? than cased objects and why a limitation in the fields iterator, that prevents `==` to be automatically lifted, is connected to that?
There are general purpose languages (say, java, go, rust, ts, and c -- one is better than another for specific tasks), and then there are HDLs, DSLs, constraint and SAT solvers, and many more I don't even know about.
It would probably be a language with Python's ecosystem, a development experience like SLY provides for Common Lisp with the syntactic simplicity and elegance of Scheme/Racket that makes it so easy to create your own DSLs.
Aside from what's mentioned already: Longevity. Nothing is more frustrating than being forced to go through already working code just because some version update changed the syntax/semantic.
There will never be a Rust 2.0 for this reason. Perfect backwards compatibility is guaranteed forever. (New code can use new features using the "editions" system)
I kinda just like using the default os shell with simple cargo run/test/etc. I want to understand why REPL is more and more used, in langs like julia, elixir, etc. Do you guys find it more conveninent or something? I do like elixir playground though
Looks awesome. Haven’t had an opportunity to work with it yet but plan to.
I wish they had considered Ruby interop from inception. I am sure it's a really, really hard problem, but it would have/could have done wonders for Crystal's adoption to be able to tap into the wealth of Ruby's existing ecosystem while layering types directly in the language.
I had written two implementations of the same program in both Ruby in Crystal that need parallel (not threads!) jobs to be run. It did some more or less heavy computation on large data sets. Almost no difference in terms time execution. Crystal is nice, but if Ruby is the same, what's the point? Only thing I don't like about Ruby now is how they implemented types (in separate files... no, thank you).
But generally speaking, after 14 years of Ruby... and also trying many more languages - I do know what I want. It's not speed. It's not memory safety. It's not paradigms. It's not static or dynamic typing. It's not the packages and the community. Nope.
I want easy code navigation. I only currently know of one language, that hasn't even been fully released yet, that achieves this task to a degree. But even then, it's only 30% there.
> Crystal is nice, but if Ruby is the same, what's the point? Only thing I don't like about Ruby now is how they implemented types (in separate files... no, thank you).
You answered your own question ><
> I want easy code navigation.
Static types makes this a much, much more solvable problem. But I'm curious about what you mean here, because modern code navigation tools are pretty good - even for Ruby - but perhaps I am not fully understanding what it is that you are after.
is all you need for manual memory management and pointer arithmetic with Go.
Since the GC is fairly light and snappy, I find it's best of both world to only resort to Unsafe for the performance critical parts, and leave the GC deal with the rest.
I like to answer your post title and brainstorm at a bit higher plane (but it will have implications for the things, such as toolchains, that you highlighted).
The germinating thought was remembering the frustration of having to choose between abstraction and performance. Sure, there are languages out there that do address this generally mutually exclusive dual to some degree of success. But there are other duals out there: {semantic-clarity and concision}, and, {effectiveness, and cognitive-load}. And the existing solutions to the first dual generally don't do so well, imho, in the other dimensions.
So my "perfect" programming language would have to:
- allow the programmer (henceforth me) effective abstraction capabilities, in the sense of what the language is used to define and describe, while also allowing for practical high performance implementation.
- have excellent semantic-clarity so the poor me who has to read code more than writing it gets a break. But who wants to read War and Peace to understand how a subsystem works? Not me.
- be effective. Get things done, and have the above two qualities. But I'd rather not have to carry some monsterous mental model in my head. That can hurt. /g
So how do we get there?
We already know a lot about tricks to make abstract code fast. So a ~near possibility that I foresee is either teaming up with, or borrowing the knowledge and tool base of, cognitive sciences to get a handle on things like semantic-clarity and cognitive-load. PLT designers will need to -start measuring things-. At some point we need to start measuring cognitive load of languages as we design them, and to do that we also need a bit of help from AI (D/ML) ("cheating", "not art", "not computer science"? :) so this feedback loop can be hugely sped up.
So first we need to have measures, actual physical measurements of a sample of end users, with existing languages and tools, up to operating and debugging software systems. At this point we'll have numbers to for example compare the cognitive load of Rust vs Go. Stuff like that. We feed that data to our DL model. That model then is expected to furnish what it thinks of your shiny new language. At that point, the PLT designer first defines the characteristic measures (of those duals), and possibly let say start from an existing language, and then have our friend AI go at it.
What will they look like? Hopefully we live long enough to see.
p.s.
fair to say to get this off the ground (measuring and creating the model) requires DARPA level research effort.
Ooh this is something Ive been pondering too. Perhaps a transformer model like GPT but focused primarily on language semantics and such. Especially the point about cognitive load - which I find to be quite undersaid
// comments would be my perfect language, write good documentation
And leave the coding mostly over to CoPilot, or another AI.
Although one needs to understand coding, we should work at a higher level.
If you cannot write a good CoPilot prompt, i wouldn't accept you as colleague, if you cannot describe code your unable to properly write your own thoughts, makes you a team disaster. The language itself thus becomes less relevant these days, and the years to come.
I love Python's syntax. It's usage of whitespace forces people to have some semblance of proper formatting. Overall, I find Python code extremely easy to read.
But my god is it slow, and I've always assumed a lot of its slowness is due to being interpreted and the duck typing.