The Power of Crystal:A language for humans and computers
Johannes Müller – Crystal Core Team / Manas.Tech
Agenda
Intro to Crystal
Comparison with Ruby
Combined Power
Learnings for Ruby devs
Last point: In case you go out of this talk and feel like you never want to have anything to do with
Crystal,
there are some useful insights when looking at Ruby from a Crystal perspective which might help you become
a better Ruby programmer.
Johannes Müller Manas.Tech
jmuller@manas.tech
github: @straight-shoota
mastodon: @straightshoota
Follow the slides
straight-shoota.github.io/power-of-crystal
crystal-lang.org/install | play.crystal-lang.org
Slide info
....
When I learned Ruby, it was the first time I fell in love with a programming language (fun)
Later Crystal was the second time
it expanded on Ruby, adding some extra flavour that I find very attractive
Crystal was/is still young, so lots of stuff was missing and I started contributing (stdlib and compiler)
Eventually became a Core Team member
now working on Crystal full time. I'm principal engineer of the Crystal team at Manas, the place were
Crystal was born and continues stewardship of the project.
Manas.Tech
Manas is a software agency from Argentina with an international team
we use lots of different technologies
lot of love for Ruby (on Rails)
Ruby has some great properties:
Benefits of Ruby
User-friendly syntax
Versatile and productive
Simple and intuitive
I just picked some examples, we could continue this list
These are a result of the creators vision.
Matz (1993): “What would a really good programming language look
like?”
puts "Hello, Ruby!"
Ary (2011): “What if Matz's good programming language could
somehow statically compile?”
puts "Hello, Crystal!"
Journey back in time, to a dude in Japan. Yukihiro Matsumoto
Matz (father of Ruby) realized his vision of “a good” programming language. Many people agree on
that and found it useful
Hello Ruby
approval rate is probably pretty high in this room here
Ary (father of Crystal) wondered “what if we could somehow
statically compile Ruby?”
Hello Crystal
Started as experiment. Compiler originally written in Ruby. Grew into language and ecosystem.
Matz is grandfather of Crystal
The reasons to change anything about Ruby is that it has some downsides
The story behind Crystal
Yukihiro "Matz" Matsumoto - Pushing boundaries | Crystal
1.0 Conference
Ruby Drawbacks
Type checking
Raw performance
Software distribution
Concurrency
type checking is possible via RBS or Sorbet. But those are bolted on and limited; challenging with
compatibility and coherence of the ecosystem.
Ruby 3 and JIT increased performance significantly. But dynamic nature puts it still
on a different level of efficiency compared to native code.
Ruby requires the language runtime. It's hard to distribute software to non-developers.
Writing concurren software can be an adventure
Some examples. I'm sure there are more issues with Ruby.
A programming language is always a set of features and compromises
Crystal's answers
Goodies from Ruby's lineage
Static typing with type inference
Compiles to highly efficient machine code
Strong, intuitive concurrency model
Crystal takes the best from Ruby and adds some more
moves more to the static without loosing to much dynamic feel
...
fresh approach to the underlying ideas of Ruby as a new but similar language.
somewhat similar to alternative Ruby implementations (JRuby, Rubinious, or mruby), but it goes a step
further
still very similar and largely identical to Ruby.
You can leverage your Ruby knowledge for Crystal.
Lets explore some commonalities and differences
Syntax
# src/hello.crb
# Polyglot program that is valid Ruby and Crystal
# and can tell one from the other
LANGUAGE = Array.to_s == "Array(T)" ? "Crystal" : "Ruby"
puts "Hello, #{LANGUAGE}!"
$ ruby src/hello.crb
Hello, Ruby!
$ crystal src/hello.crb
Hello, Crystal!
extremely similar
Detection uses a trick: `Array.to_s` returns `"Array(T)"` in Crystal. The `T` is a generic type argument and we'll come to that
later
Batteries included
# A very basic HTTP server
require "http/server"
server = HTTP::Server.new do |context|
context.response.content_type = "text/plain"
context.response.print "Hello, Crystal!"
end
address = server.bind_tcp(8080)
puts "Listening on http://#{address}"
server.listen
This could be a Ruby program, right? Just using some `HTTP` library
http/server
is part of Crystal's standard library
Crystal comes with a lot of useful tools out of the box
requests are handled in individual fibers, enabling concurrency (not that useful if you just print hello Crystalm but imagine bigger)
the last call blocks until the process is terminated
$ crystal build --release src/http-server.cr
$ ./http-server &
$ wrk -t8 -c400 -d60s http://localhost:8080/
Running 1m test @ http://localhost:8080/
8 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 7.11ms 1.09ms 17.66ms 79.34%
Req/Sec 7.07k 1.15k 62.53k 85.64%
3373196 requests in 1.00m, 334.56MB read
Requests/sec: 56126.81
Transfer/sec: 5.57MB
Static Typing
Type safety
Feels dynamic
def add(a, b)
a + b
end
add 1, 2 # => 3
add "foo", "bar" => "foobar"
statically typed, but feels dynamic through type inference
Compiler realizes there are two instantiations of `#add`, one with both parameters being integers, one with both being string
explicit typing is rarely necessary and in most places automatically inferred
Note: We could add type information, and it's often helpful to establish invariants and document expectations
Static Typing
Type safety
Feels dynamic
def add(a, b)
a + b
end
add 1, 2 # => 3
add "foo", "bar" => "foobar"
add "foo", 2 # Error: instantiating 'add(String, Int32)
# Error: expected argument #1 to 'String#+'
# to be Char or String, not Int32
Ruby has a similar error, but it only appears when the code executes
In Crystal you get the error at compile time
Excludes a whole class of common errors in Ruby (like
NoMethodError: undefined method for nil:NilClass
)
in Ruby you need to do extensive testing to lock this down
Static Typing
ary = [1, 2, 3]
ary.class # => Array(Int32)
ary << 4
typeof(ary[0]) # Int32
ary << "foo" # Error: expected argument #1 to 'Array(Int32)#<<'
# to be Int32, not String
Static Typing
ary = [1, 2, 3] of Int32 | String
ary.class # => Array(Int32 | String)
ary << 4
typeof(ary[0]) # Int32 | String
ary << "foo"
Static Typing
[] # Error: for empty arrays use '[] of ElementType'
[] of String
# or
Array(String).new
Static Typing
instance variables
class Foo
@bar = 123
def initialize(@baz : String)
end
end
Instance variable need to be explicitly typed
sometimes the compiler can infer the type
If not, a type restriction is mandatory
Compilation
$ crystal build src/hello.crb
$ ls -sh hello
2,0M hello-world
$ file hello
hello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d7768fdb368d11e8e08c9bcaea7cc1a629914f04, for GNU/Linux 3.2.0, with debug_info, not stripped
$ ./hello
Hello, Crystal!
Compile first, then run: Extra step, takes some time, but runtime efficiency pays off
Result is a dynamically linked executable for my current system
link some shared system libraries but no Crystal code
distribution of binaries is easy, binaries include the language runtime (especially when linked statically)
cross compilation is also possible
LLVM is used for codegen by other languages as well (Rust for example, or clang)
optimizations are sometimes ridiculously good
All code needs to be fixed at compile time. There’s no `eval` or `send`. Cannot modify program at runtime. Core feature of Ruby's dynamicness.
Compilation
Runtime is embedded into the binary
LLVM backend generates efficient code
Limited dynamic language features
Metaprogramming: macro
Code that writes other code at compile time
macro getter(var)
# macro body
end
getter foo
Macro expansion:
# macro body
macro
has same structure as def
, just differen keyword
simplified example from stdlib
equivalent to `attr_reader`
Ruby approach modifies the program at runtime, which is not possible with a compiled program
compile time macros offer similar features. It’s code that writes other code. Looks similar to a liquid template.
there's also hooks like macro inherited
Metaprogramming: macro
Code that writes other code at compile time
macro getter(var)
def {{ var.id }}
@{{ var.id }}
end
end
getter foo
Macro expansion:
def foo
@foo
end
macro
has same structure as def
, just differen keyword
simplified example from stdlib
equivalent to `attr_reader`
Ruby approach modifies the program at runtime, which is not possible with a compiled program
compile time macros offer similar features. It’s code that writes other code. Looks similar to a liquid template.
there's also hooks like macro inherited
Metaprogramming: def
class Foo
def ==(other)
{% for ivar in @type.instance_vars %}
return false unless @{{ivar.id}} == other.@{{ivar.id}}
{% end %}
true
end
end
Macro expansion:
# macro expansion of method body with ivars @bar and @baz
return false unless @baz == other.@baz
return false unless @bar == other.@bar
true
API gotchas
#includes?
instead of #include?
Only #reduce
, no #inject
Only #size
, no #length
Overall, the standard library has very many similarities
but there are some explicit choices
better grammar
avoid aliases
Easier for newcomers
No need to learn two things when one suffices
Concurrency
Lightweight threads, CSP-style
spawn foo()
launches a Fiber
Deeply integrated into the runtime
socket.puts "ping"
Communication via Channel
communicating sequential processes (used also by Elixir and Go)
all code is implicitly async
but no callback hell
basically every time you need to wait on something (like reading from a socket), the scheduler
just continues running another fiber and the current one wakes up when its ready to continue.
multi-threading is still experimental, but expected to be completed within the next couple of months
Dependencies
# shard.yml
name: my-first-crystal-app
version: 1.0.0
dependencies:
mysql:
github: crystal-lang/crystal-mysql
version: >=0.16.0
$ shards install
$ shards update
Shards instead of gems
CLI similar to bundler
decentralized dependency resolution, no registry
dependencies point to repositories
Ruby with Crystal
Embed Crystal code directly in Ruby
require 'crystalruby'
module MyTestModule
# The below method will be replaced by a compiled Crystal version
# linked using FFI.
crystalize [a: :int, b: :int] => :int
def add(a, b)
a + b
end
end
# This method is run in Crystal, not Ruby!
MyTestModule.add(1, 2) # => 3
wouterken/crystalruby
Crystal with Ruby
Embed an mruby interpreter in Crystal
require "anyolite"
Anyolite::RbInterpreter.create do |rb|
rb.execute_script_line(%[puts "Hello from Ruby"])
end
Anyolite/anyolite
Combine Ruby & Crystal
delegate performance-critical workloads
background job processing
service backend
Other forms of collaboration
Crystal is as good as other alternatives (Rust, Go, Zig, C ...)
let Crystal handle performance-critical jobs
bg-jobs via sidekiq adapter etc.
e.g. websocket server, streaming server
Learnings for Rubyists
avoiding repeat calculations
abstract def bar : String | Nil
abstract def foo(arg : String)
if bar
foo(bar) # Error: expected argument #1 to 'foo'
# to be String, not (String | Nil)
end
if b = bar
foo(b)
end
Learnings for Rubyists
{
"action" => "foo",
"credentials" => {
"id" => "abc",
"secret" => "psst"
},
"value" => 12,
}
In Ruby there's often a tendency to put semi-structured data into shapeless hashes
This is problematic in Crystal due to static typing
explicitly structured types are more efficient, descriptive
for static key names, use a type
Conclusion
Crystal can be a useful asset in your toolbox
Enhance your Ruby project
Knowing Ruby makes you almost a Crystal developer
Knowing Crystal makes you a better Ruby developer