First time programming using the Crystal language

Recently I posted ‘Doing less’. Tl;dr: I wondered why we (as tech-society) seem to be thrilled about making inefficient round trips using AI for development, or chase each other to use typed languages, while we could be using more expressive programming languages instead. Instead of guessing human input, we could write untyped short scripts that detail every edge case carefully, but without extreme uncertainty of human language input nor the extreme preciseness of typed languages. Scripting, however, is scoffed at by Real Programmers, but then why oh why do we AI?

Someone suggested I should try a different programming language (knowing that I’m a rubyist). Try Crystal, a language that shares performance characteristics of other compiled languages like C and Rust (not always in the top regions, but close). And although I heard of it a long time ago, I kinda forgot about it, shifting my interests towards learning Rust instead (one day). But Rust is not as fun as ruby. Crystal is also not ruby, but closer? To test it I tried rewriting a very simple static site generator. And it worked well for me.

One line hello world: puts "Hello world"

The title is a one line crystal program. No unnecessary boiler plate criterium (which I like about ruby as well) is met.

Package manager included: shards

Crystal comes with its own package manager shards, and it is based mostly on git-repo’s that you include. It is a YAML file, requiring that holds a bit between a package.json (it also contains the app’s name and some other metadata) and a Gemfile in ruby.

I needed some Markdown parsing and front matter parser (for metadata) and it was easy to find using Shardbox. YAML parsing, templating and more was already covered by the standard libraries. I wasn’t disappointed.

Simple templating: ECR

ECR is what replaces ERB. One thing to understand though, is that it is has to be precompiled. So no dynamic inclusion based on a variable. But it is simple and effective. I was looking shortly at Blueprint which reminded me of Phlex in ruby, but for now ECR was sufficient.

Everything is an object

Everything is an object, which is what I enjoy about ruby, and I can even extend core objects:

class String
  def pluralize
    self + "s"
  end
end

"demon".pluralize # demons

Of course you should not do this, but it is nice that it is possible, especially if you want a behaviour on an object that you’re missing from e.g. Ruby or Rails’ ActiveSupport.

But, typing?

To successfully pre-compile, Crystal is actually a typed language. The fun part: you might not even notice. Sometimes you have to define the expected type (e.g. when defining object properties). In my simple use case it hasn’t been a big problem, although my initial plan of converting a YAML structure in a Hash turned out to be more complicated than expected, hence I decided to rely on the YAML::Any-class as front matter. The proper way might have been to iterate over the front matter key values to expected classes, but for my little experiment there was no need to bother.

Iterating with Integer#times

Yes, you can do:

10.times do |i|
  p i
end

Not ruby.

They explicitly write in their Crystal for Rubyists-documentation:

Crystal is a different language, not another Ruby implementation.

But in general it feels much like it. You can even extend the base classes, allowing one to make necessary adjustments when needed. The removal of synonyms (it only supports Enumerable#find, Enumerable#map, and Enumerable#size for example (and not select & count)) is fine by current me, but would have bothered me in the past. And Crystal only does double quoting, but that’s my preferred style anyway.

Also no attr_accessor for classes, but property, which is a much better method name anyway.

Thread & memory safety?

I haven’t spent much time exploring this, developer happiness was more important to me here (I try to follow best practices). On the other hand this is what is so attractive about Rust. Which I’ll return to.

Popular? No.

Crystal doesn’t hit the marks for popularity / community size, but I think it fits a nice niche of compiled languages that are actually fun and easy to write, with great performance.

The biggest problem with low popularity is that you might be the first person to run into issues. With more popular languages, many problems have already been solved and/or worked around properly (eying towards aforementioned safety issues for example).

Conclusion

It may be slightly less flexible than ruby, and has a much smaller ecosystem, but in case I need to write something fast and simple, I might choose to use Crystal more. The language hits quite a few high marks. And (esp. coming from ruby) the learning curve is smooth.

First time crystal code by me

require "markd"
require "front_matter"
require "yaml"
require "ecr"
require "html"

STATIC_FILES = ["turbo.js", "stylesheet.css"]

class String
  def pluralize
    self + "s"
  end
end

class Page
  property contents : String
  property meta : YAML::Any
  property title : String
  property kind : String
  property filename : String

  def initialize(contents, meta, filename)
    @contents = contents
    @meta = meta
    @title = meta.as_h.fetch("title") { "Geen titel" }.to_s
    @kind = meta.as_h.fetch("kind") { "article" }.to_s
    @filename = filename
  end

  def path
    File.join(kind.pluralize, @filename, "/index.html")
  end

  def file_path
    File.join("public", path)
  end

  def write
    Dir.mkdir_p(File.dirname(file_path))
    File.write(file_path, to_full)
  end

  def to_full
    # we can't make this dynamic; ECR is inlined during compilation
    if kind == "article"
      ECR.render("crystal_layouts/article.ecr.html")
    else
      ECR.render("crystal_layouts/default.ecr.html")
    end
  end
end

pages = [] of Page

Dir["content/**/*.md"].each do |filename|
  FrontMatter.open(filename) do |front_matter, content_io|
    contents = Markd.to_html(content_io.gets_to_end)
    filename = File.basename(filename, ".md")
    page = Page.new(contents, YAML.parse(front_matter), filename)
    pages << page
  end
end

pages.each do |page|
  page.write
end

def link_to(text, url)
  "<a href=\"#{url}\">#{HTML.escape(text)}</a>"
end

# create index
sorted_articles = pages

contents = ECR.render("content/index.html.ecr")
title = "Waarom links?"


STATIC_FILES.each do |file_name|
  File.copy(File.join("content",file_name), "public/#{file_name}")
end

File.write("public/index.html", ECR.render("crystal_layouts/default.ecr.html"))

Op de hoogte blijven?

Maandelijks maak ik een selectie artikelen en zorg ik voor wat extra context bij de meer technische stukken. Schrijf je hieronder in:

Mailfrequentie = 1x per maand. Je privacy wordt serieus genomen: de mailinglijst bestaat alleen op onze servers.