Kirk Haines
Principal Developer Relations Engineer
khaines@newrelic.com
Rubyist since 2001
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
(mostly for testing while developing)
Syntactically valid for both Crystal and Ruby
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.
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 type inference is pretty smart and pretty transparent.
Empty arrays (and hashes) require a type declaration.
Crystal can coerce types so that this still works
Overloading ensures that methods are responsible for their inputs while keeping the code easy to read.
Type Unions are a combination of two or more types. With type unions, dynamic structures can accept complex typing.
In Ruby, single and double quotes may be interchangeable for String literals. You can't do that in Crystal.
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.
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
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.
Recursive Golang Fibonacci Calculator
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 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
Ruby with Fibers needs a loop to keep returning execution to the Fibers. It is otherwise fairly similar.
Crystal Fibers <-> Ruby Threads
Ruby -- real system threads; heavy; preemptive
Ruby with Threads is more like the Golang program, because of preemption of the Threads.
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 |
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 runs an event loop for you, for efficient concurrency when working with IO.
Massively concurrent because the event loop efficiently handles IO wait to keep fibers maximally busy.
The Ruby version, with Threads, is pretty similar.
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
If fibers mutate data without synchronizing access, bad things can happen. Use a Channel or use a Mutex, or be prepared to be surprised.
Carol should be 30!
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 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 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.
It's complicated.
An entire presentation could be done just on metaprogramming with Crystal
Crystal supports linking to C libraries directly within the language syntax.
Crystal is comes pre-equipped with some useful tooling
Testing
Scaffolding
Sharding
Documentation
Formatting
and More....
Shards are the Crystal equivalent of Rubygems -- it is a packaging and dependency management system for Crystal libraries and applications.
Autogenerate projects docs
Automatically clean up formatting
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.
@r = [] of Int32
getter dice : Int32
getter plus : Int32
That's it! It works.
0.36.1 was released 02 Feb 2021
The next release is expected to be 1.0.0-pre1.
Thank You