DEV Community

loading...

A first look at Crystal as a Gopher

andyhaskell profile image &y H. Golang (he/him) ・7 min read

Crystal is a fairly new language, which started in 2014, and it's got some cool features:

🏷 Static types
🐹 Go-like concurrency
πŸš€ Fast binaries as a compiled language
πŸ’Ž and Ruby's satisfying syntax

Coming from Go, which I've been doing at work for five years, and hearing about Crystal having these features, I had to give that a try! So I'd like to show you around Crystal from a Gopher's eyes!

🐣 Starting a project

The first step after downloading Crystal, of course, was to actually start a project. For that, there's a command, crystal init, with two subcommands, app for making a program in Crystal, and lib for making a code library. So I started my app by running:

crystal init app first-app

Something I noticed right away is how much stuff you get out of just that command. You get things like:

  • A shard.yml file, which from a first look seems similar to package.json in Node as a central manifest for the project.
  • A license file for people using your software, which by default is the permissive MIT license.
  • A well-templated README file.
  • A sensible .gitignore to keep Git from keeping track of files like dependencies you imported.
  • A src directory to put your code in.
  • and even a spec directory to write automated tests for your code.

I really like having this all set up because all these things are things you'll want in a professional project, and having that all scaffolded means less agonizing about where to put everything.

πŸ— Building a project

We've got a Crystal file ready to go over at src/first-app.cr:

# TODO: Write documentation for `First::App`
module First::App
  VERSION = "0.1.0"

  # TODO: Put your code here
end

In Ruby, the word for "print" is "puts" (put S), so let's try that function printing out a hello world program.

  module First::App
    VERSION = "0.1.0"

-   # TODO: Put your code here
+   puts "Suplol, world!"
  end

We can then run the program through the crystal command by running crystal run src/first-app.cr, similar to how if we have a program in Go, we can run it with go run main.go.

Suplol, world!

Notice, by the way, that there's no main function like languages like Go and C have. Instead, we run our call to puts as "main code", as explained here.

But instead of just just running one Crystal file, let's make it into a binary. Inside a Go package, we'd do something like go build to build a binary for a package, or go install to install that resulting binary. In Crystal, you can build from one file with crystal build, but since we're in a project, let's try building the project with shards, Crystal's package manager (similar to npm/Yarn in JavaScript). Run this:

shards build

Now you should see a bin directory, so you can run ./bin/first-app to print the message.

You can see more of what shards can do here.

πŸ‘€ Checking out the standard library

Similar to Go, Crystal has an enormous standard library that gives you a lot to work with, without even installing any dependencies! There's code for working with strings and I/O, a JSON serialization module, and an HTTP server that works right out of the box.

So let's try making an app that prints out a string in magenta if it contains the word "sloth", since sloths love hibiscus flowers. First thing we need to do is figure out if a string contains that word. In Go, to make a function for checking if a string is slothful, we would use the strings package, like this:

package main

import (
    "strings"
)

func isSlothful(s string) bool {
    return strings.Contains(s, "sloth")
}

Scrolling through the navigator in the Crystal docs, we can see that there is a section titled String. But it's not a package the way Go's strings package is, it's a type. And in Crystal, every type, even a string, can have methods. So effectively, in Crystal, the strings package is defined as methods on the String type!

The String page reads similar to a Godoc for the string package, showing us around a package and its methods. And String has an impressive number of methods! The one we want is called includes?, which works like strings.Contains in Go.

def includes?(search : Char | String)

The function signature, Char | String is a "union type" or "either-or type" indicates that a string can take in either a character like 'A', or a whole string like 'sloth'. I find types like that convenient so the same function can work with similar types.

To give it a try, let's define our own is_slothful function. Go back to first-app.cr, and add this method to the First::App module:

  def self.is_slothful?(s: String)
    s.includes? "sloth"
  end

We're putting the self prefix on the function we're defining to make is_slothful? a class method of our First::App module.

Also notice that we didn't need a return statement, or parentheses around the arguments to includes?. This is just like in Ruby; parentheses on function calls are only needed to resolve ambiguity, and if there's no return in a function, the return value is what the last statement evaluates to.

To try this out, replace puts "Suplol, world!" with puts is_slothful? "Suplol, world!".

Then, re-compile with shared build, and when you run the binary, "false" should be printed. Change it to "Suplol, slothful world!" and "true" should be printed.

πŸ“‹ Adding test coverage

One of my favorite things about Go is that the Go command line program has a test subcommand, so you can make automated tests without any dependencies. And Crystal does that too, with crystal spec, which runs all the tests in the spec directory. And lucky us, since we made our project with crystal init, we already have a spec directory made. Go to spec/first-app_spec.cr, and you can see a simple suite of tests:

require "./spec_helper"

describe First::App do
  # TODO: Write tests

  it "works" do
    false.should eq(true)
  end
end

If you run this with crystal spec, then the test will fail since false does not equal true.

Let's try this out on our new is_slothful? method. If we were in Go, the test would look something like this:

func TestIsSlothful(t *testing.T) {
    if !isSlothful("Suplol, slothful world!") {
        t.Error(`"Suplol, slothful world!" was considered non slothful`)
    }
}

In Crystal, we don't have a testing.T; instead, we use it blocks to define test cases, and we write assertions of what we expect to be true, like in Ruby's RSPec and JavaScript's Jest. So a test for is_slothful would look like this:

  it "detects when a string is slothful" do
    First::App.is_slothful?("Suplol, world!").should be_false
    First::App.is_slothful?("Suplol, slothful world!").should be_true
  end

Get rid of the it works block from earlier and run crystal spec, and our tests should pass! But let's add one more example to this test case:

First::App.is_slothful?("Sloths for the win!").should be_true

Because we're looking for the string "sloths" with a lowercase s, our code isn't considering this capital S "Sloths for the win!" string to be slothful. Let's fix that!

Looking in the String package's docs, there is a method for making a string all-lowercase:

def downcase(options : Unicode::CaseOptions = :none) : String

We can convert a string to all-lowercase with this method, and we even can optionally pass in a CaseOptions to indicate which rules for capital and lowercase letters to use (like treating an I with and without a dot differently if you're working with text in Turkic languages). Let's use this downcase method our is_slothful method:

  def self.is_slothful?(s : String)
-   s.includes? "sloth"
+   s.downcase.includes? "sloth"
  end

Run crystal spec and the tests should now pass!

🌺 Bringing in the magenta!

Now, we've got our function working to test whether a string is slothful, so let's recolor a string if it is slothful! If we were recoloring text in Go, there isn't a standard library package for recoloring text, so we could either give our strings ANSI escape codes to colorize our text, or import Fatih Arslan's color package like this:

func printIfSlothful(s string) {
    if isSlothsul(s) {
        color.Magenta(s)
    } else {
        fmt.Println(s)
    }
}

In Crystal, colorizing functionality is actually a module right in its standard library, in the colorize module! By importing it, it adds a colorize method to the Object type, which every type inherits from! That means numbers, strings, and more complex types all can be displayed in multiple colors using this method!

Let's import that, and try this out on a print_if_slothful method in our First::App module. Up top in first-app.cr, add this line:

require 'colorize'

Then, let's make that method with this code:

  def self.magenta_if_slothful(s : String)
    if is_slothful? s
      s.colorize(:magenta)
    else
      s
    end
  end

If our string is not slothful, then we just return the string as-is, to be printed with the terminal's default text color. But if it is slothful, then we use s.colorize to return that string re-colored, and to pick a color, we're using a Crystal symbol, which is sort of like a string, and intended to be a unique name.

Now let's try this out. Remove the puts call that was there before, and add these lines of code:

  puts magenta_if_slothful "Suplol world!"
  puts magenta_if_slothful "Suplol slothful world!"
  puts magenta_if_slothful "Sloths rule!"

Run shards build and then run the binary it made, and you should get the first two line printed in your terminal's default color, and the last two lines printed in magenta! πŸ¦₯🌺

So far trying out Crystal, while I haven't done a ton with the Ruby family of languages before, I like this language so far, and find that a lot of what I know from Go and Ruby as a whole carries over well to this new language. We've barely scratched the surface, but if you're a Gopher looking for a fun new language to try, I recommend taking Crystal for a spin!

Discussion (0)

pic
Editor guide