Okay, so yes, the site is undergoing a weird transition. It's different, lacks some of my original posts, and doesn't use any of my previous Racket code. Why? Let's delve into that.

Let's Talk Racket

It's been well over 2 months since I made any real commits to this project, or for that matter, any other hobby project of mine. I spent some time writing toy code, relaxing, and mostly restrategizing my overall gameplan for life. But past that, I haven't spent any time in Racket, and for some reasons, I also soon do not see myself writing Racket (unless work requires it for whatever reason).

I like Racket, but after using it for a while, I want to move onto something better. I don't really enjoy the way Racket looks anymore, and sometimes looking at other people's code, I'm often left wondering what the heck certain lines of code do without having to refer to Racket documentation.

Let's just picture an imaginary API in Racket compared to an imaginary API in Rust. Both will scan our supplied file directory for pages, sort the pages in order of date, then scan for static pages, then publish everything. By scanning for pages, we're assuming that it just does the Markdown compiling for us.

#lang racket/base

(require (only-in "imaginary-api.rkt"
         (only-in racket/contract

(define/contract (render-site base-path)
  (-> string? void?)
  ; collect pages
  (define all-posts (get-posts base-path))
  (define sorted-posts (sort-posts all-posts))
  (define static-pages (get-static-pages base-path))
  ; publish pages
  (publish-pages sorted-posts)
  (publish-pages static-pages))

While there's nothing inherently wrong with this code, my first glance tells me that the import lines are as long as the code itself. Is it necessary for those to be that long? Yes, actually. If we weren't going the way of typed Racket but wanted contractual safety, then we need to import some contract information. Why not just import the entire module? Well, that increases the overall size of the Racket program.

Racket uses a bytecode VM and hotloads code from modules. Each time we import a function, it gets evaluated in our current namespace, then placed onto our VM stack. If we import a module with no restrictions on qualifiers, then everything gets imported by default. It's possible to import everything, some things, or even tell Racket to not load something specifically, but since it's entirely interpreted, the more functions you bring in, the bigger the program becomes.

Contracts are also required because Racket is dynamic. Contracts enforce program safety by placing guarantees on certain things at runtime. If we were to use typed Racket and compiled our programs, it should add compile-time guarantees instead, but typed Racket itself is very hard to use because programs have to supply typed/Racket bindings. If no bindings are provided, Racket then defers to contracts, which aren't compile-time. Contracts are a middle-ground where it ensures safety and is far easier to write than typed Racket, so I prefer to use it.

In this above example that looks harmless, note that we have a duplicate list of sorts of posts; one sorted while the other isn't. If we did this on a very large list, our program will no doubt crash because of lack of memory. I've had instances where I created vector arrays and because I made one too large, the program would just crash out completely.

The above code ends up being complicated. It can be optimized better, but let's see what happens if I try to make a similar API in Rust.

pub use self::files;
pub use self::publisher;

pub fn build_site(base_path: &String) -> Result<(), String>{
	// collect posts
	let posts = files::get_posts(base_path)?;
	let sorted_posts = posts.sort()?;
	let static_pages = files::get_static_pages(base_path)?;
	// publish
	// we survived

Now this example is how I would prefer my site builder to look. As it is a compiled language, there are far more guarantees here of type safety; things being a string, lists being automatically inferred but still strict, and error bubbling done in places where errors can arise. The ? at the end of each line signifies that we want to run a match-bubble on certain lines of code if the function yields a Result<T,E> type. Since our type shares a Result<T,E> type, then we can safely assume that the function we run, if an error arises, should also be returned by this function.

I do have a separate sorted_posts variable here, but this has more to do with Rust's move semantics and ownership of memory. When we created sorted_posts, it moves the ownership of posts to sorted_posts, and borrows it's data. Because of this borrow, nothing else can borrow posts information now. So there wouldn't dare be a duplicate memory issue now because our compiler can rescind access to anything that tries to refer to that memory now, because it may or may not even be the same anymore (I could be missing a mut somewhere but we can fix that later).

Rust doesn't use a bytecode VM, so any imports are automatically fixed out at compile time. We have a global import qualifier, but the Rust linker will correctly only import things it actually needs. We could further qualify our imports, which can be done for namespace pollution reasons, but here it is not really needed.

I don't think my Rust skills are great, but so far I like how the Rust version looks compared to the Racket version. I still really like Racket as a language and thinks it's a lot of fun, but there can be some drawbacks that might just be annoying later on.

Let's Talk Rust

Rust is a language I've had a love/hate relationship with for such a long time. It's been out for years, but it doesn't have much of a standard library when comparing it to Racket or Golang. I've written premature, amateurish, non-third party importing code before and found it okay.

There are things I definitely like about Rust, and how things like algebraic types are zero-cost abstractions. I like definining algebraic types a la Haskell, so I spent a decent amount of time playing around with Rust and seeing what I could and couldn't do. Overall, there wasn't much I couldn't do, but I would have to mangle libraries that weren't stable yet, or tended to import billions of other packages and created mini-DSLs within Rust's macro system that you'd have to learn or adapt to.

Overall, I have a limited amount of experience with Rust past just writing basic file I/O applications. I've never done networking, or created servers, or really done much more advanced things. I have done some multithreaded stuff by using a multiple-producer/single-consumer pattern, but that's it. I want to go back and re-write some of my original Doom code and break up the program into multiple applications as opposed to monolithically putting it into one program with lots of arguments.

So why all this talk about Rust exactly? It's because I believe if I want to better myself as a programmer and make better things, I need to stick to a single language and learn to contribute back to the tools I use. I can't see myself contributing to Racket, because there's many things I don't understand. I wouldn't dare contribute to Haskell for fear of lack of understanding category theory as well as others. But Rust I feel comfortable with knowing that it's still relatively new and there are new projects being developed with it coming out constantly.

With that being said, it's time to retire my old Racket program which built my website, and move onto something else: Zola.

Let's Talk Zola

Zola is a static site generator, much like Frog, Jekyll, Hugo, Pelican, Hakyll, or any other major ones I missed out on (sorry). The difference is that it's written in Rust, and supports a lot of major fundamentals of site generators. I decided to pick this because of it's massive feature array, because it's in Rust, and because it is simply fast.

I use GitLab CI to publish my site, and because I'm a free user, I have a limited amount of time to use the GitLab CI runners per month. If I went over, I'd have to wait to publish (or just simply move to a commit-manual site where I push the HTML myself). My Racket program might consume too much time because Racket isn't necessarily getting that much faster with the push to a Chez Scheme backend. It will take a long time before the Racket CS compiler starts to become the defacto. I am honestly surprised they chose to move to a Chez Scheme compiler before considering the possibilities of moving to Rust, since a lot of their chief complaints came from having to maintain a C codebase, but it's whatever.

Not that I make that many additions to my site in such a small period of time that it would matter, but I like to do premature optimizations like that. My detachment from Racket is so large that I simply would not care to go back to it, so why not move onto something that is more friendlier and has an active community that I can try to support and engage with?

Let's Talk Website

Okay, anyways, back on track. Some posts will go missing, and may likely not come back. But I will try my hardest to come up with some new posts, and maybe go for a weekly-update kind of thing. It may keep me more active if I try to incorporate programming into some of my new areas of study that I am trying to work on overall.

This is an entirely new build process so I'm pushing this with the hopes that GitLab CI will just accept my .gitlab-ci.yml and everything will work perfectly, but sadly I do not believe that will happen in one attempt, so learning to build a website with Zola may be an experience I'll talk about more.

Anyways, thanks for reading and hopefully this website won't set itself on fire in the next commit.

Edited appendum: I can indeed confirm it took me a second commit to make the project build. Turns out using a Docker image was not the solution, and instead pulling the binary release from Zola's GitHub proved more reliable. So lesson learned: examine Dockerfiles first and read documentation.