YANTS - Composable Nix types with polymorphism, structs, enums (+ matching) and more

This week seems to be my week for bizarre Nix ideas and, in that spirit, I present Yants: Yet Another Nix Type System.

(Click the link for amazing screenshots!)

Yants is inspired by runtime type-checking in Common Lisp, where checks wrap around values like a “type assertion” and return the value on success or raise an error.

Current features:

  • Supports type-checking Nix primitives (int, string, function, derivation etc.)
  • Supports polymorphic types (option, list, attrs, either etc.) that compose with others (e.g. option (list (either int string)))
  • Supports named & anonymous struct/record type definitions (e.g. struct "person" { name = string; age = int; }) which also fully compose with other types
  • Supports enum declarations and matching with exhaustiveness checks
  • Supports defun declarations for typed functions (including currying!)
  • Not tied to NixOS modules in any way
  • Fewer than 100 lines of Nix! (Not anymore, but only barely above!)

I’m not sure why I wrote this yet, but it does feel like something that could be useful - for something. We’ll see.

15 Likes

I can see this being useful when trying to debug one of those mysterious errors Nix is giving back.
Being able to quickly sprinkle the nix code with those asserts would allow to make the debugging experience much nicer.

arrow type missing :slight_smile:

It also doesn’t look like “type-system”, but runtime validators. But it provides nice error messages.

Well, it’s a type system - just not a static one :slight_smile:

Typing functions and their arguments is quite hard, two strategies I’ve considered:

  1. Carrying metadata (ala lib.setFunctionArgs) with type-checks for each argument, this probably doesn’t work for curried functions though.
  2. Typed function utilities which wrap nested calls in closures that wrap the type checks.

I’m not sure why I wrote this yet, but it does feel like something that could be useful - for something. We’ll see.

… then you will write a unification layer on top of that, then we get a competing deep-merge system…

Nice work, of course, regardless of existence of obvious applications!

No joke, I clicked the link for the screenshots.

In all seriousness, this looks neat. I bet you had fun writing it. I’m not sure how practical it is though, because these are just runtime type assertions rather than type annotations, meaning I can’t look at a function signature and see what types it’s expecting. But in the absence of a real type system I could see someone using this (combined with comments) as a “it’s better than nothing” solution.

Challenge accepted!

I think this is already useful for providing useful feedback & annotations in cases where people write “extensible” Nix APIs. A more function-oriented NixOS module system could also be an interesting use-case … lots of experimentation to be done!

Would you like some Lisp keywords in your Nix?

Typed functions! Signatures! Currying! This is starting to come together in a much nicer way than I expected.

6 Likes

Interesting idea. Two observations come to mind:

  • Modules are already functions. types.deferredModule in particular can be used as a function (although functions are of course unidiomatic for configuration systems where all configuration is data, which is why you won’t see it often, and it didn’t exist when this conversation was initially held)
  • Functions lead to UX problems with overriding, and possibly also with merging (although I haven’t full explored that difference in this context). As an example, consider the pkg.override method and pkg.overrideAttrs. Let’s call the function we pass to callPackage a package function. override and overrideAttrs are separated by the package function: the prior changes it arguments, whereas the latter overrides the result. The trouble comes when you start combining these. What if you call override after overrideAttrs? By default this wouldn’t be even be supported, so callPackage has special code to support overrideAttrs. This is clumsy and error prone because it creates coupling, really bad coupling. We haven’t solved this architectural problem, and I don’t think it’s for a lack of trying. Certainly I’ve spent some time to fix that, but there didn’t seem to be an easy fix in the confines of what can be done in Nixpkgs without “too significant” changes. I’d love to be proven wrong, and maybe it really does take an interface change like [RFC 0067] Common override interface derivations by FRidh · Pull Request #67 · NixOS/rfcs · GitHub, although I don’t know if that RFC is backed by a solution to this problem, and even then it might not scale to overriding methods like withPackages that could be defined in passthru using finalAttrs.overrideAttrs.

Just some insights in case anyone wants to go off and play around with such an idea. There could still be an interesting idea hiding here somewhere, and I guess deferredModule and that rfc are actually proof of that, but there could be more!

Sorry for reviving this old thread, to those who take offense :slight_smile:

2 Likes