Disclaimer: the perfect programming language does not exist. Even if it did, different people want different things, so probably the ideas in this post would not reflect the ideas from different people. With that being said, let’s start with a little background:
I usually prefer dynamic languages. There are also moments when I miss static typing, but most of the time the “code/solution exploration” and a good REPL (and code design) do the job of reasoning about the shape of my data.
Except when it doesn’t. Then, things get ugly. Real fast. There’s always bad code that you need to work with, even one that you wrote about six months ago, and now you’re dumbfounded, looking at the code trying to remember what the hell were you thinking when you wrote the code and why you did think it was a good idea to wrote it that way at all. It happens with everyone. And that’s when static typing can (and will) help: it reasons about your data. You can have a variable named
a, but at least you know it have the fields
c: Int, whatever that means. But it helps.
Maybe we could have a language that allows you to turn on/off the typing whenever you wanted? With better REPL support? So, this would be my dream language to work with.
Prerequisite is to have a REPL as good or better than Clojure. This means a REPL that can be used for tooling (autocomplete, goto definition, etc, as Clojure’s is used) and that opens a port to connect to it and to run code. Also, it should be able to run the webservers/applications from inside the REPL, to hot-reload code (ClojureScript’s Shadow-CLJS REPL is even better than Clojure in this matter for me), to show compilation errors without needing an external process, and run tests (or fragments of tests).
First of all, I would like it to be a LISP. So far, only lisps gave me the feeling of wanting to work with macros, and by doing so it is simple to add behavior that the language would not support (like pattern-match, deconstruction, etc). Also, there’s no “syntax” to learn, just commands, and somehow there’s a beauty in that sea of parenthesis. But, if someone could make a language with macros as expressive as in a LISP, it should work fine too. Also, homoiconicity is a must – the power to run code on a REPL, pretty-print the result and use it as a seed for a test is too powerful to be left alone.
Second, it needs to be functional. It doesn’t need to be Haskell-functional, LISP-functional is good enough for me, and I would love it to have dependent types. Let’s see for instance how it works on Clojure:
(defn positive? "Any integer z in Z where z > 0." [i] (and (integer? i) (pos? i))) (def PositiveInt (s/pred positive? 'positive-integer?))
This is using Prismatic Schema. It works as we expect: we define a function, then we check it. Probably would be even better if, together with these ideas, we could also add a “coercer” and a “generator”, so we could receive a data format and define “if the format does not conforms with this type, try to coerce it to this type, and if its possible, return the coerced value”. Generators would be perfect too to add seeds to tests. There are coercions on Prismatic Schema, but they are no so smooth to use. Also, after working with immutable data structures, I can see their value waay better than before, so if possible I would not want to go back to mutable data. Ever.
Third, I would like to turn on/off types for some functions (or moments). In these cases, my code would not run/compile (exactly like traditional static typing) when the types doesn’t match, and when these functions would be called from a dynamic part of the system, it would add a run-time check. I think that Typed Racket does this, but it does not allow me to turn off all typecheck as far as I know – once in typed land, always typed.
Fourth, I would like this language to have most of data-structures and objects written on itself (bootstrapped language). This would mean that it would be easier to port to different runtimes (for example, port to Node.JS and JVM). This would mean that, as in Clojure, we probably would prefer a reduced number of structures, like
Record. This is fine too, as long as they are implemented in the language itself, so that adding a new runtime would be simple and easy (something like implementing a bunch of classes or structs/functions).
The reason for it is that the ability that we have on Clojure to use Java libs, and on ClojureScript to use Node or JS modules is too powerful to be lost. I do believe that there’s merit on creating a new runtime for any new language, specially if we’re targeting for native, but new programming languages suffer too much resistance; if we add on it the problem that there’s still no library to connect on databases or the absence of mature web servers on the new language, this would probably mean a premature death of the language. As an example, Ruby is a very old language and some libraries are simple not mature enough to be used on their own (for example, the
pg gem that connects to PostgreSQL does not typecast attributes – they all return as strings).
Fifth, I would like the absence of
null. Even on the dynamic side of the language. One possible implementation would be that on interop we should call two versions of a method: one “strict” that would throw an exception if a null was returned, and another non-strict that would return a
Maybe. Using Clojure, again, as a base:
;; This can give us a "Null pointer exception" (def a-name-from-db (.getString resultset "name")) ;; This can't, but we need a default value (def a-name-from-db (.getString? resultset "name" "<NO NAME>"))
The above is a “naive” implementation of a nil-free Clojure: every interop could have a method that ends with
?, that needs to accept an extra parameter the same type as the original. If the function returns
nil, it would be replaced with the default parameter. This means that there’ll be no way to even represent
nil on the language. For functions that generate side-effects, we can maybe return
IO, as Haskell does, or something like
Unit from Scala – so it would be a different type, and will not typecheck, will give us assertion errors and not null pointer exceptions that we have today. This also means that functions would never be able to return
nil – they’ll all throw an exception when you try to get an invalid key from a hash, or an overflow if you pass a wrong index to a vector/list.
Today we have to make a trade-off: or you use a static language and loose flexibility to experiment, but gain confidence because you have a type-checker and simple mistakes will be caught at compile-time, or you use a dynamic language and accept all the risks giving up multiple compile-time checks (or even having no compile-time at all).
The problem (for me) is that at different moments I want different things. Right now, for example, I’m working with a system that have lots of experimenting: I’m find data on a database and correlating multiple things using a RETE algorithm; then, at the end, I have rows for a specific (and very complicated report). The problem is that the report have multiple nuances that need to be addressed one at a time, and our real world representation of data doesn’t always comply with the report’s rules; so, there are lots of experimenting: at one time, I just publish “raw data”, but this means that my algorithm will need to correlate more things and will consume too much memory; at other time, I normalize some of the things at database side (make a complicated query and use Java’s
ResultSet to generate a denormalized Clojure map that will be fed to the RETE).
So, if I start the program with static typing, I’ll have to change types all the time, and it’ll propagate to all parts of the system. With dynamic, I can make a new testcase, test the hypothesis of using a different shape of data, and if it works, I can adapt the old code to use this new shape (but then I’ll need to update all my older tests and hope that things work fine). None of these two approaches are really ideal.
What I am doing is using Schemas at Clojure side so that I can instrument things when it’s convenient (it’s kinda a type-checker that only works when activated, and only at runtime). But this approach needs that every branch of code is covered by at least one test; also it’s harder to know when there’s an error, and where is the error (I need to read a stacktrace). One more problem is that Prismatic Schema messes code coverage a little.
So, what’s this language?
It doesn’t exist ¯¯_(ツ)_/¯¯.
No, really, it doesn’t. The only language that is close enough, for me, is Clojure – it passes almost all pre-requisites except static typing (Typed Clojure does not work really that well, and Prismatic Schema does not checks types at “compile-time”, whatever that means on Clojure; also, specs solves some of the problems, but are more difficult to use and, somehow, schemas integrate better with Clojure’s toolings). But also, Clojure fails miserably with the absence of
nils: it have then everywhere, and at some points they are even expected: for example,
nil is understood as an empty vector, list, array, or string sometimes – and that sometimes is the problem, because without testing the code or referring to documentation, you’ll never know when you’ll have a null pointer exception.
At the end, programming is not a solved solution. I think there are multiple ways that we can experiment and discover new ways of solving the same problems, but somehow we keep making the same kinds of languages with the same kinds of trade-offs. Maybe what I’m hoping is something that does not make any sense. Maybe it does have sense, but it’s impossible to implement in real life (even a subset of it); maybe it just doesn’t worth it, or maybe somebody did try it on the past but it never became famous – who knows?
The only thing I’m sure is that we’re still not at the end of the road of things that we are able to do – and I really hope that we keep experimenting.