One additional comment, because I don’t want my last response to be seen as criticism of Unitful.jl or DynamicQuantities.jl, two awesome libraries that I could not live without.
The point that I wanted to make is that type-level programming is simply not the easiest approach for these kinds of problems.
The n-queens problem is the perfect example, because it is very similar to what a typechecker would do. In Typing the Julia Interview, you’ll find 200 lines of code of the form:
struct AddQueens{n,x,cs}
function AddQueens{n,x,cs}() where {n,x,cs}
pred = PeanoLT{x,n}()
AddQueensIf{pred,n,x,cs}()
end
end
struct Solution{n}
Solution{n}() where {n} = First{AddQueens{n,Z,Cons{Nil,Nil}}()}()
end
I barely understand what this does, and I am certainly not able to write such code on my own, probably a safe assumption for 99% of the Julia community. Obviously the insanity of this approach is what makes it such a great hack.
Trying to express complicated structures through type-level programming is a fun exercise, but not a good engineering practice, as long as there are alternatives. Just like C++ template metaprogramming.
On the other hand, a version of the n-queens based on constraint programming, CLP(FD), is very simple. Even if you’ve never seen Picat, the code is immediately clear once you know that #!=
means to post a constraint that both sides are not equal.
This is the entire code, while the type-level version can barely solve N6, the Picat version can do N1000 easily.
queens_naive(N, Q) =>
Q = new_list(N),
Q :: 1..N,
foreach(I in 1..N, J in 1..N, I < J)
Q[I] #!= Q[J],
Q[I] + I #!= Q[J] + J,
Q[I] - I #!= Q[J] - J
end,
solve([ff], Q).
I chose Picat, a slightly modernized Prolog version, because it has loops, Prolog would have used a recursive call instead. Of course, an SMT solver could also be used and there are CLP libraries for most programming languages, including Julia. CLP just happens to be perfectly integrated into many logic programming languages.
For this reason, I strongly believe that we need to abandon the idea of creating more and more complex type systems, then making more and more heroic efforts to express our program logic in such type systems, only to discover that we’ve pushed the compiler to far and need to follow up with even more heroic efforts on the compiler side to get our programs to compile in a reasonable time, if at all. Julia is neither Haskell, nor Rust.
Instead, the language should provide a basic type system that makes sure the right machine code gets generated with the minimum amount of dynamism. All checks beyond this should be left to user-space libraries.
This would also provide additional features that are unthinkable otherwise, such as e.g. different levels of warnings depending on the type consistency.
In the case of measurement units, a system could have the following warning levels, depending on whether it was used for quick scripting or for important engineering calculations:
- Units are not consistent, e.g. 1u"m" + 1u"kg". This is clearly an error.
- Units are consistent, but not uniquely determined. The equivalent of a type instability, e.g. a function that returns u"m" or u"kg" depending on the context. This is most likely an error.
- Constants don’t (yet) have units, e.g. 1u"m" + 1. An error, but might need to be ignored during gradual typing.
- Units are consistent, but not fixed, e.g. the unit-polymorphic function f(a,b) = a+b. Might be ok for a generic function.
- Units are consistent and fixed, but not all variables have units specified. E.g. x=f(1u"m", 2u"m"). x inherits the unit through f, which is probably ok.
- Units are consistent and fixed, and all variables/functions have types attached to them, e.g. in the example above x is specified to have a type u"m". I’d want this for a high reliability application.
The nice thing of such an approach is that the programmer can start with an untyped program, and then add types throughout the program, and get a warning the moment the types are not consistent. There is no all or nothing requirement, where the program does not even compile as long as not all variables are properly typed.
Providing this level of error handling through the type system is probably impossible, but for an external library based on constraints, it would only be a very minor addition.
Julia has most of the machinery already in place. What is lacking is a good way to attach types and typing predicates to variables and functions, similar to @doc. Maybe this mechanism could be enough to capture extra type information.