Crystal

For Rubyists

Kirk Haines

Principal Developer Relations Engineer

khaines@newrelic.com

Kirk Haines

  • Rubyist since 2001

  • First professional Ruby web app in 2002
  • Former Ruby 1.8.6 Maintainer

  • ❤ Ruby and Open Source in General

  • 60/40 software engineer / devops engineer

  • I like internals, technical, fiddly bits, and hard problems

  • Located in southeastern Wyoming, US

  • I like endurance bicycling and running

Crystal for Rubyists

Compiled, based on LLVM

Crystal for Rubyists

Compile & Run In One Command

(mostly for testing while developing)

Crystal for Rubyists

Statically Typed, with Type Inference

Crystal for Rubyists

...and Type Checking

Crystal for Rubyists

Null Reference Checking

Crystal for Rubyists

CSP style Concurrency, With Channels,

Like Golang

Crystal for Rubyists

A Syntax Very Similar to Ruby

Syntactically valid for both Crystal and Ruby

Crystal for Rubyists

But Not Quite Ruby

Crystal for Rubyists

Types

  • Ruby is dynamically typed, with optional typing and type checking (as of Ruby 3.0.0)
  • Crystal is statically typed with type inference

Crystal for Rubyists

Types

In Crystal, everything is an object, where object is fundamentally defined by two properties:

 

1. It has a type.

2. It has methods that can be called on it.

Crystal for Rubyists

Types

Literal Sample Values
Nil nil
Bool true, false
Integers* 13, 36, -119, 985323456234_u64
Floats* 2.0, 1.3e12, +46.87, -3.19
Char 'a', '\n', '💎'
String "bhal\tblah",  "क्रिस्टल कमाल का है!" 
Symbol :just, :like, :ruby
Array [1,2,3], [1. 2. 3] of UInt64, [1, 2, "3"] of Int32|String
Array-like Set{1, 2, 3}, MyClass{"term", "definition"}
Hash {"a" => "b"}, {} of String => String|Symbol|Int32
Hash-like MyOtherClass{fizz: "buzz"}
Range 1..100, 1...100, ..100, 1..
Regex /(just)? like\sruby/
Tuple {"finite", 4, "set", 'x', "of", :x, "elements"}
NamedTuple {this: "that"}
Proc ->(x : Int32, y : Int32) { x + y }
Command `df -k`, %x(jekyll build}

* There are multiple varieties of floats and integers.

Float32 Float64
Int8 Int16
Int32 Int64
Int128 UInt8
UInt16 UInt32
UInt64 UInt128

Crystal for Rubyists

Types

Crystal type inference is pretty smart and pretty transparent.

Crystal for Rubyists

Types

Empty arrays (and hashes) require a type declaration.

Crystal for Rubyists

Types

Crystal for Rubyists

Ruby fails at runtime

Crystal for Rubyists

Crystal fails at compile time

Crystal for Rubyists

The Ruby Way works for both...

Crystal can coerce types so that this still works

Crystal for Rubyists

Overloading FTW

Overloading ensures that methods are responsible for their inputs while keeping the code easy to read.

Crystal for Rubyists

Types

Type Unions are a combination of two or more types. With type unions, dynamic structures can accept complex typing.

Crystal for Rubyists

Types

In Ruby, single and double quotes may be interchangeable for String literals. You can't do that in Crystal.

Crystal for Rubyists

Types

Typing inference and type handling is complex, but most of the time it just works, and if there is an error, the error messages usually make it very clear what is wrong.

 

If there is still doubt, a few minutes in the docs will clear it up.

Crystal for Rubyists

Concurrency

As of version 0.36.1

    Single-Threaded by default

    Multi-Threaded available if compiles are done with

        -Dpreview_mt

    Fibers (lightweight cooperatively scheduled processes)

    Channels are used for fiber communications

    Built-in event loop for efficient handling of IO

Crystal for Rubyists

Concurrency

  • Crystal Fibers <-> Golang Goroutines
    They are very similar, but Crystal fibers are not preemptively scheduled like Golang coroutines are.
  • Crystal Fibers <-> Ruby Fibers
    Strong conceptual overlap, but Ruby makes the programmer do everything themselves if they want to use Fibers; Crystal helps you with all of the hard stuff.
  • Crystal Channels <-> Golang Channels
    Conceptually similar

Crystal for Rubyists

Key Fiber Differences with Ruby

  • Crystal has a built in event loop
  • All IO in Crystal is Fiber-aware
  • In Ruby, Fiber context switches MUST be explicit*:
    - Fiber.yield / Fiber.resume
  • In Crystal, any IO wait/sleep state allows a fiber yield, implicitly. Fiber.yield / Fiber.resume are also available.
  • When built with threading enabled, Fibers are automatically spread across threads.
  • Some Ruby idioms (like Thread.join) don't exist in Crystal, and are done with channels, instead.

Crystal for Rubyists

Key Fiber Differences with Ruby

Ruby 3.0.0 is a gamechanger, but that's a separate talk...

 

Ruby 3.0.0 introduces a built in event loop, via the Async, which makes using Fibers for concurrency less painful.

 

Ruby 3.0.0 also introduces Ractors, which are a flavor of CSP concurrency, using message passing.

Crystal for Rubyists

Example -- waiting for a thread

Crystal for Rubyists

Concurrency

Recursive Golang Fibonacci Calculator

Crystal for Rubyists

Concurrency

Crystal is conceptually similar, but without preemptive fibers, the code has to do it's own scheduling by choosing when to yield (and not doing it too often).

Crystal for Rubyists

Concurrency

Crystal Fibers <-> Ruby Fibers

Very similar, but Ruby offers no automated scheduling

Crystal Channels <-> Somewhat Similar to Queue

Ruby is similar, but it is more work for the programmer

Crystal for Rubyists

Concurrency

Ruby with Fibers needs a loop to keep returning execution to the Fibers. It is otherwise fairly similar.

Crystal for Rubyists

Concurrency

Crystal Fibers <-> Ruby Threads

Ruby -- real system threads; heavy; preemptive

Crystal for Rubyists

Concurrency

Ruby with Threads is more like the Golang program, because of preemption of the Threads.

Crystal for Rubyists

Concurrency

Ruby 2.7.1 with either Threads of Fibers is vastly slower.

Timings
ruby fib_spinner.rb  76.08s user 0.04s system 100% cpu 1:16.09 total
RUBYOPT=--jit ruby fib_spinner.rb  24.25s user 0.04s system 102% cpu 23.734 total
fib_spinner_go  5.11s user 0.01s system 100% cpu 5.114 total
fib_spinner_crystal  3.85s user 0.01s system 100% cpu 3.856 total

Crystal for Rubyists

Sidenote -- Go JIT go! Vroom!

Ruby 3.0.0  JIT helps a lot on tasks like this!

3x Performance Improvement from the JIT. Nice!

Timings
ruby fib_spinner.rb  76.08s user 0.04s system 100% cpu 1:16.09 total
RUBYOPT=--jit ruby fib_spinner.rb  24.25s user 0.04s system 102% cpu 23.734 total
fib_spinner_go  5.11s user 0.01s system 100% cpu 5.114 total
fib_spinner_crystal  3.85s user 0.01s system 100% cpu 3.856 total

Crystal for Rubyists

Concurrency

Crystal runs an event loop for you, for efficient concurrency when working with IO.

Crystal for Rubyists

Concurrency

Massively concurrent because the event loop efficiently handles IO wait to keep fibers maximally busy.

Crystal for Rubyists

Concurrency

The Ruby version, with Threads, is pretty similar.

Crystal for Rubyists

Concurrency

Performance to count all of the lines in all of the files (10539 of them) in the Ruby Github repository, running Ubuntu 20.04 on under Windows 10 WSL 1

crystal_linecount  0.78s user 2.22s system 114% cpu 2.620 total

ruby linecount.rb  1.91s user 24.50s system 328% cpu 8.048 total

Crystal for Rubyists

Concurrency

If fibers mutate data without synchronizing access, bad things can happen. Use a Channel or use a Mutex, or be prepared to be surprised.

Crystal for Rubyists

Concurrency

Carol should be 30!

Crystal for Rubyists

Concurrency

Just like Ruby, if execution context might move around, or if there might be race conditions, use a Channel, or use a Mutex to synchronize access.

Crystal for Rubyists

Metaprogramming

Crystal is compiled; it doesn't support a runtime #eval

It also does not support #send *

 

These are the bread and butter of Ruby Metaprogramming

 

* It is possible to implement #send (more or less), but it isn't generally considered a good idea to do so.

Crystal for Rubyists

Metaprogramming

Crystal does support Macros!

 

Macros are methods that receive AST bodies at compile time, and using a macro definition language (complete with conditionals, looping, conditional evaluation, and other constructs), produces code that is inserted back into the program before compilation continues.

Crystal for Rubyists

Metaprogramming

It's complicated.

 

An entire presentation could be done just on metaprogramming with Crystal

Crystal for Rubyists

Metaprogramming

Crystal for Rubyists

C Bindings

Crystal supports linking to C libraries directly within the language syntax.

Crystal for Rubyists

Tooling

Crystal is comes pre-equipped with some useful tooling

 

Testing

Scaffolding

Sharding

Documentation

Formatting

and More....

Crystal for Rubyists

Tooling

Crystal for Rubyists

Tooling

Crystal for Rubyists

Tooling

Shards are the Crystal equivalent of Rubygems -- it is a packaging and dependency management system for Crystal libraries and applications.

Crystal for Rubyists

Tooling

Autogenerate projects docs

Crystal for Rubyists

Tooling

Automatically clean up formatting

Crystal for Rubyists

Example Rewrite

Ruby program that simulates rolling a set of 6-sided dice, keeps the top N dice, and then compares the result to a target number ten million times, and then reports the odds of meeting or beating the target number.

Crystal for Rubyists

Example Rewrite

Crystal

      for Rubyists

Example Rewrite

Crystal for Rubyists

Example Rewrite

@r = [] of Int32

Crystal for Rubyists

Example Rewrite

getter dice : Int32

getter plus : Int32

Crystal for Rubyists

Example Rewrite

That's it!  It works.

Crystal for Rubyists

Example Rewrite

Crystal for Rubyists

The 1.0.0 Question...

0.36.1 was released 02 Feb 2021

The next release is expected to be 1.0.0-pre1.

Crystal for Rubyists

Questions?

Thank You