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 topackage.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!
Top comments (0)