Crystal言語入門 - Rubyライクな構文で高速動作するコンパイル言語


Crystal言語とは

Crystalは、Rubyのような読みやすく書きやすい構文を持ちながら、C言語に匹敵する実行速度を実現するコンパイル言語です。2014年に登場し、静的型付けと型推論を組み合わせることで、開発者の生産性とパフォーマンスの両立を目指しています。

Crystalの主な特徴

  • Rubyライクな構文: Rubyに似た読みやすい構文
  • 静的型付け + 型推論: 明示的な型注釈なしで型安全性を確保
  • 高速な実行速度: LLVMベースのコンパイラによりC言語並みの速度
  • 並行処理: ファイバーベースの軽量並行処理
  • C言語バインディング: 既存のC言語ライブラリを簡単に利用可能
  • ガベージコレクション: メモリ管理を自動化

インストール

macOS

# Homebrewを使用
brew install crystal

# バージョン確認
crystal --version

Linux (Ubuntu/Debian)

# 公式リポジトリを追加
curl -fsSL https://crystal-lang.org/install.sh | sudo bash

# インストール
sudo apt install crystal

# バージョン確認
crystal --version

Docker

# Dockerイメージを使用
docker pull crystallang/crystal

# Crystalプログラムを実行
docker run --rm -v $(pwd):/workspace -w /workspace crystallang/crystal crystal run hello.cr

基本構文

Hello World

# hello.cr
puts "Hello, Crystal!"

# 実行
# crystal run hello.cr

# コンパイル
# crystal build hello.cr
# ./hello

変数と型推論

# 型推論による変数定義
name = "Crystal"        # String
age = 11                # Int32
price = 99.99           # Float64
is_fast = true          # Bool

# 明示的な型注釈
count : Int32 = 100
message : String = "Hello"

# 定数
MAX_SIZE = 1000

# 複数代入
x, y, z = 1, 2, 3

基本的なデータ型

# 数値型
int32_num = 42          # Int32
int64_num = 42_i64      # Int64
float_num = 3.14        # Float64
float32_num = 3.14_f32  # Float32

# 文字列
str = "Hello"
multiline = <<-TEXT
  複数行の
  文字列
  TEXT

# シンボル
symbol = :hello

# 配列
numbers = [1, 2, 3, 4, 5]
mixed = [1, "two", 3.0]  # Union型 (Int32 | String | Float64)

# ハッシュ
person = {
  "name" => "Alice",
  "age" => 30
}

# 名前付きタプル
config = {name: "App", version: "1.0.0"}
puts config[:name]  # => App

# タプル
tuple = {1, "hello", true}
puts tuple[0]  # => 1

関数とメソッド

関数定義

# 基本的な関数
def greet(name)
  "Hello, #{name}!"
end

puts greet("Crystal")  # => Hello, Crystal!

# 型注釈付き関数
def add(a : Int32, b : Int32) : Int32
  a + b
end

# デフォルト引数
def greet(name = "World")
  "Hello, #{name}!"
end

# 名前付き引数
def create_user(name : String, age : Int32, admin : Bool = false)
  {name: name, age: age, admin: admin}
end

user = create_user(name: "Alice", age: 30)

ブロックとイテレーション

# ブロック付きメソッド
[1, 2, 3, 4, 5].each do |num|
  puts num
end

# 短縮形
[1, 2, 3, 4, 5].each { |num| puts num }

# map, select, reject
numbers = [1, 2, 3, 4, 5]
doubled = numbers.map { |n| n * 2 }
evens = numbers.select { |n| n.even? }
odds = numbers.reject { |n| n.even? }

# reduce
sum = numbers.reduce(0) { |acc, n| acc + n }

クラスとオブジェクト指向

クラス定義

class Person
  # プロパティ定義
  property name : String
  property age : Int32

  # ゲッターのみ
  getter email : String

  # セッターのみ
  setter phone : String?

  # コンストラクタ
  def initialize(@name, @age, @email)
    @phone = nil
  end

  # メソッド
  def greet
    "Hello, I'm #{@name}, #{@age} years old."
  end

  # クラスメソッド
  def self.create_anonymous
    new("Anonymous", 0, "no-email@example.com")
  end
end

# インスタンス生成
person = Person.new("Alice", 30, "alice@example.com")
puts person.greet
puts person.name

# クラスメソッド呼び出し
anon = Person.create_anonymous

継承とモジュール

# 継承
class Employee < Person
  property company : String

  def initialize(name, age, email, @company)
    super(name, age, email)
  end

  def greet
    "#{super} I work at #{@company}."
  end
end

# モジュール(ミックスイン)
module Walkable
  def walk
    "Walking..."
  end
end

module Runnable
  def run
    "Running..."
  end
end

class Animal
  include Walkable
  include Runnable

  property name : String

  def initialize(@name)
  end
end

dog = Animal.new("Buddy")
puts dog.walk  # => Walking...
puts dog.run   # => Running...

型システム

Union型

# 複数の型を受け入れる
def print_value(value : Int32 | String)
  puts value
end

print_value(42)
print_value("hello")

# Nil許容型
name : String? = nil  # String | Nil
name = "Crystal"

# 型チェック
if name.is_a?(String)
  puts name.upcase
end

ジェネリクス

# ジェネリッククラス
class Box(T)
  property value : T

  def initialize(@value)
  end

  def get : T
    @value
  end
end

int_box = Box(Int32).new(42)
str_box = Box(String).new("hello")

# ジェネリックメソッド
def first(array : Array(T)) : T forall T
  array[0]
end

puts first([1, 2, 3])      # => 1
puts first(["a", "b", "c"]) # => a

構造体 (Struct)

# 値型として扱われる
struct Point
  property x : Int32
  property y : Int32

  def initialize(@x, @y)
  end

  def distance_from_origin
    Math.sqrt(x ** 2 + y ** 2)
  end
end

p1 = Point.new(3, 4)
puts p1.distance_from_origin  # => 5.0

# 構造体はコピーされる(値渡し)
p2 = p1
p2.x = 10
puts p1.x  # => 3 (変更されない)

マクロ

Crystalの強力な機能の一つがマクロです。コンパイル時にコードを生成できます。

# 基本的なマクロ
macro define_property(name)
  def {{name}}
    @{{name}}
  end

  def {{name}}=(value)
    @{{name}} = value
  end
end

class User
  define_property name
  define_property email

  def initialize
    @name = ""
    @email = ""
  end
end

# デバッグマクロ
macro debug(expression)
  puts "{{expression}} = #{{{expression}}}"
end

x = 42
debug(x)  # => x = 42
debug(x * 2)  # => x * 2 = 84

# ボイラープレート削減
macro define_crud(model)
  class {{model}}Repository
    def find(id : Int32)
      # データベースから検索
    end

    def save({{model.id.downcase}} : {{model}})
      # データベースに保存
    end

    def delete(id : Int32)
      # データベースから削除
    end
  end
end

class User
  property id : Int32?
  property name : String
end

define_crud(User)

# UserRepositoryクラスが自動生成される
repo = UserRepository.new

並行処理 - ファイバー

Crystalはファイバーベースの軽量並行処理をサポートしています。

# ファイバーの生成
spawn do
  puts "ファイバー1開始"
  sleep 1
  puts "ファイバー1終了"
end

spawn do
  puts "ファイバー2開始"
  sleep 0.5
  puts "ファイバー2終了"
end

sleep 2  # メインファイバーを待機

# チャネルを使った通信
channel = Channel(String).new

spawn do
  channel.send("Hello from fiber!")
end

message = channel.receive
puts message

# 複数のファイバーでの並行処理
results = Channel(Int32).new

10.times do |i|
  spawn do
    sleep rand
    results.send(i * i)
  end
end

10.times do
  puts results.receive
end

HTTPサーバー構築

Crystalの標準ライブラリでHTTPサーバーを簡単に構築できます。

require "http/server"

server = HTTP::Server.new do |context|
  path = context.request.path

  case path
  when "/"
    context.response.content_type = "text/html"
    context.response.print <<-HTML
      <!DOCTYPE html>
      <html>
        <head><title>Crystal Server</title></head>
        <body>
          <h1>Hello from Crystal!</h1>
          <p>This is a simple HTTP server.</p>
        </body>
      </html>
      HTML
  when "/api/hello"
    context.response.content_type = "application/json"
    context.response.print({message: "Hello, API!"}.to_json)
  else
    context.response.status_code = 404
    context.response.print "Not Found"
  end
end

address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen

JSON処理

require "json"

# JSON Serializable
class User
  include JSON::Serializable

  property name : String
  property age : Int32
  property email : String

  def initialize(@name, @age, @email)
  end
end

# オブジェクト → JSON
user = User.new("Alice", 30, "alice@example.com")
json_string = user.to_json
puts json_string
# => {"name":"Alice","age":30,"email":"alice@example.com"}

# JSON → オブジェクト
json = %{{"name":"Bob","age":25,"email":"bob@example.com"}}
user = User.from_json(json)
puts user.name  # => Bob

# JSON配列
users = [
  User.new("Alice", 30, "alice@example.com"),
  User.new("Bob", 25, "bob@example.com")
]

json_array = users.to_json
puts json_array

parsed_users = Array(User).from_json(json_array)
parsed_users.each do |u|
  puts u.name
end

データベース接続

Crystalには複数のデータベースシャードがあります。

# PostgreSQL
require "pg"

DB.open "postgres://user:password@localhost/mydb" do |db|
  # クエリ実行
  db.exec "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, email TEXT)"

  # データ挿入
  db.exec "INSERT INTO users (name, email) VALUES ($1, $2)", "Alice", "alice@example.com"

  # データ取得
  db.query "SELECT * FROM users" do |rs|
    rs.each do
      id = rs.read(Int32)
      name = rs.read(String)
      email = rs.read(String)
      puts "#{id}: #{name} (#{email})"
    end
  end
end

# SQLite
require "sqlite3"

DB.open "sqlite3://./data.db" do |db|
  db.exec "CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT, price REAL)"

  db.exec "INSERT INTO products (name, price) VALUES (?, ?)", "Product A", 99.99

  db.query "SELECT * FROM products" do |rs|
    rs.each do
      puts "#{rs.read(Int64)}: #{rs.read(String)} - $#{rs.read(Float64)}"
    end
  end
end

Crystalフレームワーク - Kemal

Kemalは、SinatraライクなCrystal製Webフレームワークです。

# shard.yml
dependencies:
  kemal:
    github: kemalcr/kemal

# app.cr
require "kemal"

get "/" do
  "Hello Kemal!"
end

get "/hello/:name" do |env|
  name = env.params.url["name"]
  "Hello #{name}!"
end

post "/api/users" do |env|
  name = env.params.json["name"].as(String)
  email = env.params.json["email"].as(String)

  # ユーザー作成処理

  env.response.content_type = "application/json"
  {id: 1, name: name, email: email}.to_json
end

Kemal.run

パフォーマンス比較

Crystalの実行速度をRubyと比較してみましょう。

# fibonacci.cr
def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

start = Time.monotonic
result = fibonacci(40)
elapsed = Time.monotonic - start

puts "Result: #{result}"
puts "Time: #{elapsed.total_seconds}s"

# Crystalでコンパイル実行: 約0.5秒
# Rubyで実行: 約30秒(約60倍の差)

ベストプラクティス

1. 型注釈の活用

# パブリックAPIには型注釈をつける
def calculate_total(items : Array(Item)) : Float64
  items.sum(&.price)
end

# プライベートメソッドは型推論に任せてもOK
private def helper(value)
  value * 2
end

2. Nilableの適切な処理

def find_user(id : Int32) : User?
  # ...
end

# 安全な使い方
if user = find_user(123)
  puts user.name
else
  puts "User not found"
end

# try演算子
user.try(&.name)  # userがnilでもエラーにならない

3. 構造体 vs クラス

# 小さなデータはStructを使う(値型、コピーされる)
struct Point
  property x : Int32
  property y : Int32
end

# 大きなデータや共有が必要ならClass(参照型)
class User
  property name : String
  property posts : Array(Post)
end

まとめ

Crystalは、Rubyの美しい構文とC言語の高速性を組み合わせた魅力的な言語です。以下のような場合に特におすすめです:

  • 高速なWebサーバー: APIサーバーやマイクロサービス
  • CLIツール: コマンドラインアプリケーション
  • システムプログラミング: 低レベルなシステム開発
  • Rubyからの移行: パフォーマンスが必要な部分の置き換え

型推論により、動的型付け言語の書きやすさと静的型付け言語の安全性を両立しており、生産性を保ちながら高速なアプリケーションを開発できます。ぜひCrystalを試してみてください!