Why you should get ready for a Crystal era

Why you should get ready for a Crystal era

Lately, I have been talking a lot about Crystal - probably too much if you ask my coworkers at Kalvad. So let me explain once and for all why Crystal is such an amazing language and how it will quickly grow in popularity.

Since I started to write code, I have always been looking for the perfect programming language. A language combining clarity, easy of use and performances. Nothing more, but nothing less either. In this quest of perfection, I've being playing with a lot of languages, but I always had some complains with the most famous ones: JavaScript is over-permissive, Python is great, but as all scripted language (Ruby, PHP, ...) they lack of performances. C/C++ or even Rust works well, but at the cost of ease of use and readability. Golang brought some fresh air into Software industry but its package system is terrible.
At this point, you might have noticed that I am really picky person. Which emphasize how amazing Crystal is.

Crystal in a few lines

If you check out the website, you will quickly grasp the main concepts of Crystal:

  • compiled: better performances & limit errors at run time
  • statically type checked: means you can omit types but they are still here and the code won't compile if a type is ambiguous. It increase code readability and limit error at run time.
  • types are non-nullable by default: crystal said NO to the famous billion dollar mistake
  • green concurrency: just like channels in go, Crystal allows to easily run async tasks and share data without using shared memory and locks
  • ruby-like syntax: reading crystal is like reading a book. Really
  • shards: one of the best package system ever built
  • an amazing standard library
  • ... and lot of other cool things: C binding, macros, native unit test, native template system, everything is an object, etc.

Crystal in depth

Code is worth a thousand of words.

Syntax and readability

Crystal has a Ruby-like syntax, but it also add some cool feature to improve code organization such as modules.

module Controllers # use module to scope your code

  class ApiResult
    property http_code : Int16
    property message : String
    property data : Json::Any
    
    # our class constructor
    def initialize(@http_code, @message, @data)
    	# automatically set the properties based on the variables names
    end
  end

  class UserController < Controller
    # create a login method which return an 'ApiResult' object
    def login : ApiResult
    
      # '!' by convention means that this function raises an exception in case of failure
      params.validate!

      # '?' means that the function will return 'nil' in case of failure
      user = Models::User.find_by_email? params["email"]
      
      # clear one-liner, '?' here means the function returns a boolean
      return ApiResult.new(404, "user not found", nil) if user.nil?
      
      # last value of a function is automatically returned
      ApiResult.new 200, "ok", {
        token: Services::JWT.encode(user.id.as(String)),
        user: user
      }.to_json
      
    rescue
      # You can add a 'rescue' for the whole funtion. How convenient is that
      ApiResult.new 500, "Oops. Something happened!", nil
    ensure
      # And finally you can use 'ensure' to always run a piece of code
      Services::Log.action "new login"
    end
  end
end

Types

Types in Crystal are very powerful. Crystal is a strictly typed language but thanks to type inference, the compiler will be able to assign types by himself. Therefore, unless the type of a variable is ambiguous for the compiler, you can omit to write it down.

On top of that, types are non nullable by default but can be set as nullable by adding a '?' at the end of the type declaration. Let see some code.

# return 'hello' if @input is an Int32 or a null value if @input is a boolean
def hello_world(input : Bool|Int32) : String? # since this function can return a null value, we need to set the return value as a nullable string using '?'
  typeof(input) == Int32 ? "hello" : nil
end

def main
  puts hello_world(true) # => "[nil]"
  hello_world(1) # => "hello" 
end

Everything is object

And you can extend everything ! The code below parse a YAML file and push the content into an array.

class Array(T)
  # Extend the Array class to add a new class method
  def self.from_envcr_yaml_file(file_path : String)
    # Simply read a YML file from a pathname and parse it into an array of EnvItem defined below
    File.open(file_path) { |f| Array(T).from_yaml(f) }
  end
end

module Envcr

  class EnvItem
    YAML.mapping(
      name: String,
      optional: {type: Bool, default: false},
      default: String?,
    )
    def eval
      # do whatever you need here with the values
    end
  end

  def load!(file_path : String)
    # Use the newly created 'from_envcr_yaml_file' function method to build an array of EnvItem from a YML file
    Array(EnvItem).from_envcr_yaml_file(file_path).each { |x| x.eval }
    #...
  end

  # ...

end

Standard Library

The Crystal standard library offers a lot of amazing features. As a direct result, the different libraries and frameworks will rely on the same core components. And you know what is cool about that? You will always get back on your feet even with when learning a different library/frameworks.

See, below are 2 piece of code from 2 different projects using 2 different frameworks. However, notice how both simply expose the standard HTTP request object

# Middleware inside a Kemal project
class AuthMiddleware < Kemal::Handler
    def call(context)
        return call_next context if context.request.headers.has_key?("Authorization") == false
            || context.request.headers["Authorization"] == ""
        user_id = Services::JWT.decode(context.request.headers["Authorization"].split(' ')[1])
        # ...
    end
    # ...
end
# Middleware inside an Amber project
  class Auth < Amber::Pipe::Base
    def call(context)
        return unauthorized context unless context.request.headers.has_key?("Authorization")
            && context.request.headers["Authorization"] != ""
        parse_jwt context, context.request.headers["Authorization"]
        # ...
    end
    # ...
end

Shards

Shard is the package system of Crystal. And it's one of the best out there. You can simply create a new crystal project using crystal init :

- crystal init app cool_stuff will generate a new binary application
- crystal init lib cool_stuff will generate a new library

In both case, the folder created will look like that:

├── LICENSE
├── README.md
├── shard.yml
├── spec
│   ├── cool_stuff.cr
│   └── spec_helper.cr
└── src
    └── cool_stuff.cr

Congratulations, you just created a new Crystal shard. You can then edit 'shard.yml' to add dependencies, edit the author or add a description, and once you are done, you can simply push your project on github (on https://github.com/dlacreme/cool_stuff for example).

And once your project is available on github, you can import it on any other Crystal application:

# shard.yml
dependencies:
  cool_stuff:
    github: dlacreme/cool_stuff
    branch: master

What can you do with Crystal?

Crystal can be used for pretty much everything. However, I believe it has 2 domains in which it performs better:

  • low-level programming. Thanks to its awesome standard library, you can pretty much do everything out of box in a few lines of code. In case the standard library does not have what you need, you can simply embed some C code
  • back-end system. Born during the web era, back-end developers quickly built strong web frameworks in Crystal

Current status & Future of Crystal

Crystal is still in beta. Currently running v0.35, it's actually the last version before the first stable release (v1) which is expected before 2021. However, we will still needs a few years to build the whole ecosystem around it. We have some strong frameworks and libraries already, but they all still need to reach their own v1.

Also, it's important to note Crystal is still not fully running on Windows. It is actually a requirement for the v1 of Crystal and this is one of the main focus of the core team. Hopefully, this won't be an issue for long.

Why Crystal will slowly climb to the top  

To end this post, and now you have a better overview of what Crystal can do, let me explain why I think Crystal will become one of the major programming languages:

  • readability and access: Crystal is very easy to learn and it's even easier if you have some experience with Ruby. Crystal can easily be used by a wide range of developers.
  • performances: Crystal is fast. Very fast. And fast is always better
  • release error-less product: thanks to unit tests, strict types and non-nilable types, you can only build stronger products
  • fast development: with its awesome package manager & extended standard library, you can focus on the core of your product
  • web friendly: Crystal natively provides everything you need to build a web application (native template system, great HTTP standard library). It will slowly slide into every tech-stack

In short, Crystal is the best and I hope to see you soon into the Crystal community !

... and I am done for today.

Have fun coding and talk to you soon !