You can pick and choose features from existing languages, create entirely new features, real or fictional ones even, that we might not even be able to actually construct today!
That goal isn't possible because designing syntax to make certain tasks "easy & simple" inevitably means you're making some other task hard & complicated.
That's why there isn't a One True Programming Language to end all other languages because you can't design perfect syntax to handle everything from low-level embedded all the way up to 4GL and scripting.
So a language designer would have to know upfront what the technical compromises will be even when starting from scratch with a clean sheet for a brand new language.
> length i = 12 inches
> length h = 1 foot
> i * h
area 1 square_foot
> square_inches(ih)
area 144 square_inches
> hours(ih)
error: type mismatch
Time and money, as well (currency conversion not required)
* Enumerated errors. Some part of the tooling should be able to tell me all the errors that a function could raise. I don't know why this isn't more common, but sometimes errors are not of the type that I would have expected for the failure, or I don't know what type the error might have (i.e. there isn't usually a stdlib error for an HTTP 503 in a client).
* Optional static typing or a "loose syntax mode". Even if they don't behave exactly the same, it would be great to be able to do rough prototyping and production-ready work in the same ecosystem. Imagine if Python and Go or Python and Rust used the same ecosystem, and libraries were usable in either. You prototype in the loose Python syntax, and tighten it up to the Rust syntax when you're happy with the prototype.
* Macros. I don't think any language is ever perfect, and macros give you a means to work around warts in the language. They're complicated to write and maintain, but I still think it's often better than dealing with the nasty side of a language.
Because it's for everyone, it has to be idiot proof. It'll be strongly typed. It'll use curly brackets, not indentations. It'll give a build error when you do bad practice stuff like have functions that do too many things or name classes on anything that isn't a noun. It's 2021. It'll have AI that checks for bad practices, trained on Uncle Rob's Code Clean.
One of the distinguishing features will be plugins. You don't like the rules that E came in? Make your own!
Instead of writing binary searches, or setting up network functions and unit tests, everything will just be prebuilt. We have eQuery that simplifies everything.
Better yet, we can decentralise that so that everyone can share E code. You can do everything from padding to blockchain with a single import. epm will manage packages and dependencies for you!
What if you think E is too restrictive? Maybe you work best with weak typing. Or you think the dates should be yyyy-dd-mm format instead? Or that the prebuilt grey_500 should be gray_550. Then there's TypeFreE, an unopinionated version.
Apple doesn't support it? That's fine, just use E Native, which ports everything perfectly on all platforms.
Do you worry E is too powerful and may used for evil? That's okay, the whole thing is being maintained by the OpenE Foundation, whose goal is to make sure that E is usable by everyone but restricts access to those who they deem not evil.
Then don't add to it - avoid the "which python" problem. (Python does need any of the recent features, and few will use them, but they'll confuse many and add many bugs.)
I know some languages implement such features, but these aren't mainstream ones.
My ideal language would combine these in some way, but with some changes, e.g. garbage collection should be scoped instead of on/off and newtypes should be first-class.
It's difficult to have this much functionality without going overboard on complexity like C++ has done, so careful thought would need to be put into how to harmonize all these features and what compromises need to be made.
The most important thing would be to adopt an edition system like Rust has, not just for the language but also for the standard library. This would allow breaking changes to fix inevitable design mistakes.
I want code that's easy to write and easy to read. No fancy tricks. No decorators, no hundred-character lines for a print statement, no manual memory management or custom operators. A child should be able to use it.
I want code that's easy to maintain and deploy. Never worry about language/tooling versions again. No gcc, no g++, no rbenv, no Maven, no venv/pipenv/poetry whatever.
My target audience is anyone that doesn't have a computer science degree. A language for accountants and office workers. For school teachers and twelve-year-olds.
Throw out the foobar tutorials and Tower of Hanoi logic puzzles.
"Here's how you can download all your students' submissions from Blackboard in one button click."
"Here's how you automatically format a spreadsheet without having to click on every columns."
"Here's how to make your computer wiggle its mouse so you can skip the mandatory attendance checks for your online classes."
Another interesting idea might be introducing architectural layers as a language concept wherein you can specify which layers can interact with one another and then provide some enforcement mechanism in the compiler to prevent layers from interacting
Is based on the relational model (+spiced with multidimensional-array capabilities like kdb+/pandas), and allow to have universal query/crud capabilities to anything (files, vectors, btrees, hashmaps, rdbms access(not yet!)), etc.
In other words, it has in-built linq-like capabilities:
-- A column, aka: Vectors...
let qty := [10.5, 4.0, 3.0]
-- Like APL/kdb+ operations apply to more than scalars
prices * 16.0
-- The ? (query) operator allow to do SQL-like queries to anything
let doubled := qty ?select #0 * 2.0
let products := open("products.csv")
-- like files!
for p in products ?where #price > 0.0 do
print(p)
end
--so, we can do joins between anything:
for p in cross(products, qty) ?limit 10 do
print(p.products.price * p.qty)
end
It also has some small things to fit my domain (business apps), like decimals instead of binary floats as the default, in-built dates, easy to write tabular data in code, lazy streaming I/O and the like.---
I'm now trying several things that I wish to make it work:
* Loss-less parsing and high-quality errors
* Type inferencing relational operators (like a JOIN b JOIN c JOIN d SELECT a, b) because type this manually is not ergonomic
* Making a way to the user to create relational operators (like with generators on python) that fit well with query optimizations. In other words, I wish to have a simplified way to do compiler optimizations but for the query/array operators, that I bet will dominate all.
With this I wish to get into version 1.
---
And moving further in the dream: I wish to build a access/foxpro/excel tool, so it also means an improved language with a UI toolkit across platforms. And maybe even a in-built rdbms (using sqlite? from scratch?). For this, is where manpower and money is needed.
Anyway, I think get the lang to be enough for my scripting needs is feasible, so I only limited to how much far I can go. But living the dream!
- request reordering arguments to find a match for when you don't remember the right order. I think this is better than named parameters and other hacks.
- The very opposite of Python: non-printing characters are skipped and won't affect anything. This requires dedicating a character to the separation of tokens. I think comma could be used. I would also use parentheses for grouping tokens.
Edit: Forgot that HN wants empty line to respect '\n'.
hashes can written as `id: 1, name: "Dorian"`
js-deconstruction like `{ id, name }` and `{ id, name } = user`
comments in hashes like json5, e.g.
id: 1 # first user
name: Dorian # me
ruby "everything is an object" without the $ perlismstypescript style types, e.g. optionals, mostly deduced
interpreter and compile to binary options for fast iteration and fast production code
compatibility with web assembly
compiles to javascript
all the meta-programming of ruby like define_method, method_missing etc.
autoload like what rails does
a prettier-style formatter
and mostly ruby for the rest
and a sane dependency manager like rubygems/bundler
1. A dependent type language with superior IDE, so that the people could write a signature (specification) and leave the implementation to the computer (similar to Copilot, but it derives from types. If we could combine dependent type and Copilot it would be really powerful). Also, people could write specs and try to auto-complete proofs of the same system.
2. Or another language similar to Erlang, but statically typed. Since Erlang feels like an OS, the language is going to BE the OS, the experience would be very integrated like Smalltalk and Pharo, or Elisp and Emacs. It would not use plain text for everything like Unix, but structured data of the language itself. The key of the language/OS would be the lightweight processes and fast context switching. It could also be installed on bare metal, making it an ideal server-side language. Like Erlang or Kubernetes, it can form a cluster very easily - it lets you treat a cluster just like a single machine transparently, because a single machine is a collection of processes, a cluster is also a collection of processes as well. So there would be no difference between code for a single machine and a cluster. If major cloud providers support this language/OS, the ops part would be extremely easy because there would be no *nix, Kubernetes, and Docker, instead, there would be GenServers, Supervisors, and Registries which are just application code from developers (instead of ops/devOps).
3. Or another more practical language would support a superior range of platforms, like JavaScript but much better. JavaScript already dominated the browser, the desktop, and sometimes the mobile and backend. But the desktop and mobile experience is not the best, and there's not much about system programming. If we take a look at languages like Nim, it's a system programming language with a compact syntax that feels like scripting language. So assume a language that 1) feels like a scripting language for faster development 2) not only reach all platforms support JavaScript, but also for system programming, and game programming, etc 3) very fast so there could be better alternatives for Electron and React Native. It seems possible to build such language on WASM, it just requires a lot of work. The thing with a WASM language is it also should be a language with a very small core, and very extensible, or the binary would be large and waste time for download.
Well, in case if the three fictional languages happen to be the same it would be even better...
Other concrete features I would like to have is:
4. Symbolic programming (from time to time I wish the language I'm using supports it)
5. Built-in logic programming sub-language (so that it's easier to query things)
5. Other than the language IS the OS, the idea of language IS the database is also very useful. Instead of writing server talk to DB, we write code on the DB cluster so there's less inconsistency (because the logic and data are in-place) and less back and forth. There are many DB support stored procedures or Lua scripts, but if the language is a more full-sized general-purpose language that runs on WASM would be great.
Have various computer science algorithms (graphs, trees, parsing, et al) tightly integrated into the language. (I know the popular opinion is to have these in libraries, however, I think having built-in data structures and algorithms would lead to further innovation.) The tooling should aware of run-time complexities of various algorithms and steps and thereby reason about the same. (E.g., "Warning: You have declared 'xyz' to be a graph, however, a tree is sufficient here.") Tooling should be able to say that while the programmer thought the implementation is Big-O[N^3], it's actually exponential.
I wonder if programming languages can start making native use of machine learning advancements. Copilot et al already would have some internal representation as embeddings, so instead of just generating code as text, there should be value in using those representations as a part of the build tool-chain too.
Better representation of 'is-a' relationships between various types. E.g., I should be able to pass a tree to a graph algorithm without writing any extra code or having run-time performance overheads.
Get rid of overflows or precision constraints fundamentally from the language (and the the underlying hardware; over the time). I would like the tooling to automatically reason about which type is the best fit for the implementation during compilation -- byte, unsigned int 32, big-int, etc., via analysis of the program, and tell me what it chose and why. It may chose to typecast automatically for specific calculations when intermediary calculations need larger bit widths.
Likewise, I would like innovation on how null references can be fundamentally dealt with as opposed to how each new programming language tries to work around the same in various ways.
The fact that we use monospace fonts for programming seems to me to be a weakness of programming languages. We often need some alignment between adjacent lines of code to see the semantics of the code well. I would like to see a programming language which works well with variable-width fonts, without causing readability issues. Why you may ask? I think this should be explored as this would lead to innovation on how to make semantics come out independently of font width and text alignment.
Having an 'alias' at many places would be super helpful. E.g. a base class may be generic method named such, whereas a derived class (more specific) may use the very same method implementation, while choosing a better name for the method. This is syntactic sugar only as this is readily doable now by having a new method that calls the base method as it is. This may be handy for example when the is-a relationship is handled between a tree and a graph in the example above.
Language-native integration of units and dimensional analysis.
Finally, I am a big fan of symbolic computation abilities like seen in Wolfram Language. The tooling can reason about the expressions/code, and figure an optimal strategy whether at compile-time or run-time.
(1) Structured concurrency, best explained at [1], [2], [3], and [4]. More on this later.
(2) Extensibility. Most people think that this means macros. It doesn't. However it's done, as curryst said, languages are not perfect, and extending a language is important. See "Growing a Language" by Guy Steele, Jr. [5] More on this later.
(3) Like many other posts, having real-world units like feet, meters, pounds, kilograms, etc., for calculations and type checking, but the default number type should also be a BigRational.
(4) Ranged types, like Ada, allowing programmers to explicitly limit the range a type can be, eliminating errors. For example, a type that is used in the denominator of a division could be restricted to being not zero, and you don't need to worry about divide-by-zero for those divisions.
(5) Consistency, a massively underrated feature. The use of something in the language should be the same everywhere. This simplifies learning and makes it easier to understand features you haven't seen yet.
(6) Design-by-Contract and other formal methods "in the small". It should be possible to use formal methods on little pieces of code. Even if you can't go so far as to prove them correct, it should be easy to increase your confidence that they are.
(7) Conditions and restarts instead of exceptions. Conditions and restarts come from Lisp, and it is one of the greatest ideas in computer science that almost nobody knows about.
(8) Composition, not inheritance. I think that most programmers have agreed on this.
(9) A way to solve the Expression Problem. This can be multi-methods or some other way.
(10) Dependent types. This will help with formal methods, but they are also powerful outside of that context.
There's definitely more, but I'm starting to feel lazy.
About (2), as implied, it is possible to make a language extensible without macros. This is done by fully defining the internals of the compiler and allowing the language access to them. In fact, you get 99% of the way by not even defining everything, but by just defining the IR and allowing the programmers to add keywords with functions to parse after them. In fact, I would argue that defining keywords gets you 100% of the way that macros do because macros are, in essence, keywords themselves.
As an example, say a programmer builds a build system in my language and call it "awesomebuild". They can define the keyword "target" for making a target to build, and others can use it, like so:
import awesomebuild
awesomebuild.target program_name: prereq1, prereq2 {
// `$` Means run the following as a command.
$ compiler -o program_name prereq1 prereq2
}
About (1), I will just reproduce a comment I made on lobste.rs [6] here.Structured concurrency will be, I think, amazing, but I have had an idea recently that I think would make it even better.
Structured concurrency, as currently defined, turns execution into a tree of threads. But what if we could turn it into a DAG?
It would have these restrictions:
(1) To prevent potential memory cycles (and thus, needing a garbage collector or reference counting), forbid all types from being able to, directly or indirectly, have pointers to themselves.
(2) Some form of RAII with automatic calling of destructors, like C++ or Rust’s Drop trait.
(3) Have structured concurrency as it currently stands.
(4) But add the ability for threads to ask another thread to run code for them.
Number 4 is the magic. What happens is that, when a thread creates data (the “create thread”) that other threads might want to use, it then creates a child thread to run the code it would have run on the data and waits for requests from other threads. Then, when other threads put in requests for the data, instead of getting a pointer to that data and continuing on their merry way, requiring reference counting for the data, each thread sends a function to the create thread, from which the create thread spawns another child thread that has access to the data it created, which would give every thread indirect access to the data that it wanted.
Then, every thread that requested the data is paused, waiting, while the execution of the thread it requested runs.
The reason the create thread spawns a child thread instead of just running code on the data itself is two-fold:
* It allows it to respond to requests, and
* It prevents the situation where an already existing child of the create thread requests the data and then the child is dependent on its parent to exit first. If the child of the create thread is dependent on a sibling to exit first, all is well.
In other words, creating the child thread first is what keeps the execution a DAG.
Restriction number 1 is important to get rid of all need of any kind of GC, including borrow checking!
Some may claim that it’s impossible to eliminate cycles, but you can always create a “parent” type that points to all of the child types that could be in a cycle.
For example, to create a linked list, create a parent type that stores an array with data, and alongside the data, it stores handles that it knows how to interpret as the prev and next pointers. The children don’t know how to interpret those handles, but the parent does, and it can do everything you need it to do in order to emulate a linked list.
A programming language that provides these facilities should also have reference counting, like Rust does despite its borrow checker, because sometimes, more complicated things are needed. However, if a programmer decides to program with these restrictions, they would have these benefits:
* No memory leaks.
* No use-after-free.
* No memory safety issues.
* No borrow checker to explode compilation times.
* Memory allocation that could be entirely done with bump allocators, except for data that may need to grow, such as resizable arrays or maps. Even dynamically-allocated stuff that knows what size it needs, but doesn’t need to change its size after allocation, could use the bump allocator.
While there are some details (a compiler would need to figure out when an item escapes a function), I think these ideas could be pretty useful.
As for escape analysis, we could make it easy and say that the compiler should just error if a value escapes. I personally would prefer something more sophisticated, where we ensure that a value does not escape to global variables. If a value escapes by being returned, the compiler should just treat that as though the caller has responsibility for it, and it already knows what to do in those cases (nothing in the callee, and call the destructor in the caller, unless it escapes, etc.).
[1]: https://250bpm.com/blog:71/
[2]: https://250bpm.com/blog:137/
[3]: https://vorpus.org/blog/notes-on-structured-concurrency-or-g...
[4]: https://gavinhoward.com/2019/12/structured-concurrency-defin...
[5]: https://www.youtube.com/watch?v=lw6TaiXzHAE
[6]: https://lobste.rs/s/8msejg/notes_on_structured_concurrency_g...