[HN Gopher] Zig's comptime is bonkers good
___________________________________________________________________
Zig's comptime is bonkers good
Author : todsacerdoti
Score : 393 points
Date : 2025-01-07 01:15 UTC (21 hours ago)
(HTM) web link (www.scottredig.com)
(TXT) w3m dump (www.scottredig.com)
| HeliumHydride wrote:
| With reflection and code generation coming to C++26, and the
| already existing constexpr/consteval machinery, C++ will be able
| to do all of this.
| dwattttt wrote:
| Bash scripts and (optionally) an assembler can do this. That's
| not really the point.
| pipeline_peak wrote:
| > Bash scripts and (optionally) an assembler
|
| What do you mean by that ?
| dwattttt wrote:
| In much the same way as "being Turing complete means you
| can do anything", it just being possible to do compile-time
| execution isn't the reason Zig is exciting here.
|
| C++ adding the same features means it'll be possible, but
| it has a much larger intersection of alternative features
| to introduce edge cases with, and _many_ implementations
| that will have this feature with varying quirks: all of
| this means C++ will have to do a _much_ better job than Zig
| does here to achieve nearly the same result.
| rowbin wrote:
| Thanks for that. I loathe replys like "but
| lang/framework/... can do/will be able to do something
| similar/does something else which I like better/...".
| Well, it's not about that. It's about how easy it is to
| use, how good it is from preventing you to shoot yourself
| in the foot, sometimes how performant it is, ...
| benatkin wrote:
| It's suggesting that Zig is convenient and that C/C++ won't
| be.
|
| I think both will be nice.
| carlos22 wrote:
| Well C++ has a history of not really being convenient and
| makes it easy to shoot you in the foot.
| logicchains wrote:
| That's something different, code generation. The point is
| that C++26 will have compile-time reflection without any need
| for an external code generation tool, similar to Zig.
| kstrauser wrote:
| With the main difference being that if you start Zig today,
| you'll get to use that a good decade before it's reliably
| available in C++.
|
| Hmm. Actually, now that makes me want to learn Zig.
| kevin_thibedeau wrote:
| Consteval is usable today for a lot of scenarios that
| would require code generation tooling in years past.
| dwattttt wrote:
| How does consteval interact with thread_local? Defined
| (although I'm not sure what definition could make sense),
| error, or the dreaded "No diagnostic required"?
| flohofwoe wrote:
| In the end, generics, comptime and code generation are just
| different steps on the 'stamping out specialized code and
| data' ladder though (e.g. all three are useful).
| int_19h wrote:
| Ergonomics matters, though. In C++, this feature is bolted
| onto the language decades after it was originally designed.
| Zig is designed around this from the get go (which is why
| it doesn't have e.g. templates as a distinct feature).
| edflsafoiewq wrote:
| comptime simplifies and unifies much of that machinery into a
| clean, conceptual framework.
| rochak wrote:
| Good. Keep on adding stuff to an already bloated language.
| logicchains wrote:
| Compile-time reflection will remove countless of lines of
| bloat from real C++ code bases due to eliminating the need to
| manually write formatting, hashing and serialisation
| implementations for classes.
| riwsky wrote:
| ...10 years from now
| fredrikholm wrote:
| If not more; the words "modern" and "C++11" are still
| used in conjunction despite the fact that 2014 was a long
| time ago.
| pjmlp wrote:
| All programming languages either die, or become bloated, as
| any software product.
|
| I bet a Fortran 77 developer will think the same of Fortran
| 2023, a COBOL 60 developer of COBOL 2023, a K&R C developer
| from C23, a 1975 Scheme developer from R7RS, a Python 1.0
| developer from Python 3.13,... even Go 1.0 developers from
| 1.24 with generics, generators,...
| tialaramex wrote:
| One trick that can help you here is Rust's Editions (and
| the proposed but never implemented C++ Epochs).
|
| This lets the language throw away bad ideas, without
| throwing away the code people wrote in the era when we
| didn't realise that's a bad idea.
| brabel wrote:
| The examples in thist post seem very similar to how you do
| metaprogramming in the D language. That has existed for years
| yet you rarely hear about that.
| pjmlp wrote:
| With a much better ecosystem, 40 years of IDE tooling,
| frameworks, OS support.
|
| Which is what people always forget when comparing language
| grammars.
| 9dev wrote:
| I'm not sure if you actually want to call "copy these header
| files into your local file system" an ecosystem at all. The
| last 40 years have brought _heaps_ of improvements in
| software development ergonomics. Zig is growing just fine, as
| other languages like Rust or Go.
| pjmlp wrote:
| That is what script kiddies do when using compiled
| languages.
|
| Rust has had an almost usable implementation for affine
| types, and being the second coming of Ada, to win the
| hearths of the industry, including all major OS vendors and
| hyperscallers.
|
| Go got lucky with Docker and Kubernetes rewrites, and their
| adoption across the industry.
|
| So far Zig is basically Modula-2 with C like syntax, and
| compile time execution, relies on the same tooling that C
| and C++ have had for decades for use-after-free, doesn't
| support a binary libraries ecosystem by design, and really
| Bun isn't going to be the killer project that triggers a
| Rewrite in Zig movement.
|
| It remains to be seen if Zig 1.0 happens, and how its
| adoption story at scale will be like.
| vkazanov wrote:
| I agree that zig is taking too long to be finalised. And
| rust has made certain questionable life choices.
|
| But people really, REALLY want to get off c and c++ for
| all the numerous reasons everybody knows.
| pjmlp wrote:
| They might want, yet while Khronos keeps publishing
| standards using C and C++,
| GCC/LLVM/CUDA/.NET/Java/V8/Metal make use of C++,
| Nintendo/PlayStation/XBox rely on C++, ... they are going
| to stay around no matter what.
| Ygg2 wrote:
| > I agree that zig is taking too long to be finalised.
| And rust has made certain questionable life choices.
|
| Any language that's older than 10 years is going to make
| questionable life choices. It's very easy to be Captain
| Hindsight, and ask why didn't you do X, 5 years ago? But
| adding feature X also makes another feature or property
| impossible, either via opportunity cost or
| features/properties being at odds.
|
| That said, what do you mean by questionable life choices?
| vkazanov wrote:
| > Any language that's older than 10 years..
|
| Yes, true. Every language has to make some early
| foundational choices, as few as possible, and try to
| carefully think about any new addition to the core
| because of the extra congnitive load that comes with it.
|
| Go is an extreme example here, leaning towards the
| conservative side. C as well. Zig. Not a fan of Java but
| it also is kinda slow to add things. Python used to be
| very careful as well but that epoch is gone.
|
| C++ is the opposite example. It tries to add as much as
| possible, and it was always the case. C compatibility!
| And classes! Templates! RAII! Metaprogramming! More of
| everything! Until it reached a point where it's
| unforgiving hard to add things. Or even learn it
| properly.
|
| Now, Rust feels like a C++ reimplementation, complete
| with a culture of adding as much as possible as quickly
| as possible, and ignoring the resulting cognitive load.
|
| I mean, it's a choice. Rust definitely has some great,
| even amazing, ideas to it. But I am afraid of thinking
| what the language will feel like in 10 years.
| Ygg2 wrote:
| > Now, Rust feels like a C++ reimplementation, complete
| with a culture of adding as much as possible as quickly
| as possible, and ignoring the resulting cognitive load.
|
| Ok, but I did ask for what specifically do you mean by
| questionable life choices? I feel Java is moving at a
| fast pace (and adding everything and the kitchen sink).
| Hence, why I wanted specific examples. Can you separate
| your feelings from facts, and see from where the feelings
| come from? I'm not saying you're wrong, I'm saying I want
| to understand your basis for that.
|
| > Go is an extreme example here, leaning towards the
| conservative side.
|
| Is it? Didn't it also start adding features that it swore
| not to add (generics)?
| rastignack wrote:
| > Bun isn't going to be the killer project that triggers
| a Rewrite in Zig movement.
|
| It might be enough to make the zig ecosystem viable. This
| along with tiger beetle (they have raised tens of
| millions).
|
| I think a lot of time is spent right now on the tooling,
| I hope that in a near feature the zig team will be able
| to switch to the event loop / standard library topics
| which really need love.
| flohofwoe wrote:
| There has been an awful amount of stagnation in those 40
| years though. The Visual Studio debugger is still the best
| overall debugger but has hardly advanced since the late 90s.
| While the Debug Adapter Protocol and Language Server Protocol
| invented by the VSCode team are not perfect, they both make
| debugger and 'Intellisense' support in IDEs for new languages
| quite trivial and easily get into the 80% 'good enough' area
| of what a 'proper' IDE offers.
| pjmlp wrote:
| It has advanced quite a lot, but people don't care to learn
| about debuggers are they care about low level details of
| their programming languages.
|
| Also it is quite telling that outside IDE friendly
| languages, debbugers are kind of stuck in the 80's, so no
| wonder that many think 80% of Visual Studio and friends is
| good enough.
| katzenversteher wrote:
| Reminds me of "Der Untergang". Mit dem Angriff C++26 wird das
| alles in Ordnung kommen!
| flohofwoe wrote:
| Interesting, I had the _exact_ same image in my mind :D
| LAC-Tech wrote:
| LOL
|
| Mein Fuehrer, Dieses feature... dieses feature wird von MVCC
| nicht eingebaut.
| flohofwoe wrote:
| C++ could do generic programming long before Zig even was an
| idea, but writing generic code in Zig is still much more
| straightforward - also Zig is usable now, while C++26 features
| will probably land in real world C++ compilers around 2036 ;)
| diimdeep wrote:
| Yeah it's good, and tech evangelism is just marketing.
| SMP-UX wrote:
| Zig is overall pretty good as a language and it does what it
| needs to: staying in the lane of the purpose is very important.
| It is why I do not particularly care for some languages being
| used just because.
| littlestymaar wrote:
| Comptime to replace macros is indeed good, comptime to replace
| generics on the other hand isn't and that really makes me think
| of the "when all you have is a hammer" quote.
| edflsafoiewq wrote:
| Why?
| bsder wrote:
| The problem is that the alternatives to comptime for generics
| generally seems to have a hideous effect on compile times (see:
| C++ and Rust).
|
| Is there a language that does generics in such a way that
| doesn't send compile times to the moon?
| edflsafoiewq wrote:
| Shouldn't comptime have the same compile time implications as
| templates? In both cases you're essentially recompiling the
| code for every distinct set of comptime args/template
| parameters.
| bsder wrote:
| Zig doesn't instantiate anything the doesn't get called.
| So, it doesn't have to generate a whole bunch of templated
| functions and then optimize down to the ones that actually
| get used.
|
| The upside is that if you only call a generic function with
| a u32, you don't instantiate an f32 as well. The downside
| is that when you do decide to call that function with an
| f32, all the comptime stuff suddenly gets compiled for the
| f32 and might have an error.
|
| In practice, I feel that I gain way more from the fast
| compile than I lose from having a path that accidentally
| never got compiled as my unit tests almost always force
| those paths to be compiled at least once.
| dwattttt wrote:
| > it doesn't have to generate a whole bunch of templated
| functions and then optimize down to the ones that
| actually get used.
|
| It's been a long time since I've dealt with templated
| C++, but I thought this was how C++ does it too.
|
| C++ will only generate functions for template parameters
| that are actually used, because it compiles a version of
| the templated function for each unique template
| parameters.
| anonymoushn wrote:
| C++ is at the very least less lazy than Zig. As an
| example, if you write some constexpr expression that
| evaluates a ternary and instantiates a function
| differently in the two prongs, both will be instantiated,
| even the one that doesn't end up in the final program.
| Yes, there are workarounds, but I didn't end up using
| them. I just moved the offending assert from compile time
| to runtime because this particular code was not that
| important.
| edflsafoiewq wrote:
| But the question is if that's actually decisive in the
| slow compilation problem. The solution in an eager
| language for evaluating too much stuff is basically "more
| if statements". Same thing in C++ metaprogramming, use
| more "if constexpr". If _that 's_ all it took to fix C++
| compile times, it would have been done a decade ago. The
| actual problem is all the stuff that you _do_ actually
| use that has to get repeatedly inlined and optimized away
| for zero cost abstraction to work.
| anonymoushn wrote:
| No, I don't think it is a big deal for compilation speed.
| vnorilo wrote:
| C++ monomorphises generics on demand too. That's why it
| can have errors specific to specialization and why
| template error messages spam long causal chains.
|
| C++ compile times are due to headers. Which in case of
| templates result in a lot of redundant work then
| deduplicated by the linker.
| hansvm wrote:
| Zig is lazy, and C++ is eager. I can define an infinite set
| of mutually recursive types in Zig, and only the ones I
| actually use will be instantiated (not an everyday need,
| but occasionally interesting -- I had fun building an
| autodiff package that way with no virtual function
| overhead, and the set of type descriptors being closed
| under VJP meant that you could support arbitrary (still
| only finite) derivative-like tensors, not just first and
| second order).
| pjmlp wrote:
| As proven by C++ with modules and binary libraries, compile
| times can be better in C++.
|
| Rust suffers because they compile everything from source, and
| the frontend sends piles of unprocessed LLVM IR to the
| traditional slow backend.
|
| This can be improved with better tooling, one example is the
| Cranelift backend, there could be an interpreter, and so on.
|
| Examples of languages that don't send compile times to the
| moon with similar polymorphic power, Standard ML, OCaml,
| Haskell, D, Ada.
| littlestymaar wrote:
| AFAIK Part of the problem with Rust is also that it
| compiles crates individually before linking them and
| because of that cannot use the upfront knowledge of what's
| going to be needed, and as such a generic function that
| crosses the crate boundary is going to be handled twice by
| the compiler.
|
| This was initially done so that Rust could compile things
| in parallel between crates by with spawning more rustc
| processes, which is obviously much easier than building a
| parallel compiler directly, but in the end it's suboptimal
| for performance.
| anonymoushn wrote:
| comptime for generics is a superset of the things that C++
| and Rust do for generics
| shikck200 wrote:
| Ocaml
| wtetzner wrote:
| OCaml doesn't monomorphize functions. Instead references to
| every type are the same size (either a tagged int or a
| pointer). This is a sweet spot for OCaml, but doesn't
| really work for a language that doesn't allocate everything
| on the heap.
| cyco130 wrote:
| It's a tradeoff. Advanced generic programming as implemented in
| many other languages requires you to learn a completely new
| language. That new language is better suited for some use
| cases. Functions that take types and return types, on the other
| hand, can be more intuitive in other cases.
| bryango wrote:
| I hope we can have something that combines the meta-programming
| capabilities of Zig with the vast ecosystem, community and safety
| of Rust.
|
| Looking at the language design, I really prefer Zig to Rust, but
| as an incompetent, amateur programmer, I couldn't write anything
| in Zig that's actually useful (or reliable), at least for now.
| norman784 wrote:
| I Agree. I tried briefly Zig and quickly gave up because, as
| someone used to Rust, the compiler wasn't helping me find those
| issues at compile time. I know that Zig doesn't make those
| promises, but for me, it's a deal breaker, so I suppose Zig
| isn't the language for me.
|
| On the other hand, I do like the concept of comptime vs Rust
| macros.
| raptorfactor wrote:
| Please keep the Rust community away from Zig. (I joke.
| Mostly...)
| Tiberium wrote:
| If you're surprised by Zig's comptime, you should definitely take
| a look at Nim which also has compile-time code evaluation, plus a
| full AST macro system.
| foretop_yardarm wrote:
| Nim is a fun language but I wouldn't consider it for "serious"
| work. It has the same issues as other niche languages (ie.
| ecosystem), plus: a polarising maintainer (most core
| contributors don't seem to last long) and primarily funded by a
| crypto company (if you care about that). Then again, 10 years
| ago none of that would have bothered me.
| planetis wrote:
| All these organizations[1] using nim in production must
| disagree with you then.
|
| [1]: https://github.com/nim-lang/Nim/wiki/Organizations-
| using-Nim
| zamalek wrote:
| Zig has the feature of not having exceptions. I see that Nim is
| trying to move away from them, but exceptions color functions,
| which means that you have to account for them even if you don't
| use functions that throw them[1]. Life is too short to deal
| with invisible control flow.
|
| [1]: https://github.com/status-im/nim-stew
| cb321 wrote:
| Whether you want to handle every error is context dependent.
| StatusIM has long running servers & clients as primary
| products and so tilt away from exceptions, but for a CLI
| utility you might want the convenience of a stack trace
| instead. I've seen this many times in Python CL apps, for
| example.
|
| Alternatively, there is also a Nim effects tracking system
| that lets the compiler help you track the hidden control flow
| for you. So, at the top of a module, you can say {.push
| raises: [].} to make sure that you handled all exceptions
| somewhere. So, it may not be as "Wild West" as other
| exceptions systems that you are used to.
|
| As with so many aspects, Nim is Choice.
| pjmlp wrote:
| While interesting, this is one of the cases, where I agree with
| "D did it first" kind of comments.
| sixthDot wrote:
| sure, and hygienically, it's not a preprocessor thing.
| noelwelsh wrote:
| It would be nice to have a more indepth discussion of the issues
| that have been found with compile-time programming, rather than
| uncritical acclaim. Staged programming is not new, and people
| have run into many issues and design tradeoffs in that time.
| (E.g. the same stuff has been done in Lisps for decades, though
| most Lisps don't have a type system, which makes things a bit
| more complicated.)
|
| Some of the issues that come to mind:
|
| * Implementing generics in this way breaks parametricity. Simply
| put, parametricity means being able to reason about functions
| just from their type signature. You can't do this when the
| function can do arbitrary computation based on the concrete type
| a generic type is instantiated with.
|
| * It's not clear to me how Zig handles recursive generic types.
| Generally, type systems are lazy to allow recursion. So I can
| write something like
|
| type Example = Something[Example]
|
| (Yes, this is useful.)
|
| * Type checking and compile-time computation can interact in
| interesting ways. Does type checking take place before compile-
| time code runs, after it runs, or can they be interleaved?
| Different choices give different trade-offs. It's not clear to me
| what Zig does and hence what tradeoffs it makes.
|
| * The article suggests that compile-time code can generate code
| (not just values) but doesn't discuss hygiene.
|
| There is a good discussion of some issues here:
| https://typesanitizer.com/blog/zig-generics.html
| flohofwoe wrote:
| I think most of those points one only stumbles over after a few
| thousand lines of Zig and going really deep into the comptime
| features.
|
| And some features in your list are of questionable value IMHO
| (e.g. the "reasoning over a function type signature" - Rust
| could be a much more ergonomic language if the compiler
| wouldn't have to rely on function signatures alone but instead
| could peek into called function bodies).
|
| There are definitely some tradeoffs in Zig's comptime system,
| but I think the more important point is that nothing about it
| is surprising when working with it, it's only when coming from
| languages like Rust or C++ where Zig's comptime, generics and
| reflection might look 'weird'.
| eptcyka wrote:
| > Rust could be a much more ergonomic language if the
| compiler wouldn't have to rely on function signatures alone
| but instead could peek into called function bodies
|
| This path leads to unbounded runtime for the type
| checker/borrow checker. We're not happy about build times as
| is.
| littlestymaar wrote:
| And also lead to subtle errors for users of crates. Semver
| is hard enough to do when the compiler only deals with
| signatures, but if the body of the functions now contain
| invariants that can be relied on by function callers then
| it becomes intractable.
| lmm wrote:
| > most of those points one only stumbles over after a few
| thousand lines of Zig and going really deep into the comptime
| features.
|
| > nothing about it is surprising when working with it
|
| I think there's a contradiction here - when you get deep into
| using this kind of feature in a complex way is precisely when
| you most need it to behave consistently, and tends to be
| where this kind of ad-hoc approach breaks down.
| throwawaymaths wrote:
| if youve never gone deep into zig comptime... really
| nothing is surprising, except possibly when you _can 't_ do
| things and usually after a bit of thinking about it you
| understand why you can't.
| marhee wrote:
| Interesting points.
|
| > Implementing generics in this way breaks parametricity.
| Simply put, parametricity means being able to reason about
| functions just from their type signature. You can't do this
| when the function can do arbitrary computation based on the
| concrete type a generic type is instantiated with.
|
| Do you mean reasoning about a function in the sense of just
| understanding what a functions does (or can do), i.e. in the
| view of the practical programmer, or reasoning about the
| function in a typed theoretical system (e.g. typed lambda
| calculus or maybe even more exotic)? Or maybe a bit of both?
| There is certainly a concern from the theoretical viewpoint but
| how important is that for a practical programming language?
|
| For example, I believe C++ template programming also breaks
| "parametricity" by supporting template specialisation. While
| there are many mundane issues with C++ templates, breaking
| parametricity is not a very big deal in practice. In contrast,
| it enables optimisations that are not otherwise possible (for
| templates). Consider for example std::vector<bool>:
| implementations can be made that actually store a single bit
| per vector element (instead of how a bool normally is
| represented using an int or char). Maybe this is even required
| by the standard, I don't recall. My point is that in makes
| sense for C++ to allow this, I think.
| SkiFire13 wrote:
| > For example, I believe C++ template programming also breaks
| "parametricity" by supporting template specialisation.
|
| C++ breaks parametricity even with normal templates, since
| you can e.g. call a method that exists/is valid only on some
| instantiations of the template.
|
| The issue is that the compiler can't help you check whether
| your template type checks or not, you will only figure out
| when you instantiate it with a concrete type. Things get
| worse when you call a templated function from within another
| templated function, since the error can then be arbitrarily
| levels deep.
|
| > My point is that in makes sense for C++ to allow this, I
| think.
|
| Whether it makes sense or not it's a big pain point and some
| are trying to move away from it (see e.g. Carbon's approach
| to generics)
| marhee wrote:
| > C++ breaks parametricity even with normal templates
|
| I might be wrong here, but as I understand it
| "parametricity" means loosely that all instantiations use
| the same function body. To quote wikipedia:
|
| "parametricity is an abstract uniformity property enjoyed
| by parametrically polymorphic functions, which captures the
| intuition that all instances of a polymorphic function act
| the same way"
|
| In this view, C++ does not break parametricity with
| "normal" (i.e. non-specialised) templates. Of course, C++
| does not type check a template body against its parameters
| (unless concepts/trairs are used), leading to the problems
| you describe, but it's a different thing as far as I
| understand.
| jcelerier wrote:
| Hm, wouldn't any use of if constexpr break that
| definition?
|
| e.g. template<typename T>
| void f() { if constexpr (is_int<T>) { return
| 0; } else ...
| deredede wrote:
| Parametricity is about behavior, not code. A function
| parametric in a variable should bevave identically for
| all values of the variable. If one instance of a C++
| template fails to compile and another instance of the
| same template does compile it is a stretch to say they
| behave identically, even though they use the same code.
| lmm wrote:
| To be parametric it needs to be the same body
| semantically, not just textually. Particularly in C++
| with its heavy operator overloading and limited safety,
| you can very easily write a template whose body will do
| the right thing for some types and be undefined behaviour
| for others (e.g. if your template has comparisons in it
| and then you instantiate it with a pointer or something).
| Quekid5 wrote:
| > Consider for example std::vector<bool>: implementations can
| be made that actually store a single bit per vector element
| (instead of how a bool normally is represented using an int
| or char).
|
| Your example is considered a misfeature and demonstrates why
| breaking parametricity is a problem: the specialized
| vector<bool> is not a standard STL container even though
| vector<anythingelse> is. That's at best confusing -- and can
| leads to very confusing problems in generic code. (In this
| specific case, C++11's "auto" and AAA lessens some of the
| issues, but even then it can cause hard-to-diagnose
| performance problems even when the code compiles)
|
| See https://stackoverflow.com/a/17797560 for more details.
| beached_whale wrote:
| Vector bool may not have to store a range of space
| optimized bool values but the interface is still different
| enough and guarantees different enough that is is largely
| thought of as a mistake. For one the const reference type
| is bool and not bool const &. Plus other members like
| flip... mostly the issue is in generic code expecting a
| normal vector
| HelloNurse wrote:
| The C++ vector<bool> specialization is bad because breaking
| many important _implicit_ contracts about taking the
| address of vector <> elements makes it practically unusable
| if a normal vector<> is expected, but it isn't specialized
| incorrectly in a formally meaningful sense: all errors are
| outside the class (unsatisfied expectations from client
| code) and implicit (particularly for C++ as it was at the
| time).
| Quekid5 wrote:
| You're not wrong, but is at the very least _weird_ that a
| specialization doesn 't conform to the concept that the
| general template does. Something which proper
| parametricity _would_ have avoided -- if it were
| available.
|
| (The Hysterical Raisins are understandable, esp. given
| that it wasn't even possible to specify Concepts in C++
| until 2020...)
| HelloNurse wrote:
| The point is exactly that the "concept" of what the
| template should do is informal and a careful, detailed
| specification in a far more expressive language than
| vintage C++ would be needed to elicit good compilation
| errors from something like vector<bool>.
|
| Proper parametricity is only a starting point: types that
| specify alignment, equality and lifetimes would be needed
| to make it useful.
| nialv7 wrote:
| one thing you can reason about a function is: does it exist
| at all? if you don't have parametricity, you can't even be
| sure about that. in Rust, as long as your type satisfies a
| generic function's bounds, you can be sure instantiating that
| function with this type will compile; in C++ you don't have
| that luxury.
| noelwelsh wrote:
| In terms of implementation, you can view parametricity as
| meaning that within the body of a function with a generic
| type, the only operations that can be applied to values of
| that type are also arguments to that function.
|
| This means you cannot write
|
| fn sort<A>(elts: Vec<A>): Vec<A>
|
| because you cannot compare values of type A within the
| implementation of sort with this definition. You can write
|
| fn sort<A>(elts: Vec<A>, lessThan: (A, A) -> Bool): Vec<A>
|
| because a comparison function is now a parameter to sort.
|
| This helps both the programmer and the compiler. The
| practical upshot is that functions are modular: they specify
| everything they require. It follows from this that if you can
| compile a call to a function there is a subset of errors that
| cannot occur.
|
| In a language without parametricity, functions can work with
| only a subset of possible calls. If we take the first
| definition of sort, it means a call to sort could fail at
| compile-time, or worse, at run-time, because the body of the
| function doesn't have a case that knows how to compare
| elements of that particular type. This leads to a language
| that is full of special cases and arbitrary decisions.
|
| Javascript / Typescript is an example of a language without
| parametricity. sort in Javascript has what are, to me, insane
| semantics: converting values to strings and comparing them
| lexicographically. (See https://developer.mozilla.org/en-
| US/docs/Web/JavaScript/Refe...) This in turn can lead to
| amazing bugs, which are only prevented by the programmer
| remembering to do the right thing. Remembering to do the
| right thing is fine in the small but it doesn't scale.
|
| Breaking parametricity definitely has uses. The question
| becomes one about the tradeoffs one makes. That's why I'd
| rather have a discussion about those tradeoffs than just
| "constime good" or "parametricity good". Better yet are neat
| ideas that capture the good parts of both. (E.g. type classes
| / implicit parameters reduce the notational overhead of
| calling functions with constrained generic types, but this
| bring their own tradeoffs around modularity and coherence.)
| dmvdoug wrote:
| Do you have a blog or other site where you post your
| writing? Your explanations are quite good and easy to
| follow for someone like me, an interested/curious onlooker.
| noelwelsh wrote:
| Thanks! I appreciate that. A few things:
|
| - https://scalawithcats.com/ is a book I'm writing. There
| is an associated newsletter to which I post blog articles
| and the like.
|
| - https://noelwelsh.com/ is my personal site, which hosts
| my blog.
| norir wrote:
| Fair point about parametricity. A language could in the
| macro expansion do the equivalent of a scala implicit
| lookup for a sorting function for the type and return an
| error at macro expansion time if it can't find one. That
| avoids the doing the right thing requires discipline
| problem but I agree it is still less clear from the type
| signature alone what the type requirements are.
| nimish wrote:
| This is _also_ doable with the ability to constrain
| generics.
|
| sort<A> where A implements Comparable
|
| Simpler explanation IMO.
| tines wrote:
| Not really an "also", "implements" is just syntax sugar
| for what the GP is saying.
| nimish wrote:
| Sure, but it's the same thing in 10x fewer words. Having
| parametric generic types that accept constraints allows
| your functions to have the best of all worlds.
|
| So a language _with_ that is superior. All zig needs to
| do is add some way to allow for constraints.
| James_K wrote:
| Functions can crash anyway. I don't see how what you
| describe is different from a function on integers that
| errors on inputs that are too big. The programmer has to
| actively choose to make function break parametricity, and
| they can equally chose not to do that.
| josephg wrote:
| In an ideal world, a function that only works for small
| integers would bake that into its type system. Ie, rather
| than accepting "any integer", the function would accept a
| u8, or a value compile-time guaranteed in the range of
| 0..10 or something.
|
| Your point still stands though. Modern programming
| languages don't constrain you much at all with their type
| systems.
|
| I spent a little time in Haskell a few years ago and it
| the kind of reasoning you can do about functions is wild.
| Eg, if a function has the type signature of A -> A, we
| know the function has to be the identity function because
| that's the only function that matches the type signature.
| (Well that or the "bottom function", which loops
| forever). Lots of functions are like that - where the
| type definitions alone are enough to reason about a lot
| of code.
| medo-bear wrote:
| > Lisps don't have a type system, which makes things a bit more
| complicated
|
| SBCL, which is a very popular Common Lisp implementation, is
| indeed strongly typed. Coalton, which is an addon, is even
| statically typed
| gjm11 wrote:
| medo-bear, you might want to know that you appear to be
| shadowbanned -- all your comments for the last ~10 days are
| dead. (I have some plausible guesses at what provoked this,
| though I have to say that I didn't see anything that looks to
| _me_ like sufficient justification.)
|
| (I "vouched" for your comment to which this is a reply, and
| that seems to have been sufficient to un-dead it. Unless the
| system's set up so that vouching for a shadowbanned comment
| un-deads it _only for the person who does the vouching_ , I
| guess...)
| medo-bear wrote:
| Thanks
| anonymoushn wrote:
| > type Example = Something[Example]
|
| You can't use the binding early like this, but inside of the
| type definition you can use the @This() builtin to get a value
| that's the type you're in, and you can presumably do whatever
| you like with it.
|
| The type system barely does anything, so it's not very
| interesting when type checking runs. comptime code is type
| checked and executed. Normal code is typechecked and not
| executed.
|
| comptime is not a macro system. It doesn't have the ability to
| be unhygienic. It can cleverly monomorphize code, or it can
| unroll code, or it can omit code, but I don't think it can
| generate code.
| shakna wrote:
| Until version 0.12.0 (April 2024), you could make arbitrary
| syscalls, allowing you to generate code at comptime, and
| promote vars between comptime and runtime. [0] Before then,
| you could do some rather funky things with pointers and
| memory, and was very much not hygienic.
|
| [0] https://ziglang.org/download/0.12.0/release-
| notes.html#Compt...
| ScottRedig wrote:
| Hi, article author here. I was motivated to write this post
| after having trouble articulating some of its points while at a
| meetup, so that's why the goal of this post was focused on
| explaining things, and not being critical.
|
| So at least address your points here:
|
| * I do agree this is a direct trade-off with Zig style
| comptime, versus more statically defined function signatures. I
| don't think this affects all code, only code which does such
| reasoning with types, so it's a trade-off between reasoning and
| expressivity that you can make depending on your needs. On the
| other hand, per the post's view 0, I have found that just going
| in and reading the source code easily answers the questions I
| have when the type signature doesn't. I don't think I've ever
| been confused about how to use something for more than the time
| it takes to read a few dozen lines of code.
|
| * Your specific example for recursive generic types poses a
| problem because a name being used in the declaration causes a
| "dependency loop detected" error. There are ways around this.
| The generics example in the post for example references itself.
| If you had a concrete example showing a case where this does
| something, I could perhaps show you the zig code that does it.
|
| * Type checking happens during comptime. Eg, this code:
| pub fn main() void { @compileLog("Hi");
| const a: u32 = "42"; _ = a;
| @compileLog("Bye"); }
|
| Gives this error: when_typecheck.zig:3:17:
| error: expected type 'u32', found '*const [2:0]u8' const
| a: u32 = "42"; ^~~~ Compile Log
| Output: @as(*const [2:0]u8, "Hi")
|
| So the first @compileLog statement was run by comptime, but
| then the type check error stopped it from continuing to the
| second @compileLog statement. If you dig into the Zig issues,
| there are some subtle ways the type checking between comptime
| and runtime can cause problems. However it takes some pretty
| esoteric code to hit them, and they're easily resolved. Also,
| they're well known by the core team and I expect them to be
| addressed before 1.0.
|
| * I'm not sure what you mean by hygiene, can you elaborate?
| mananaysiempre wrote:
| "Hygiene" in the context of macro systems refers to the
| user's code and the macro's inserted code being unable to
| capture each other's variables (either at all or without
| explicit action on part of the macro author). If, say, you're
| writing a macro and your generated code declares a variable
| called 'x' for its own purposes, you most probably don't want
| that variable to interfere with a chunk of user's code you
| received that uses an 'x' from an enclosing scope, even if
| naively the user's 'x' is shadowed by the macro's 'x' at the
| insertion point of the chunk.
|
| It's possible but tedious and error-prone to avoid this
| problem by hand by generating unique identifier names for all
| macro-defined runtime variables (this usually goes by the
| Lisp name GENSYM). But what you actually want, arguably, is
| an extended notion of lexical scope where it also applies to
| the macro's text and macro user's program as written instead
| of the macroexpanded output, so the macro's and user's
| variables can't interfere with each other simply because they
| appear in completely different places of the program--again,
| as written, not as macroexpanded. That's possible to
| implement, and many Scheme implementations do it for example,
| but it's tricky. And it becomes less clear-cut what this even
| means when the macro is allowed to peer into the user's code
| and change pieces inside.
|
| (Sorry for the lack of examples; I don't know enough to write
| one in Zig, and I'm not sure giving one in Scheme would be
| helpful.)
| Validark wrote:
| Zig disallows ALL shadowing (basically variable name
| collisions where in the absence of the second variable
| declaration the first declaration would be reachable by the
| same identifier name).
|
| Generating a text file via a writer with the intent to
| compile it as source code is no worse in Zig than it is in
| any other language out there. If that's what you want to do
| with your life, go ahead.
| throwawaymaths wrote:
| zig comptime is not a macro system and you can't really
| generate code in a way that makes hygeine a thing to worry
| about (there is no ast manipulation, you can't "create
| variables"). the only sort of codegen you can do is via
| explicit conditionals (switch, if) or loops conditioned on
| compile time accessible values.
|
| thats still powerful, you could probably build a compile
| time ABNF parser, for example.
| gotoeleven wrote:
| Surely there's a way to generate code by manipulating an
| AST structure? Is there some reason this can't be done in
| Zig or is it just that no one has bothered?
|
| Doing it this way is more verbose but sidesteps all
| hygiene issues.
| Cloudef wrote:
| Generating code during comptime is explicitly forbidden
| by the author. You can still generate code during
| build.zig of course.
| withoutboats3 wrote:
| 100%. So tiring that the discourse around this is based on 15
| minute demos and not actual understandings of the trade offs.
| Varun Gandhi's post that you link to is great.
|
| Based on my experience with Rust, a lot of what people want to
| do with its "constant generics" probably would be easier to do
| with a feature like comptime. Letting you do math on constant
| generics while maintaining parametricity is hard to implement,
| and when all you really want is "a trait for a hash function
| with an output size of N," probably giving up parametricity for
| that purpose and generating the trait from N as an earlier
| codegen step is fine for you, but Rust's macros are too
| flexible and annoying for doing it that way. But as soon as you
| replace parametric polymorphism with a naive code generation
| feature, you're in for a world of hurt.
| miki123211 wrote:
| And I would add:
|
| * Documentation. In a sufficiently-powerful comptime system,
| you can write a function that takes in a path to a .proto file
| and returns the types defined in that file. How should this
| function be documented? What happens when you click a reference
| to such a generated type in the documentation viewer?
|
| * IDE autocompletions, go to definition, type hinting etc. A
| similar problem, especially when you're working on some half-
| written code and actual compilation isn't possible yet.
| adonovan wrote:
| Also: security. Does this feature imply that merely building
| someone else's program executes their code on your machine?
| mpalmer wrote:
| Syscalls aren't available to comptime code
| actionfromafar wrote:
| Also see e.g.
|
| https://github.com/rust-lang/rust-analyzer/issues/14375
| badsectoracula wrote:
| Considering that pretty much every non-toy project isn't
| built by directly calling the compiler but through build
| tools like make, cmake, autotools, etc or even scripts like
| `build.sh` that can call arbitrary commands and that even
| IDEs have functionality to let you call arbitrary commands
| before and after builds (and had since the 90s at least), i
| do not see this as a realistic concern worth of limiting a
| language's functionality.
| MathMonkeyMan wrote:
| Scheme has a "hygienic" macro system that allows you to do
| arbitrary computation and code alteration at compile time.
|
| The language doesn't see wide adoption in industry, so maybe
| its most important lessons have yet to be learned, but one
| problem with meta-programming is that it turns part of your
| program into a compiler.
|
| This happens to an extent in every language. When you're
| writing a library, you're solving the problem "I want users to
| be able to write THIS and have it be the same as if they had
| written THAT." A compiler. Meta-programming facilities just
| expand how different THIS and THAT can be.
|
| Understanding compilers is hard. So, that's at least one
| potential issue with compile-time programming.
| Validark wrote:
| By your definition practically any code is a compiler unless
| you literally typed out every individual thing the machine
| should do, one by one.
|
| "Understanding compilers is hard."
|
| I think this is just unnecessarily pessimistic or embracing
| incompetence as the norm. It's really not hard to understand
| the concept of an "inline" loop. And so what if I do write a
| compiler so that when I do `print("%d", x)` it just gives me
| a piece of code that converts `x` to a "digit" number and
| doesn't include float handling? That's not hard to
| understand.
| jmull wrote:
| > being able to reason about functions just from their type
| signature.
|
| This has nothing to do with compile-time execution, though. You
| can reason about a function from its declaration if it has a
| clear logical purpose, is well named, and has well named
| parameters. You can consider any part of a parameter the
| programmer can specify as part of the name, including label,
| type name, etc.
|
| > There is a good discussion of some issues here:
| https://typesanitizer.com/blog/zig-generics.html
|
| That's actually not a great article. While I agree with the
| conclusion stated in the title, it's a kind of "debate team"
| approach to argumentation which tries to win points rather than
| make meaningful arguments.
|
| The better way to frame the debate is flexibility vs
| complexity. A fixed function generics system in a language is
| simpler (if well designed) than a programmable one, but less
| flexible. The more flexibility you give a generics system, the
| more complex it becomes, and the closer it becomes to a
| programming language in its own right. The nice thing about
| zig's approach is that the meta-programming language is
| practically the same thing as the regular programming language
| (which, itself, is a simple language). That minimizes the
| incremental complexity cost.
|
| It does introduce an extra complexity though: it's harder for
| the programmer to keep straight what code is executing at
| compile time vs runtime because the code is interleaved and the
| context clues are minimal. I wonder if a "comptime shader"
| could be added to the language server/editor plugin that puts a
| different background color on comptime code.
| jasode wrote:
| _> You can _reason_ about a function from its declaration if
| it has a clear logical purpose, is well named, and has well
| named parameters._
|
| I think _" reason"_ in gp's context is _" compile-time
| reasoning"_ as in the compiler's _deterministic algorithm_ to
| parse the code and assign properties etc. This has downstream
| effects with generating compiler errors, etc.
|
| It's not about the human programmer's ability to reason so
| any "improved naming" of function names or parameters still
| won't help the compiler out because it's still just an
| arbitrary "symbol" in the eyes of the parser.
| jmull wrote:
| Downstream effects with generating compiler errors is still
| about the human programmer's ability to reason about the
| code, and error messages can only reference the identifier
| names provided.
|
| The compiler doesn't do anything you, the programmer, don't
| tell it to do. You tell it what to do by writing code using
| a certain syntax, connecting identifiers, keywords, and
| symbols. That's it. If the meaning isn't in the identifiers
| you provide and how you connect them together with keywords
| and symbols, it isn't in there at all. The compiler doesn't
| care what identifier names you use, but that's true whether
| the identifier is for a parameter label, type name,
| function name or any other kind of name. The programmer
| gives those meaning to human readers by choosing meaningful
| names.
|
| Anyway, zig's compile errors seem OK to me so far.
|
| Actually, the zig comptime programmer can do better than a
| non-programmable compiler when it comes to error messages.
| You can detect arbitrary logical errors and provide your
| own compiler error messages.
| noelwelsh wrote:
| I elaborated on parametricity in this comment:
| https://news.ycombinator.com/item?id=42621239
|
| There are many ways one can reason about functions, and I
| think all of us use multiple methods. Parametricity provides
| one way to do so. One nice feature is that its supported by
| the programming language, unlike, say, names.
| jmull wrote:
| I saw that. But I don't think it has bearing on zig
| comptime.
|
| zig generates a compile error when you try to pass a non-
| conforming type to a generic function that places
| conditions/restrictions on that type (such as by calling a
| certain predicate on instances of that type).
|
| It's probably important to note that parametricity is a
| property of specific solution spaces, and not really in the
| ultimate problem domain (writing correct and reliable
| software for specific contexts), so isn't necessarily
| meaningful here.
| lmm wrote:
| > zig generates a compile error when you try to pass a
| non-conforming type to a generic function that places
| conditions/restrictions on that type (such as by calling
| a certain predicate on instances of that type).
|
| Sure, but only after it's fully expanded, which is much
| harder to debug. And if a generic function doesn't fail
| to compile but rather silently behaves differently (e.g.
| if it calls a function that behaves unexpectedly, but
| still exists, on the type in question) then you don't get
| an error at all.
|
| > parametricity is a property of specific solution
| spaces, and not really in the ultimate problem domain
| (writing correct and reliable software for specific
| contexts)
|
| Nonsense. Without parametricity your software is not
| compositional and it becomes impossible to write correct
| software to solve complex problems.
| jmull wrote:
| > Sure, but only after it's fully expanded,
|
| Code goes into the compiler. Either compiled code or
| errors come out. There's no partial expansion step to
| cause confusion.
|
| You're probably referring to something about the
| flexibility zig's comptime allows, but it's important to
| note a zig programmer can be as picky as they want about
| what types a generic function will accept. People are
| really just talking about what the syntax for expression
| type restrictions is.
|
| > Without parametricity your software is not
| compositional and it becomes impossible to write correct
| software to solve complex problems.
|
| You can hold that opinion, but it's not a fact. The
| overall question isn't binary. It's one of balancing
| complexity and flexibility. A fixed system for specifying
| type restrictions is simpler and provides fewer
| opportunities for mistakes (assuming it's well designed),
| and may have parametricity. However, the lack of
| flexibility can just push the complexity elsewhere, e.g.,
| leading to convoluted usage patterns, which could lead to
| more mistakes. A programmable system for specifying type
| restrictions offers more flexibility at the cost of more
| up-front complexity, but in a well-designed system the
| flexibility could lead to less overall complexity, and
| fewer mistakes. A nice thing about zig's approach is that
| the generics metaprogramming language is pretty much the
| same as the regular language, which mitigates the
| increase in complexity. I actually think it should be
| possible to create some kind of generics system that
| could credibly be said to be programmable and have
| parametricity, though I don't think there's any point to
| doing so.
| MatthiasPortzel wrote:
| I'm a pretty big fan of Zig--I've been following it and writing
| it on-and-off for a couple of years. I think that comptime has
| a couple of use-cases where it is very cool. Generics,
| initializing complex data-structures at compile-time, and
| target-specific code-generation are the big three where
| comptime shines.
|
| However, in other situations seeing "comptime" in Zig code has
| makes me go "oh no" because, like Lisp macros, it's very easy
| to use comptime to avoid a problem that doesn't exist or
| wouldn't exist if you structured other parts of your code
| better. For example, the OP's example of iterating the fields
| of a struct to sum the values is unfortunately characteristic
| of how people use comptime in the wild--when they would often
| be better served by using a data-structure that is actually
| iterable (e.g. std.enums.EnumArray).
| bunderbunder wrote:
| This feels like it's it's a constant problem with all more
| advanced language features. I've had the same reaction to
| uses of lisp macros, C-style macros, Java compiler
| extensions, Ruby's method_missing, Python monkey patching,
| JavaScript prototype inheritance, monads, inheritance...
|
| Maybe the real WTF is the friends we made along the way. <3
| <3 <3
| arccy wrote:
| this is why Go is so great....
| bunderbunder wrote:
| Go is not immune. See: interface pollution.
| PaulHoule wrote:
| Lately I read Graham's _On Lisp_ and first felt it was one
| the greatest programming books I 'd ever read and felt it was
| so close to perfect that the little things like he made me
| look "nconc" up in the CL manual (so far he'd introduced
| everything he talked about) made me want to go through and do
| just a little editing. And his explanation of how
| continuations work isn't very clear to me which is a problem
| because I can't find a better one online (the only way I
| think I'll understand continuations is if I write the
| explanation I want to read)
|
| Then I start thinking things like: "if he was using Clojure
| he wouldn't be having the problems with nconc that he talks
| about" and "I can work most of the examples in Python because
| the magic is mostly in functions, not in the macros" and "I'm
| disappointed that he doesn't do anything that really
| transform the tree"
|
| (It's still a great book that's worth reading but anything
| about Lisp has to be seen in the context the world has moved
| on... Almost every example in
| https://www.amazon.com/Paradigms-Artificial-Intelligence-
| Pro... can be easily coded up in Python because it was the
| garbage collection, hashtables on your fingertips, first
| class functions that changed the world, not the parens)
|
| Lately I've been thinking about the gradient from the various
| tricks such as internal DSLs and simple forms of
| metaprogramming which are weak beer compared to what you can
| do if you know how compilers work.
| lispm wrote:
| > if he was using Clojure he wouldn't be having the
| problems with nconc that he talks about"
|
| Yeah, one would write the implementation in Java.
|
| Common Lisp (and Lisp in general) often aspires to be
| written in itself, efficiently. Thus it has all the
| operations, which a hosted language may get from the
| imperative/mutable/object-oriented language underneath.
| That's why CL implementations may have type declarations,
| type inference, various optimizations, stack allocation,
| TCO and other features - directly in the language
| implementation. See for example the SBCL manual.
| https://sbcl.org/manual/index.html
|
| For example the SBCL implementation is largely written in
| itself, whereas Clojure runs on top of a virtual machine
| written in a few zillion lines of C/C++ and Java. Even the
| core compiler is written in 10KLOC of Java code. https://gi
| thub.com/clojure/clojure/blob/master/src/jvm/cloju...
|
| Where the SBCL compiler is largely written Common Lisp,
| incl. the machine code backends for various platforms.
| https://github.com/sbcl/sbcl/tree/master/src/compiler
|
| The original Clojure developer made the conscious decision
| to inherit the JIT compiler from the JVM, write the Clojure
| compiler in Java and reuse the JVM in general -> this
| reuses a lot of technology maintained by others and makes
| integration into the Java ecosystem easier.
|
| The language implementations differ: Lots of CL + C and
| Assembler compared to a much smaller amount of Clojure with
| lots of Java and C/C++.
|
| CL has for a reason a lot of low-level, mutable and
| imperative features. It was designed for that, so that
| people code write efficient software largely in Lisp
| itself.
| eddd-ddde wrote:
| C++ has so many complexities like SFINAE that I wouldn't say
| you are "able to reason about functions just from their type
| signature".
| hansvm wrote:
| > parametricity
|
| That feels like the wrong word for the thing you're describing.
| Linguistic arguments aside, yes, you're absolutely right.
|
| In Zig though, that issue is completely orthogonal to generics.
| The first implementation `foo` is the "only" option available
| for "truly arbitrary" `T` if you don't magic up some extra
| information from somewhere. The second implementation `bar`
| uses an extra language feature unrelated to generics to return
| a different valid value (it's valid so long as the result of
| `bar(T, x)` is never accessed). The third option `baz` works on
| any type with non-zero width and just clobbers some data for
| fun (you could golf it some more, but I think the 5-line
| implementation makes it easier to read for non-Zig
| programmers).
|
| Notice that we haven't performed a computation with `T` and
| were still able to do things that particular definition of
| parametricity would not approve of. fn foo(T:
| type, x: T) T { return x; } fn
| bar(T: type, x: T) T { _ = x; return
| undefined; } fn baz(T: type, x: T) T {
| var result: T = x; const result_ptr: *T = &result;
| const dangerous_shenanigans_ptr: *u8 = @ptrCast(result_ptr);
| dangerous_shenanigans_ptr.* = 42; return result;
| }
|
| Zig does give up that particular property (being able to rely
| on just a type signature to understand what's going on). Its
| model is closer to "compile-time duck-typing." The constraints
| on `T` aren't an explicitly enumerated list of constraints;
| they're an in vivo set of properties the code using `T`
| actually requires.
|
| That fact is extremely annoying from time to time (e.g., for
| one or two major releases the reference Reader/Writer didn't
| include the full set of methods, but all functions using
| readers and writers just took in an `anytype`, so implementers
| either had to read a lot of source or play a game of whack-a-
| mole with the compiler errors to find the true interface), but
| for most code it's really not hard to handle.
|
| E.g., if you've seen the `Iterator` pattern once, the following
| isn't all that hard to understand. Your constraints on `It` are
| that it tell you what the return type is, that return type
| ought to be some sort of non-comptime numeric, and it should
| have a `fn next(self: *It) ?T` method whose return values after
| the first `null` you're allowed to ignore. If you violate any
| of those constraints (except, perhaps, the last one -- maybe
| your iterator chooses to return null and then a few more
| values) then the code will fail at comptime. If you're afraid
| of excessive compiler error message lengths, you can use
| `@compileError()` to create a friendlier message documenting
| your constraints.
|
| It's a different pattern from what you're describing, but it's
| absolutely not hard to use correctly. fn
| sum(It: type, it: *It) It.T { var total: T = 0;
| while (it.next()) |item| total += item;
| return total; }
|
| > recursive generics
|
| A decent mental model (most of which follows from "view 4" in
| TFA, where the runtime code is the residue after the
| interpreter resolves everything it can at comptime) is treating
| types as immutable and treating comptime evaluation like an
| interpreted language.
|
| With that view, `type Example = Something[Example]` can't work
| because `Example` must be fully defined before you can pass it
| into `Something`. The laziness you see in ordinary non-generic
| type instantiations doesn't cross function boundaries. I'm not
| sure if there's a feature request for that (nothing obvious is
| standing out), but I'd be a fan @AndyKelley if you're
| interested.
|
| In terms of that causing problems IRL, it's only been annoying
| a few times in the last few years for me. The most recent one
| involved some comptime parser combinators, and there was a
| recursive message structure I wanted to handle. I worked around
| it by creating a concrete `FooParser` type with its associated
| manually implemented `parse` function (which itself was able to
| mostly call into rather than re-implement other parsers)
| instead of building up `FooParser` using combinators, so that
| the normal type instantiation laziness would work without
| issues.
|
| > when does type checking run
|
| Type inference is simplistic enough that this is almost a non-
| issue in Zig, aside from the normal tradeoffs from limited type
| inference (last I checked, they plan to keep it that way
| because it's not very important to them, it actively hinders
| the goal of being able to understand code by looking at a local
| snapshot, and that sort of complexity and constraint might keep
| the project from hitting more important goals like incremental
| compilation and binary editing). They are interleaved though
| (at least in the observable behavior, if you treat comptime
| execution as an interpreter).
| bilekas wrote:
| I see a lot of people in the comments basically saying "well X
| did it first" and that it's not worth talking about. This missed
| the point for me, zig is an interesting one personally and not
| out of semantics of std lib or anything really, it's just
| something nice to play around with so far. I think with the above
| attitude we probably could have stopped systems programming at
| c++, that wouldn't be too fun at all, what we all do without java
| to laugh at?
| sixthDot wrote:
| Just admit that discussions about proglangs are just so
| delicate. They turn people mad. End of the story.
| xeonmc wrote:
| > In the beginning, computers were invented. This has made
| lots of people very angry and was generally considered a bad
| move.
| koe123 wrote:
| Another interesting pattern is the ability to generate structs at
| compile time.
|
| Ive ran experiments where a neural net is implemented by creating
| a json file from pytorch, reading it in using @embedFile, and
| generating the subsequent a struct with a specific "run" method.
|
| This in theory allows the compiler to optimize the neural network
| directly (I havent proven a great benefit from this though). Also
| the whole network lived on the stack, which is means not having
| any dynamic allocation (not sure if this is good?).
| 0x1ceb00da wrote:
| How does this affect the compile times?
| erichocean wrote:
| It's nothing like C++ templates.
| koe123 wrote:
| They become quite long, but it was surprisingly tolerable. I
| recall it vaguely but a 100MB neural network was on the order
| of minutes with all optimizations turned on. I guess it would
| be fair to say it scaled more or less linearly with the file
| size (from what I saw). Moreover I work in essentially a
| tinyml field so my neural networks are on the order of 1 to 2
| MB for the most part. For me it wouldve been reasonable!
|
| I guess in theory you could compile once into a static
| library and just link that into a main program. Also there
| will be incremental compilation in zig I believe, maybe that
| helps? Not sure on the details there.
| anonymoushn wrote:
| I've done this sort of thing by writing a code generator in
| python instead of using comptime. I'm not confident that
| comptime zig is particularly fast, and I don't want to run the
| json parser that generates the struct all the time.
| DanielHB wrote:
| I believe that Zig build system can cache comptime processes,
| so if the JSON didn't change it doesn't run again.
| Validark wrote:
| I think if you integrated with the build system, yes, Zig
| can do things only when the file changed. But I'm not sure
| that Zig figured out incremental comptime yet. That's way
| harder to accomplish.
| koe123 wrote:
| Another thing I tried as an alternative is using ZON (zig
| object notation) instead of json. This can natively be
| included directly as a source file. It involved writing a
| custom python exporter though (read: I gave up).
| mk12 wrote:
| FWIW the goal for comptime Zig execution is to be at least as
| fast as Python. I can't find it now but I remember Andrew
| saying this in one of his talks at some point.
| pakkarde wrote:
| After having written a somewhat complete C parser library I don't
| really get the big deal about needing meta programming in the
| language itself. If I want to generate structs, serialization,
| properties, instrumentation, etc, I just write a regular C
| program that processes some source files and output source files
| and run that first in by build script.
|
| How do you people debug and test these meta programs? Mine are
| just regular C programs that uses the exact same debuggers and
| tools as anything else.
| nox101 wrote:
| I don't know about zig bit the power of lisp is that youre
| manipulating the s-expressions or to put it another way, you're
| manipulating the ast. To do that in C you'd need to write a
| full C parser for your C program that processes source files.
| modernerd wrote:
| Yes, this is the same approach Ryan Fleury and others advocate,
| and it's perfectly good:
|
| > Arbitrary compile-time execution in C:
|
| > cl /nologo /Zi metaprogram.c && metaprogram.exe
|
| > cl /nologo /Zi program.c
|
| > Compile-time code runs at native speed, can be debugged, and
| is completely procedural & arbitrary
|
| > You do not need your compiler to execute code for you
|
| https://x.com/ryanjfleury/status/1875824288487571873
| chikere232 wrote:
| MS DOS choice of / for commandline arguments and \ for paths
| always hurts my eyes
| zamalek wrote:
| The only benefit that some (certainly more rare) compilers
| can provide is type metadata/compile-time reflection.
| Otherwise, totally.
| p0nce wrote:
| Because it goes hand-in-hand with your code self-describing
| with static reflection.
| pjc50 wrote:
| C# (strictly, Roslyn/dotnet) provides this in a pretty nice
| way: because the compiler is itself written in the language,
| you can just drop in plugins which have (readonly!) access to
| the AST and emit C# source.
|
| Debugging .. well, you have to do a bit more work to set up a
| nice test framework, but you can then run the compiler with
| your plugin from inside your standard unit test framework,
| inside the interactive debugger.
| adev_ wrote:
| > I just write a regular C program that processes some source
| files and output source files and run that first in by build
| script.
|
| Cool, you now invented your own DSL and half-baked meta
| programming macro language for something that shall have been
| in the language to begin with.
|
| In addition, any complex interaction between your "own made
| template engine" and the native code is now a pile of hack. E.g
| write a generic function: Good luck to interpret any error
| based on the typing.
|
| Code generation is almost consistently the worst solution to a
| meta-programming problem.
| kreco wrote:
| > Cool, you now invented your own DSL and half-baked meta
| programming macro language
|
| I'm not sure I'm following your statement. What he said was
| to use a C program to parse C code and emit additional C
| code. There is no mention of DSL.
|
| > for something that shall have been in the language to begin
| with.
|
| The whole point of this discussion is to debate on that.
|
| I have no strong opinion (yet) but the meta program looks
| easy to understand compared to the pandora box of
| metaprogrammaing withing the language (since it requires
| standardization, limitations etc.).
| ezwoodland wrote:
| I think the parsed c program is the DSL. You have to write
| a parser and compiler for the c-like source to the actual c
| code.
| adev_ wrote:
| > What he said was to use a C program to parse C code and
| emit additional C code. There is no mention of DSL
|
| Because it is always the same story:
|
| - You start by writing a little meta-compiler to solve one
| specific codegen problem in a specific portion your code.
|
| - Then you realize their is many slight variations of this
| problem in other areas of your project or other projects...
| because it is exactly what *Genericity* is all about. And
| we _know_ that since literally the 1970s and freaking LISP.
|
| - To avoid your meta-compiler to become a Frankenstein of
| options with endless hardcoded logic: you make it interpret
| some annotations in C comments, some preprocessors or some
| template files somewhere.
|
| -> Congratulations: you invented your own half baked DSL.
|
| I have seen that many time, in many places. Again and
| again. Often because there is a category of C programmers
| that would prefer to swim in their own shit instead of
| using few C++ templates.
|
| Codegen is consistently a terrible solution to a well
| studied problem: meta-programming. The fear of the feature
| creep (templates, macros) shall never be a justification to
| create some half baked complexity monster that will alaways
| finish worst than the problem they try to avoid.
|
| If you doubt about that: Just use a lexer or parser
| generator. Or better, the quintessence of codegen:
| Autotools. They are a perfect illustration of how terrible
| and how fucking unmaintainable Codegen is.
|
| I can think of a single use case where a meta-compiler and
| a DSL are appropriate solutions: *Serialization* (Protobuf,
| Thrift, CapNProto, ...). Because in this precise case, you
| actually _do_ want a language neutral way to express your
| interface: you want an IDL.
|
| Currently here, Zig does the right thing: Comptime
| execution for meta-programming is one order of magnitude
| better than anything available in C or C++ before C++20
| coliveira wrote:
| The notion of "something that shall have been in the
| language" is not properly defined, as you can basically start
| from that and arrive at C++. So it is perfectly fine to
| assume that compile-time programming does not belong into the
| language and write your own processor.
|
| As for DSL, any DSL is a separate language that needs to be
| learned, so it is not very different from creating your own
| processor.
| benob wrote:
| > How do you people debug and test these meta programs?
|
| I couldn't find any other answer than using @compileLog to
| print-debug [1]. In lisp, apparently some implementations allow
| to trace macros [2]. Couln'd find anything about Nim's macro
| debugging capabilities.
|
| This whole thing looks like a severe limitation that is not
| balanced by the benefit of having all code in the same place.
| Do you know other languages that provide sensible meta-
| programming facilities?
|
| [1]
| https://www.reddit.com/r/Zig/comments/jkol30/is_there_a_way_...
| [2] https://stackoverflow.com/questions/44872280/macros-and-
| how-...
| disentanglement wrote:
| In lisp, macros are just ordinary functions whose input and
| output is an AST. So you can debug them as you would any
| other function, by tracing, print debugging, unit tests or
| even stepping through them in a debugger.
| michaelsbradley wrote:
| To debug macros in Nim, you'll likely need to print arguments
| and expansions at compile-time, inspect the output, change
| things to see what happens, repeat...
|
| https://nim-lang.org/docs/macros.html#toStrLit%2CNimNode
|
| https://nim-lang.org/docs/macros.html#astGenRepr%2CNimNode
|
| https://nim-lang.org/docs/macros.html#dumpAstGen.m%2Cuntyped
|
| https://nim-lang.org/docs/macros.html#treeRepr%2CNimNode
|
| https://nim-lang.org/docs/macros.html#dumpTree.m%2Cuntyped
|
| https://orlybooks.com/books/changing-stuff-and-seeing
| agentkilo wrote:
| Well put. I always have the feeling that any language which has
| an `eval` function or an invokable compiler can do meta
| program. That said, I think the "big deal" is in UX/DX. It's
| really nice to have meta programming support built-in to the
| language when you need it.
| Certhas wrote:
| I used to do that in Python with the numba jit. Write Python
| code that generates a Python code that then gets compiled.
|
| It's a fragile horrible mess, and the need to do this was a
| major reason for me to switch away from Python. It's a bit like
| asking why we don't just pass all arguments to functions as
| strings. Yeah, people write stringly typed code, but it should
| rarely be necessary, and your language should provide means to
| avoid it.
| coldtea wrote:
| > _I don 't really get the big deal about needing meta
| programming in the language itself. If I want to generate
| structs, serialization, properties, instrumentation, etc, I
| just write a regular C program that processes some source files
| and output source files and run that first in by build script._
|
| This describes exactly what people don't want to do.
| dboreham wrote:
| But exactly why?
| 0x696C6961 wrote:
| Because after enough hands have touched a codegen script,
| debugging it becomes impossible.
| 0x1ceb00da wrote:
| In jai you use the same language for programming and
| metaprogramming. The compiler knows how to execute the
| bytecode it generates. The compiler also has a builtin
| debugger for the bytecode.
| jerf wrote:
| If you just walked up to me out of the blue and asked "what
| computer language do you know is the worst for processing
| strings?", well, technically I might answer "assembler",
| but if you excluded that, my next answer would be C.
|
| Furthermore, you want some sort of AST representation, at
| one level of convenience or another (I include this
| compgen-style "being 'in' the AST" to be included in that,
| even if it doesn't necessarily directly manipulate AST
| nodes), and C isn't particularly great at manipulating
| those, either, in a lot of different ways.
|
| A consequence of C being _the_ definitive language that
| pretty much every other language has had to react to, one
| way or another through however many layers of indirection,
| for the past 40+ years, is that pretty much every language
| created since then is better than C at these things. C 's
| pretty long in the tooth now, even with the various
| polishings it has received over the years.
| jmull wrote:
| Whether you consider it a big deal or not is up to you, but
| with zig's approach you don't have to write/maintain a separate
| parser, nor worry about whether it's complete enough to process
| your source files.
|
| I don't know a lot about debugging zig comptime, though. I use
| printf-style debugging and the built-in unit test blocks.
| That's all I've needed so far. (Perhaps that's all there is.)
| elcritch wrote:
| Ah 'fieldNames', looks very similar to Nim's 'fieldPairs'. It's
| an incredibly handy construct! It makes doing efficient
| serialization a breeze. I recently implemented a compile time
| check for thread safety checks on types using 'fieldPairs' in
| about 20 lines.
|
| This needs to become a standard feature of programming languages
| IMHO.
|
| It's actually one of the biggest things I find lacking in Rust
| which is limited to non-typed macros (last I tried). It's so
| limiting not to have it. You just have to hope serde is
| implemented on the structs in a crate. You can't even make your
| own structs with the same fields in Rust programmatically.
| drogus wrote:
| At some point there was a discussion about compile time
| reflection, which I guess could include functionality like
| that, but I think the topic died along with some kind of drama
| around it. Quite a bummer, cause things like serde would have
| been so much easier to imeplement with compile time reflection
| cb321 wrote:
| Another example applying compile-time reflection is something
| like https://github.com/c-blake/cligen { but it helps if your
| host prog.lang has named parameters like Python's foo(a=1,
| b=2) }.
| ptrwis wrote:
| With comp-time reflection you can build frameworks like ORMs or
| web frameworks. The only trade-off is that you have to include
| such a library in the form of source code.
| datadeft wrote:
| This website is not mobile ready.
| cowsandmilk wrote:
| The article went off the rails at partial evaluation as it
| doesn't even show an example of partial evaluation. And then the
| section on generating code really went nowhere useful.
| G3rn0ti wrote:
| Site is down:
|
| https://web.archive.org/web/20250107090641/https://www.scott...
| melodyogonna wrote:
| Mojo's compiletime metaprogramming [1] is inspired by Zig's.
| Though Mojo takes things further by implementing a fully-featured
| generic programming system.
|
| 1. https://docs.modular.com/mojo/manual/parameters/
| Validark wrote:
| What can you do in Mojo that you can't do in Zig?
| kstrauser wrote:
| Pay a company for the privilege of being allowed to develop
| with it. It has a commercial license, where Zig is MIT'd.
|
| I haven't written a line of either. I could see using Zig,
| but there's no plausible scenario where I'd ever write Mojo.
| Weird proprietary languages tend to be a career pigeonhole:
| "you've been doing _what_ for the last 5 years?"
| thefaux wrote:
| Weird proprietary languages _can_ also be much better for a
| particular task than anything else and can thus be smart
| business. Someone who will dismiss something they don't
| know on the grounds that is weird and proprietary is not
| someone I'd want to work with. But of course if this is how
| a lot of people think then there may be no choice but for
| most people to try and stick with the tried and true.
| melodyogonna wrote:
| Nobody is paying anybody to use Mojo, its main issue is
| cross-platform support, specifically lack of native Windows
| support.
|
| Like I always say, most languages start off closed,
| incubated for some years by a tiny group, before being
| opened. Mojo is no different, in fact, Modular have given a
| pretty solid timeline about when they plan to open source
| the compiler -
| https://youtu.be/XYzp5rzlXqM?si=nmvghH3KWX6SrDzz&t=1025
| kstrauser wrote:
| Every language I've used in the last few years has been
| FOSS from the very beginning time that it was shared
| outside its original developers. A proprietary language
| is the odd exception, not the common case.
|
| Looks like I was wrong about having to pay to use Mojo
| itself. It's their "MAX" product you have to pay for, at
| least today. The language currently free-of-charge,
| although proprietary.
| melodyogonna wrote:
| GPU kernels.
| bionhoward wrote:
| Rust really needs comptime, I love cargo and the ecosystem but
| trait level programming is weird, macros are weird, why can't you
| just be normal? (rust screams)
| spacecow wrote:
| Tiny bug report: The second code example's output is still
| ">>array's<< sum is 6" (emphasis mine) even though the code
| snippet's printout is "struct's sum is {d}"
| ScottRedig wrote:
| Fixed, thanks for the report.
| skocznymroczny wrote:
| Zig looks interesting, I just wish it had operator overloading. I
| don't really buy most of the arguments against operator
| overloading. A common argument is that with operator overloading
| you don't know what actually happens under the hood. Which
| doesn't work, because you might as well create a function named
| "add" which does multiplication. Another argument is iostreams in
| C++ or boost::spirit as examples of operator overloading abuse.
| But I haven't really seen that happen in other languages that
| have operator overloading, it seems to be C++ specific.
| hiccuphippo wrote:
| You don't know the amout of magic that goes behind the scenes
| in python and php with the __ functions. I think zig's approach
| is refreshing. Being able to follow the code trumps the seconds
| wasted typing the extra code.
| magicalhippo wrote:
| Depends on domain I think. In some cases it can be very
| beneficial to keep the code close to the source, say math
| equations, to ensure they've been correctly implemented.
|
| In this case the operators should be unsurprising, so they do
| what one would expect based on the source domain. Multiplying
| a vector and a scalar for example should return the scaled
| vector, but one should most likely not implement
| multiplication between vectors as that would likely cause
| confusion.
| melodyogonna wrote:
| I don't know about PHP, what amount of magic goes in behind
| Python's dunder methods? You can open it and see
| akkad33 wrote:
| There are many gotchas to Python dunder methods. An example
| is there is a bunch of functions that can be called when
| you do something like 's.d' where s is an object. Does it
| call "getattr" on the object, getattr on the class or get a
| property, or execute a descriptor? It is very hard to tell
| unless you're an expert
| ptrwis wrote:
| Maybe such operators for basic linear algebra (for arrays of
| numbers) should be just built into the language instead of
| overloading operations. I'm not sure if such a proposal doesn't
| already exists.
| spiffyk wrote:
| There is a specialized `@Vector` builtin for SIMD operations
| like this.
| bigpingo wrote:
| Yeah I never got the aversion to operator overloading either.
|
| "+ can do anything!" As you said, so can plus().
|
| "Hidden function calls?" Have they never programmed a soft
| float or microcontroller without a div instruction? Function
| calls for every floating point op.
| mk12 wrote:
| The problem is not that + calls a function. The problem is
| that + could call one of many different functions, i.e. it is
| overloaded. Zig does not allow overloading plus() based on
| the argument types. When you see plus(), you know there is
| exactly one function named "plus" in scope and it calls that.
| kps wrote:
| Not if `plus` is a pointer. Then `plus()` is a conditional
| branch where the condition can be arbitrarily far away in
| space (dynamically scoped) and time. That's why I think
| invisible indirection is a mistake. (C _used to_ require
| `(*plus)()`.)
| LAC-Tech wrote:
| I feel like the ocaml solution would fit zigs usecase well.
|
| In ocaml you can redefine operators... but only in the context
| of another module.
|
| So if I re-define + in some module Vec3, I can do:
| Vec3.(a + b + c + d)
|
| Or even: let open Vec3 in a + b + c + d
|
| So there you go, no "where did the + operator come from?"
| questions when reading the source, and still much nicer than:
| a.add(b).add(c).add(d)
|
| I doubt zig will change though. The language is starting to
| crystallize and anything that solved this challenge would be
| massive.
| brylie wrote:
| Is anyone here using Zig for audio plugin development? It seems
| like a good candidate as an alternative to C++ but lacks the
| ecosystem (like JUCE). Are there any ongoing efforts to bring
| DSP/audio plugin development to Zig?
| hiccuphippo wrote:
| IIRC Andrew Kelley's original goal for developing Zig was to
| build a DAW.
| p0nce wrote:
| I'm using D for audio plugins and we do use CTFE extensively
| (named comptime in Zig). Zig might be a bit more fit maybe
| because of the easier C and C++ interop and targetting, but I'm
| not sure about the COM and OOP story.
| melon_tusk wrote:
| Zig is alright, but Odin is amazing.
| akkad33 wrote:
| Why?
| WalterBright wrote:
| D had it 17 years ago! D features steadily move into other
| languages.
|
| > Here the comptime keyword indicates that the block it precedes
| will run during the compile.
|
| D doesn't use a keyword to trigger it. What triggers it is being
| a "const expression". Naturally, const expressions must be
| evaluatable at compile time. For example: int
| sum(int a, int b) => a + b; void test() {
| int s = sum(3, 4); // runs at run time enum e =
| sum(3, 4); // runs at compile time }
|
| By avoiding use of non-constant globals, I/O and calling system
| functions like malloc(), quite a large percentage of functions
| can be run at compile time without any changes.
|
| Even memory can be allocated with it (using D's automatic memory
| management).
| WalterBright wrote:
| Here's one of my favorite uses for it. I used to write a
| separate program to generate static tables. With compile time
| function execution, this was no longer necessary. Here's an
| example: __gshared uint[256] tytab =
| tytab_init; extern (D) private enum tytab_init =
| () { uint[256] tab; foreach (i; TXptr)
| { tab[i] |= TYFLptr; } foreach (i; TXptr_nflat) {
| tab[i] |= TYFLptr; } foreach (i; TXreal) {
| tab[i] |= TYFLreal; } /* more lines removed for
| brevity */ return tab; } ();
|
| The initializer for the array `tytab` is returned by a lambda
| that computes the array and then returns it.
|
| A link to the full glory of it:
|
| https://github.com/dlang/dmd/blob/master/compiler/src/dmd/ba...
|
| Another common use for CTFE is to use it to create a DSL.
| optymizer wrote:
| Walter, I'll take any chance I can get to say: thank you for
| creating D! One thing I was wondering about is the limits of
| compile time execution.
|
| How does the D compiler ensure correctness if the machine the
| compiler runs on is different from the machine the program will
| execute on?
|
| For example, how does the compiler know that "int s =
| sum(100000, 1000000)" is the same value on every x86 machine?
|
| I'm thinking there could be subtle differences between
| generations of CPU, how can a compiler guarantee that a
| computation on the host machine will result in the same value
| on the target machine in practice, or is it assuming that host
| and target are sufficiently similar, as long as the
| architecture matches? (which is fine, I'm wondering as to what
| approaches exist)
| WalterBright wrote:
| > thank you for creating D!
|
| My pleasure!
|
| > is the same value on every x86 machine?
|
| It's the same value on all machines, because integer types
| are fixed size (not implementation dependent) and 2's
| complement arithmetic is mandated.
|
| Floating point results can vary, however, due to different
| orders in which constants are evaluated. The x87, for
| example, evaluates to a higher precision and then rounds it
| only when writing to memory.
| WalterBright wrote:
| D's ImportC also can do CTFE with C code! int
| sum(int a, int b) { return a + b; }
| _Static_assert(sum(3, 4) == 7, "look ma, check at compile
| time!");
|
| Why doesn't the C Standard add this? It works great!
| dminik wrote:
| As a disclaimer, the last time I gave Zig a solid shot was when
| 0.12 released. The last time I played with comptime properly was
| in 0.11.
|
| There's a heap of praise thrown at zig comptime. I can certainly
| see why. From a programming language perspective it's an elegant
| and very powerful solution. It's a shame that Rust doesn't have a
| similar system in place. It works wonderfully if you need to
| precompute something or do some light reflection work.
|
| But, from an actual user perspective it's not very fun or easy to
| use as soon as you try something harder. The biggest issue I see
| is that there's no static trait/interface/concept in the
| language. Any comptime type you receive as a parameter is
| essentially the `any` type from TypeScript or `void _` from C
| /C++. If you want to do _something specific* with it, like call a
| specific method on it, you have to make sure to check that the
| type has it. You can of course ignore it and try to call it
| without checking it, but you're not going to like the errors. Of
| course, since there are no interfaces you have to do that
| manually. This is done by reading the Zig stdlib source code to
| figure out the type enum/structures and then pattern-matching
| like 6 levels deep. For every field, every method, every
| parameter of a method. This sucks hard. Of course, once you do
| check for the type you still won't get any intellisense or any
| help at all from your IDE/editor.
|
| Now, there are generally two solutions to this:
|
| One would be to add static interfaces/concepts to the language.
| At the time this was shot down as "unnecessary". Maybe, but it
| does make this feature extremely difficult to use for anyone but
| the absolutely most experienced programmers. Honestly, it feels
| very similar to how Rust proc macros are impenetrable for most
| people.
|
| The second one is to take a hint from TypeScript and take their
| relatively complex type system and type assertions. Eg. `(a:
| unknown): a is number => typeof a === 'number'`. This one also
| seems like a bust as it seems to go against the "minimal
| language" mantra. Also, I don't get the feeling that the language
| dev team particularly cares about IDEs/LSPs as the Zig LSP server
| was quite bad the last time I tried it.
|
| Now, the third solution and the one the people behind the Zig LSP
| server went with is to just execute your comptime functions to
| get the required type information. Of course, this can't really
| make the experience of writing comptime any easier, just makes it
| so that your IDE knows what the result of a comptime invocation
| was.
|
| So in short it is as difficult to use as it is cool. Really, most
| of the language is like this. The C interop isn't that great and
| is severly overhyped. The docs suck. The stdlib docs are even
| worse. I guess I'm mostly dissapointed since I was hoping Zig
| could be used where unsafe Rust sucks, but I walked away
| unsatisfied.
| ajkjk wrote:
| Is there such a thing as a language that is always comptime by
| default? I.e. the main source code of the language runs at
| compile time, but emits as output another object in some data
| structure which then becomes the runnable program?
___________________________________________________________________
(page generated 2025-01-07 23:00 UTC)