The goal is to provide better error messages across the board.
Right now we have something like
/* succ :: num -> num
*/
succ = x: x + 1;
assuming num = int | float.
But what if we had something like
/*
typecheck :: Type -> a -> a
Throws if `a` is not of type `Type`
Note that errorContext could be more structured, this is just a draft
*/
typecheck = type: value: { errorContext ? optional.nothing }:
if type.check value
then value
else builtins.throw "type error with good message here, adding in errorContext if present";
/*
typecheckNum :: a -> a
Throws if `a` is not of type `either int float`
Note that all custom numeric types satisfy this (e.g. {i,u}{8,16,32}, port)
*/
typecheckNum = value: ctx: typecheck (types.either types.int types.float) value ctx;
succ = x:
let tyCheckedX = typecheckNum x { errorContext = optional.just ("std.num.succ: argument not a number!"); };
in tyCheckedX + 1;
If a function accepts something "deep" (like an arbitrary record), we can either not check anything, or just check some shallow piece of it. Like, imagine we accept a record as an argument and we are looking for a field "x" or a field "x.y", we could at least typecheck those/check for their presence. Whereas if we are accepting an arbitrary record which may potentially be fully evaluated, typechecking everything could be expensive. My mental model of nix is mostly gradually typed, so this is okay-enough. Unfortunately what I am proposing is to do a lot of the work we would like the nix evaluator to do, but that can't happen.
The big tradeoff here is code complexity. std implementation gets more complex for every argument we are typechecking. Better error messages also means more complexity.
Personally, in my own libraries, I am generally more in favour of higher internal code complexity if it means a cleaner and nicer user experience. I have felt that pain as a maintainer over and over. But it has a satisfying payoff as a user. If the user experience is bad, what's the point? That's my POV anyway.
If we did this, we'd have to come up with some type system, write it down, and try to keep consistent rules. We don't need to be exact, because I heavily suspect that being exact here is hard, but at least we can provide users some documentation about "here's a type, here's the syntax to denote that type, and here's what that type means"
And then we can create some hoogle-like thing and host it.
The goal is to provide better error messages across the board.
Right now we have something like
assuming
num = int | float.But what if we had something like
If a function accepts something "deep" (like an arbitrary record), we can either not check anything, or just check some shallow piece of it. Like, imagine we accept a record as an argument and we are looking for a field "x" or a field "x.y", we could at least typecheck those/check for their presence. Whereas if we are accepting an arbitrary record which may potentially be fully evaluated, typechecking everything could be expensive. My mental model of nix is mostly gradually typed, so this is okay-enough. Unfortunately what I am proposing is to do a lot of the work we would like the nix evaluator to do, but that can't happen.
The big tradeoff here is code complexity. std implementation gets more complex for every argument we are typechecking. Better error messages also means more complexity.
Personally, in my own libraries, I am generally more in favour of higher internal code complexity if it means a cleaner and nicer user experience. I have felt that pain as a maintainer over and over. But it has a satisfying payoff as a user. If the user experience is bad, what's the point? That's my POV anyway.
If we did this, we'd have to come up with some type system, write it down, and try to keep consistent rules. We don't need to be exact, because I heavily suspect that being exact here is hard, but at least we can provide users some documentation about "here's a type, here's the syntax to denote that type, and here's what that type means"
And then we can create some hoogle-like thing and host it.