1. 30
  1.  

  2. 12

    Thanks for sharing.

    In Zig, types are first-class citizens.

    I get what you are trying to say but for types to be first-class citizens you’d have to have a dependently typed programming language like Coq or Agda. Your comptime restriction is commonly known as the “phase distinction”. The ML module system works similarly and can be completely understood in terms of a preprocessor for a non-dependently typed language.

    I think this contradicts what you say later:

    But more importantly, it does so without introducing another language on top of Zig, such as a macro language or a preprocessor language. It’s Zig all the way down.

    I would rather say that the preprocessor language is Zig, and you’re using comptime to drive preprocessing. And using a language to preprocess itself is the classic Lisp approach to macros, so I don’t know if trying to distance yourself from macros makes sense here.

    Re your approach to generics: does this flag errors at all call-sites, instead of at definition-site? This is one of the major reasons people don’t like using templates to implement polymorphism.

    1. 4

      to be first-class citizens you’d have to have a dependently typed programming language

      This is the definition I used of “first-class citizen”:

      In programming language design, a first-class citizen (also type, object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities. These operations typically include being passed as an argument, returned from a function, and assigned to a variable.

      Scott, Michael (2006). Programming Language Pragmatics. San Francisco, CA: Morgan Kaufmann Publishers. p. 140. (quote and citation copied from Wikipedia)

      In Zig you can pass types as an argument, return them from a function, and assign them to variables.

      But if some people have a different definition than this, that’s fine, I can stop using the term. My driving force is not an academic study in language design; rather it is the goal of creating a language/compiler that is optimal, safe, and readable for the vast majority of real world programming problems.

      Re your approach to generics: does this flag errors at all call-sites, instead of at definition-site? This is one of the major reasons people don’t like using templates to implement polymorphism.

      It flags errors in a sort of callstack fashion. For example:

      https://gist.github.com/andrewrk/fd54e7453f8f6e8becaeb13cba7b9a7d

      People don’t like this because the errors are tricky to track down, is that right?

      1. 2

        That isn’t what I mean. I’d try this on Zig itself but I can’t get it to install. Here is an example in Haskell:

        foo :: (a -> Int) -> a -> Int
        foo f x = f (0, 1)
        
        test1 = foo length "hello"
        test2 = foo (\x -> x + 1) 9
        

        This raises one type error, saying that a doesn’t match (Int, Int). This is what I mean by flagging at the definition site.

        In a language where errors are flagged at the call-sites (aka, just about everything that uses macros), this would raise two errors: length doesn’t expect a tuple, and + doesn’t expect a tuple. Which is it in Zig?

        1. 2

          if I understand your example correctly, this is similar zig code:

          fn foo(comptime A: type, f: fn(A) -> i32, x: A) -> i32 {
              f(0, 1)
          }
          
          fn addOne(x: i32) -> i32 {
              x + 1
          }
          
          const test1 = foo(i32, "hello".len);
          const test2 = foo(i32, addOne, 9);
          

          Produces the output:

          ./test.zig:9:18: error: expected 3 arguments, found 2
          const test1 = foo(i32, "hello".len);
                           ^
          ./test.zig:1:1: note: declared here
          fn foo(comptime A: type, f: fn(A) -> i32, x: A) -> i32 {
          ^
          ./test.zig:2:6: error: expected 1 arguments, found 2
              f(0, 1)
               ^
          ./test.zig:10:18: note: called from here
          const test2 = foo(i32, addOne, 9);
                           ^
          ./test.zig:5:1: note: declared here
          fn addOne(x: i32) -> i32 {
          ^
          

          If I understand correctly, you are pointing out that the generic function foo in this case is not analyzed or instantiated until a callsite calls it. That is correct - that is how it is implemented.

          1. 1

            Yup, I believe this is correct understanding of cmm’s comment. In Rust (and Haskell, OCaml, etc.) generic functions are analyzed without instantiation, which leads to better diagnostics. C++ doesn’t, and that’s why C++’s template-related diagnostics are terrible.

        2. 1

          […] supports all the operations generally available to other entities.

          You cannot pass types at runtime, at least that’s the impression I got from the article.

      2. 11

        So the D language does this as well, the combination of Compile Time Function Evaluation (CTFE) and static if and mixins and templates is extraordinarily powerful.

        Given D seems a lot more mature than Zig at this stage….

        Anybody have a good comparison of Zig vs (D or Rust)?

        Using the feature highlight list from http://ziglang.org/ (see also http://dlang.org/overview.html#major_features)

        • Import .h files and directly use C types, variables, and functions. (Yup D does that, plus integration with C++)

        • The Zig Standard Library does not depend on libc. (D I believe provides integration with, but I don’t think requires libc.)

        • Maybe type instead of null pointers. (Pointers are available in D, but are not required, a more useful and safe abstraction called Ranges is more common)

        • A fresh take on error handling that resembles what well-written C error handling looks like, minus the boilerplate and verbosity. (While I have some sympathy for the Rust take on this, http://dlang.org/spec/errors.html)

        • Order independent top level declarations. (Defined order with cycle detection http://dlang.org/spec/module.html#staticorder)

        • Debug mode optimizes for fast compilation time and crashing when undefined behavior would happen. (D favours removing all undefined behaviour in favour of either compilation failures or defining the behaviour, or shifting such things into the “unsafe” subset)

        • Release mode produces heavily optimized code. What other projects call “Link Time Optimization” Zig does automatically. (LTO is implementation dependent, available in cland and gcc variants)

        • Friendly toward package maintainers. Reproducible build, bootstrapping process carefully documented. Issues filed by package maintainers are considered especially important. (Ok, zig wins on reproducible I believe, but packages for most major distros are available for D)

        • Easy cross-compiling. (Arm cross compilers are avaialble for D)

        • There is no preprocessor. Instead Zig has a few carefully designed features that provide a way to accomplish things you might do with a preprocessor. (Same for D)

        • Generic data structures and functions. (Same for D with very powerful compile time introspection support)

        Hmm.

        You look like a very very bright and capable guy, I wish you’d would go have a beer with these guys and discuss things and add your weight to their efforts… It looks like you have a lot in common.

        https://www.wired.com/2014/07/d-programming-language/

        1. 8

          Lisp blurred that line so well it’s hard for me to be impressed when other languages try it.

          In any case, as interesting as this looks, I prefer the way C++ does it. “template <typename T>” on the front keeps the compile time parameters separated from the runtime parameters. Mixing them into the regular parameter list is confusing, and probably makes using them as high order functions messy.

          IMO it’d be a lot cleaner if the compiler figured this out on its own without the “comptime” modifier. Make it compute values at compile time when it can and defer to runtime when it can’t.

          There should also be a way to specify a parameter that can be substituted at either compile or run time. For example, if I want to add things at compile time, but also runtime, do I need to define the function twice? Seems silly to have something like this:

          fn add_comp(comptime a_val: u32, b_val: u32) { return a_val + b_val; }
          fn add_run(a_val: u32, b_val: u3) { return a_val + b_val; }
          
          1. 5

            Also in Forth. There, you can run your own code at compile time. It’s used mostly to extend the Forth langauge (the WHILE/REPEAT construct can be written in ANS Forth itself for example).

            1. 3

              In this example, you would delete add_comp and only keep add_run. No reason to use comptime unless you need the guarantee in the body of the function that the value is compile-time known. In this situation, you don’t need that guarantee, and if you wanted to you could still do something like const result = comptime add_run(1234, 5678);.

            2. 2

              I didn’t search a lot, but I’m still gonna ask because I couldn’t easily find it and it’s late and whatever: Is this a Zero Wing reference?

              1. 8

                Happy coincidence. I generated random sets of 3 letters for inspiration and then picked “zig” out of the list after verifying that it was a relatively unpopular search term.

                1. 3

                  I’ll admit, I simply assumed it was one without thinking twice

                2. 1

                  Does Zig make use of Clang modules? The notation is the same.