Hacker Newsnew | past | comments | ask | show | jobs | submit | alethic's commentslogin

Pandoc templates use $...$ or ${...} for variable substitution, yes. body is one of the special default variables: the rest are documented in the manual. If you scroll to the bottom of the template linked from the article, you'll notice a $body$, along with a number of $if(...)$ $endif$ conditionals.

(This actually interferes with Typst's math mode. But you can manually construct math blocks, so no real problem. Pandoc variables are only valid within templates anyway.)

https://pandoc.org/MANUAL.html#variables-set-automatically


Don't forget the image rendering library!


In the context of this post, that's absolutely hilarious they're vibe-porting their Zig codebase to Rust.

I love Rust, but you couldn't pick a language with slower compile times... XD


Compiling Rust is actually quite fast in my experience. The problem with many Rust projects is that they pull in dependencies left, right, and center. Pulling in Tokio makes your project compile an entire thread management system even if you're just compiling Hello World, and simple oneliners containing macros can easily spread out into dozens of lines of code each.

Linking is also slow, and the extreme amounts of metadata produced for LLVM almost serves as a benchmark for LLVM's throughput, but that's all in an effort to produce faster, better binaries in the end.

On godbolt.org, Hello World compiles and runs in about 250ms. Zig's Hello World compiles and runs in 600ms. Of course Zig is still an unfinished language so optimisations like these are probably hardly a priority, but when it comes to lines of code per second, the difference isn't as big as people make it out to be.

What will make the most difference is how many crates the rewrite will pull in. The PORTING.md file specifies "No `tokio`, `rayon`, `hyper`, `async-trait`, `futures`" for the second phase, which should definitely get rid of the excessive compile time many people associate with Rust projects.


>Compiling Rust is actually quite fast in my experience

I guess it's all relative.

I find Rust's compile times abhorrent and it's objectively slower than many many other languages that also pull in dependencies left, right, and center. I guess that just means Rust scales very badly with amount of code.

I'd put it at a bit better than Haskell, but honestly not by much.

I really wish Rust would focus much more on compile times, or on making smaller parallel compilation units. It's quite a chore to have to keep splitting your program into smaller and smaller crates just to not sit and wait for an eternity.

As a comparison my CI job for Rust takes 14m running on a 16vCPU machine while my much larger TypeScript project compiles in 1m on a 2vCPU machine. I know people that have to spend quite a lot of work on keeping compile times manageable for Rust (nix, smaller crates, aggressive caching, etc etc).

Rust still brings me enough value that I'll stick with it, but one can still dream of a better future :)


idk, maybe you can do it, but your TypeScript project compiles to machine code?


That's true, but then there's also the case of working on the zig compiler which is roughly a million loc, and with `--watch -fincremental` you can get 200ms recompile even if you change some of the most called function. Meanwhile even a 5k-10k rust project can take a 30s to recompile on minor changes. So the impact on velocity can be quite high, I love both languages, but the Zig compiler is undeniably faster than the Rust compiler and by multiple orders of magnitudes.


Rust also has incremental compiling and is pretty fast, I haven't experienced 30 second compile times when using cargo watch. See also, cranelift, which is supposed to make compile times even faster.


The problem is not just that Rust takes a few seconds longer once. It compounds across the edit/debug cycle. If you make around 800 save/check iterations in a day, then a 2.5–3.5s feedback loop costs roughly 34–48 minutes of waiting per day. The same number of iterations at 200ms costs about 2.8 minutes.

So the practical delta is around half an hour to three quarters of an hour per day, or multiple hours per week. That directly affects flow state and experimentation speed. over the span of a month that's 2 full days worth of work waiting for the compiler. Or if you take my company's evaluation of the average engineer's hour cost it's roughly 2550 per month or almost 30k per year, obviously it's a bit exaggerated, you don't spend a full year refactoring and working like that, but even a tenth of that is still a big lump of money if you scale it to a few teams.

Now it needs to be taken with a huge pinch of salts because Rust provides other benefits that offsets the fact that it's painfully slow to compile, but still worth noting


from my own testing even their incremental on a codebase 10x smaller than the Zig compiler like Helix the text editor, on my machine almost all changes take 2/3s and with cranelift it's like 4/5s.

So it's definitely a faster feedback loop and honestly completely bearable, but it's not 200ms.


Having a gen 5 nvme helps significantly.


Recently, some 9front developers have picked up femtolisp, and are hacking it into something for their own use. https://sr.ht/~ft/StreetLISP/

I believe its adoption was motivated by needing to write/generate an OTF parser.


Yes, I have had the same experience with the specification. It really is quite difficult to follow :c

Their SpecTec system is fancy and neat but I don't think that auto-generated specifications produce something worth reading. Perhaps in the future when there's less churn, there might be a hand-written specification? In the mean time I've needed to jump into their Discord to ask clarification questions about the high-level stuff. Once understanding that and the grammar conventions and the like, the specification becomes much more readable, though still not great.

Certainly nothing like an RFC. But maybe I have too high standards...


(It doesn't help that the syntax is *weird*. You've got your choice of an S-expression Scheme syntax or a stack-oriented ML syntax, *and* you can use both together. And there's at least one undocumented de facto syntax floating around AFAIK, though I believe the standard merged support for the main features it was used for, so hopefully test suites and the like will switch away from it at some point.)


No one else has tried implementing the RCS standard.

There just aren't any open-source Android libraries for RCS out there, much less anything in AOSP.

https://github.com/search?q=rcs+android&type=repositories


They are similar, but effect handlers are more powerful and more amenable to typing.

https://lobste.rs/s/q8lz7a/what_s_condition_system_why_do_yo...


The checked exceptions analogy is a good one. Thinking of effect handlers as resumable checked exceptions with some syntactic sugar is very accurate. For someone with a Haskell background, thinking about them as "dependency injection" is also helpful (and these notions are equivalent!) but for the Java heads out there, yeah, resumable checked exceptions provides a really good mental model for what effect handlers are doing with the call stack in the general case.


What’s the difference between a resumable checked exception and a function call?


Function call always returns, and to one single caller, whereas effects can choose not to "return" at all, resume multiple times, etc


Right, though the former is just an exception. So what general effect systems provide above and beyond what we already have in most languages is "multiply-resumable" checked exceptions (also known as multi-shot continuations and often provided by "delimited continuations").

At the time I developed my Haskell effect system Bluefin there was a conventional wisdom that "you can't implement coroutines without delimited continuations". That's not true: you can implement coroutines simply as function calls, and that's what Bluefin does.

(The story is not quite as simple as that, because in order for coroutines to communicate you need to be able to pass control between threads with their own stack, but you still don't need multi-shot continuation.)


Good point! You might be interested in reading this article on the topic: https://without.boats/blog/coroutines-and-effects/


Thanks, I did find that interesting. I would say Bluefin is another entry in the static/lexical row, whereas its cousin effectful is in the static/dynamic row (although this may be a slightly different interpretation of the terms than is used in the article).


It's similar on the surface. Another language, Effekt, does actually use interfaces for their effect declarations rather than having a separate `eff` declaration.

The difference comes in their use. There's two things of note. First, the implementation of an interface is static. It's known at compile time. For any given concrete type, there is at most one implementation of MovieApi. You're using the interface, then, to be generic over some number of concrete types, by way of only specifying what you need. Effect handlers aren't like this. Effect handlers can have many implementations, actually. This is useful in the case of ex. adding logging, or writing tests to simulate I/O without actually doing it, or just having different behavior at different places across the program / call stack...

    eff MovieApi {
      def getPopularMovies();
    }
    def main() {
      run {
        println("Alice's movies: ", getPopularMovies());
      } with handler MovieApi {
        def getPopularMovies() = [
          "Dr. Strangelove", 
          "Lawrence of Arabia",
          "The End of Evangelion",
          "I Saw the TV Glow"
        ];
      }
      run {
        println("Bob's movies: ", getPopularMovies());
      } with handler MovieApi {
        def getPopularMovies() = [
          "The Magic School Bus: Space Adventures",
          "Spy Kids 3-D: Game Over",
          "Twilight: Breaking Dawn: Part II"
        ];
      }
    }
Second, the effects of effect handlers are not functions. They're under no obligation to "return", and in fact, in many of the interesting cases they don't. The `resume` construct mentioned in the article is a very special construct: it is taking the "continuation" of the program at the place where an effect was performed and providing it to the handler for use. The invocation of resume(5) with a value looks much like a return(5), yes. But: a call to resume 1) doesn't have to happen and the program can instead continue after the handler i.e. in the case of an effectful exception, 2) doesn't have to be invoked and the call to resume can instead be packaged up and thunkified and saved to happen later, and 3) doesn't have to happen just once and can be invoked multiple times to implement fancy backtracking stuff. Though this last one is a gimmick and comes at the cost of performance (can't do the fast call stack memcpy you could do otherwise).

So to answer your question more briefly, effects differ from interfaces by providing 1) a decoupling of implementation from use and 2) the ability to encompass non-local control flow. This makes them not really compete with interfaces/classes even though the syntax may look similar. You'd want them both, and most effectful languages have them both.


ORC/ARC are a reference counting garbage collector. There's a bit of a terminological clash out there as to whether "garbage collection" includes reference counting (it's common for it to not, despite reference counting... being a runtime system that collects garbage). Regardless: what makes ORC/ARC interesting is that it optimizes away some/most counts statically, by looking for linear usage and eliding counts accordingly. This is the same approach taken by the Perseus system in use in some Microsoft languages like Koka and Lean, but came a little earlier, and doesn't do the whole "memory reuse" thing the Perseus system does.

So for ergonomics: reference counting is not a complete system. It's memory safe, but it can't handle reference cycles really very well -- since if two objects retain a reference to each other there'll always be a reference to the both of them and they'll never be freed, even if nothing else depends on them. The usual way to handle this is to ship a "cycle breaker" -- a mini-tracing collector -- alongside your reference counting system, which while is a little nondeterministic works very reasonably well.

But it's a little nondeterministic. Garbage collectors that trace references, and especially tracing systems with the fast heap ("nursery" or "minor heap") / slow heap ("major heap") generational distinction are really good. There's a reason tracing collectors are used among most languages -- ORC/ARC and similar systems have put reference counting back in close competition with tracing, but it's still somewhat slower. Reference counting offers one alternative, though -- the performance is deterministic. You have particular points in the code where destructors are injected, sometimes without a reference check (if the ORC/ARC optimization is good) and sometimes with a reference check, but you know your program will deallocate only at those points. This isn't the case for tracing GCs, where the garbage collector is more along the lines of a totally separate program that barges in and performs collections whenever it so desires. Reference counting offers an advantage here. (Also in interop.)

So, while you do need a cycle breaker to not potentially leak memory, Nim tries to get it to do as little as possible. One of these tools they provide to the user is the .acyclic pragma. If you have a data structure that looks like it could be cyclic but you know is not cyclic -- for example, a tree -- you can annotate it with the .acyclic pragma to tell the compiler not to worry about it. The compiler has its own (straightforward) heuristics, too, and so if you don't have any cyclic data in your program and let the compiler know that... it just won't include the cycle collector altogether, leaving you with a program with predictable memory patterns and behavior.

What these .cyclic annotations will do in Nim 3.0, reading the design documentation, is replace the .acyclic annotations. The compiler will assume all data is acyclic, and only include the cycle breaker if the user tells it to by annotating some cyclic data structure as such. This means if the user messes up they'll get memory leaks, but in the usual case they'll get access to this predictable performance. Seems like a good tradeoff for the target audience of Nim and seems like a reasonable worst-case -- memory leaks sure aren't the same thing as memory unsafety and I'm interested to see design decisions that strike a balance between burden on the programmer vs. burden on performance, w/o being terribly unsafe in the C or C++ fashion.


The short answer is you'd write your code the same, then add .cyclic annotations on cyclic data structures.

("The same" being a bit relative, here. Nim's sum types are quite a bit worse than those of an ML. Better than Go's, at least.)


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: