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
Crystal for Rubyists
By wyhaines
Crystal for Rubyists
Crystal is a very fast statically typed language, with type inference, implemented on LLVM, but with a very Ruby-like syntax that makes it easy for Ruby developers to pick it up and become productive with it quite quickly.
- 675