9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Crystal の小さな本

Last updated at Posted at 2023-10-08

続・Crystal の小さな本 もあります。

Crystal で Hello World!

Crystal という Ruby ライクな言語は、C や Go の代わりとして使えます。つまり、Ruby と異なりインタプリタでなく実行ファイルを生成するコンパイラ言語です。

インストール方法は Ubuntu の場合こちらのページになります。

2023/9 段階ではバージョン 1.9.2 になっていますが、Windows 版はプレビュー段階ですし、Linux 版も ARM 版はありません。

と言っても内部では C のソースを生成してそれを C コンパイラがビルドして実行ファイルを生成しているようです。

Hello World! のコードは ruby と全く同じで次のようになっています。

puts "Hello World!"

このプログラムをビルドして実行するには次のコマンドを実行します。

$ crystal hello_world.cr

すると、次のようなエラーが出てしまいました。(Ubuntu 23.04LTS で)

/usr/bin/ld: -levent が見つかりません: そのようなファイルやディレクトリはありません
collect2: error: ld returned 1 exit status
Error: execution of command failed with code: 1: cc "${@}" -o /home/user/.cache/crystal/crystal-run-hello.tmp -rdynamic -L/usr/bin/../lib/crystal -lm -lgc -lpthread -levent -lrt -lpthread -ldl

つまり、「event というライブラリがないよ!」 とのことです。

これは、Ubuntu 23.04LTS のインストール時に入ってくれなかったようです。そこで、次のコマンドでインストールします。

$ sudo apt install libevent-dev

そしてもう一度、ビルドを行ったら次のように表示できました。

$ crystal hello_world.cr
Hello World!
$

これだと、実行ファイルはどこかにできているはずですが、このフォルダ内には作られません。実行ファイルが必要な場合は次のようにします。

$ crystal build hello_world.cr

これだと、現在のフォルダに実行ファイル hello_world ができているはずです。

crystal コマンドのヘルプは crystal だけあるいは crystal help で表示できます。

$ crystal
Usage: crystal [command] [switches] [program file] [--] [arguments]

Command:
    init                     generate a new project
    build                    build an executable
    clear_cache              clear the compiler cache
    docs                     generate documentation
    env                      print Crystal environment information
    eval                     eval code from args or standard input
    i/interactive            starts interactive Crystal
    play                     starts Crystal playground server
    run (default)            build and run program
    spec                     build and run specs (in spec directory)
    tool                     run a tool
    help, --help, -h         show this help
    version, --version, -v   show version

Run a command followed by --help to see command specific information, ex:
    crystal <command> --help

特定のフォルダに実行ファイルを作るには -o オプションを指定します。このオプションも含め build オプションは crystal build –help で表示できます。

$ crystal build --help
Usage: crystal build [options] [programfile] [--] [arguments]

Options:
    --cross-compile                  cross-compile
    -d, --debug                      Add full symbolic debug info
    --no-debug                       Skip any symbolic debug info
    -D FLAG, --define FLAG           Define a compile-time flag
    --emit [asm|obj|llvm-bc|llvm-ir] Comma separated list of types of output for the compiler to emit
    -f text|json, --format text|json Output format text (default) or json
    --error-trace                    Show full error trace
    -h, --help                       Show this message
    --ll                             Dump ll to Crystal's cache directory
    --link-flags FLAGS               Additional flags to pass to the linker
    --mcpu CPU                       Target specific cpu type
    --mattr CPU                      Target specific features
    --mcmodel MODEL                  Target specific code model
    --warnings all|none              Which warnings to detect. (default: all)
    --error-on-warnings              Treat warnings as errors.
    --exclude-warnings <path>        Exclude warnings from path (default: lib)
    --no-color                       Disable colored output
    --no-codegen                     Don't do code generation
    -o                               Output filename
    --prelude                        Use given file as prelude
    --release                        Compile in release mode
    -s, --stats                      Enable statistics output
    -p, --progress                   Enable progress output
    -t, --time                       Enable execution time output
    --single-module                  Generate a single LLVM module
    --threads NUM                    Maximum number of threads to use
    --target TRIPLE                  Target triple
    --verbose                        Display executed commands
    --static                         Link statically
    --stdin-filename                 Source file name to be read from STDIN

Crystal ソースのビルド

次のスクリプトは1つのソースからなる Crystal プログラムをビルドするものです。出力先が ./bin なので、./bin というフォルダを事前に作っておくか、必要ならスクリプトを修正してください。

#!/usr/bin/bash
 
if [ $# -eq 0 ]; then
  echo "Usage: build.sh source (without file extension)"
  exit 1
fi
 
echo "Compiling .."
crystal build -o ./bin/$1 -d $1.cr
 
if [ $? -eq 0 ]; then
  echo ".. OK"
fi

Crystal プロジェクトの作り方

crystal コマンドの init サブコマンドを使って新規プロジェクトを作ることができます。次のコマンド例は “macro_test” というアプリ・プロジェクトを作るものです。

$ crystal init app macro_test

もし、これがライブラリなら init lib とします。

コマンドを実行すると下のようなフォルダとファイルが生成されます。

$ crystal init app macro_test
    create  /home/user/workspace/Crystal/macro_test/.gitignore
    create  /home/user/workspace/Crystal/macro_test/.editorconfig
    create  /home/user/workspace/Crystal/macro_test/LICENSE
    create  /home/user/workspace/Crystal/macro_test/README.md
    create  /home/user/workspace/Crystal/macro_test/shard.yml
    create  /home/user/workspace/Crystal/macro_test/src/macro_test.cr
    create  /home/user/workspace/Crystal/macro_test/spec/spec_helper.cr
    create  /home/user/workspace/Crystal/macro_test/spec/macro_test_spec.cr
Initialized empty Git repository in /home/user/workspace/Crystal/macro_test/.git/

src フォルダの下にある macro_test.cr がアプリケーションのソースファイルです。

spec フォルダには spec_helper.cr と macro_test_spec.cr という2つのファイルが生成されます。これらは Crystal におけるテストコードの一種です。

spec_helper.rb ファイルには、spec を実行するために必要な設定が含まれています。spec を利用するためにはこのファイルをインポートします。

macro_test_spec.cr は実際のテストを行うコードで、次のようになっています。

require "./spec_helper"
 
describe MacroTest do
  # TODO: Write tests
 
  it "works" do
    false.should eq(true)
  end
end

Shards を使う

プロジェクトで標準ライブラリ以外のパッケージを使う必要がある場合は、shard.yml が必要になる。

具体的には 「Shards とは何か?」を参照。

Crystal と Ruby の文法上の違い

Crystal は Ruby と似た文法を取っていますが、次のような違いがあります。

  • 静的型付け Crystalは静的型付け言語ですが、Rubyは動的型付け言語です。つまり、Crystalでは変数や式の型を明示的に指定する必要がありますが、Rubyでは型を指定する必要がありません。
  • ブロック Crystalではブロックを処理する際に、do..endブロックやprocブロックを使用できます。Rubyではdo..endブロックのみを使用できます。
  • 配列 Crystalでは配列の要素に型を指定することができます。Rubyでは配列の要素に型を指定することはできません。
  • メソッド Crystalではメソッドをオーバーライドすることができます。Rubyではメソッドをオーバーライドすることはできません。

これらの違いは、CrystalとRubyの処理速度やメモリ使用量に影響を与える可能性があります。一般的に、静的型付け言語は動的型付け言語よりも処理速度が速く、メモリ使用量が少ない傾向にあります。

(Google Bard による)

さらに具体的な内容については「Crystal に for 文がない!」も参照。

Crystal: コマンドライン引数

Ruby ではコマンドライン引数は $* または ARGV で取得できます。

一方、 Crystal では $* はエラーになりました。

つまり、Crystal では Perl 風の $ 変数は使えないものがたくさんあります。

C 言語では argv はコマンド名そのものを含みますが、Crystal の ARGV ではパラメータのみです。つまり、ARGV[0] は最初のパラメータとなります。

下のコードは、コマンドライン引数一覧を表示するものですが、Ruby でも動作します。

if ARGV.size() == 0
  STDERR.puts("No arguments")
  exit 1
end
 
ARGV.each {|a| puts(a)}
 
puts("Done.")

Crystal のデバッグ

Crystal のデバッグですが、lldb と crystal 公式プラグインをインストールした VSCode で可能です。

また、下のように –debug を指定した build が必要です。

$ crystal build --debug until.cr

そして、このビルド結果である実行ファイル、この例では until を VSCode の launch.json に設定します。(下記の例を参照)

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "lldb",
            "request": "launch",
            "name": "Debug",
            "program": "${workspaceFolder}/until",
            "args": [],
            "cwd": "${workspaceFolder}"
        }
    ]
}

VSCode でこのプログラム until.cr を読み込んで実行すると下の画像のようにデバッグが可能のはずです。

Crystal: p と p! の違い

Crystal のサンプルプログラムで p! という関数をよく見かけます。

Ruby には p という関数がありますが、Crystal にも p はあります。しかし、p! だと何がそれを表示したのかが簡単にわかります。以下に例を挙げます。

a = true
b = false
p! a, b
p a, b
c = false
p !c
p! !c

これを実行すると下のように表示されます。

a # => true
b # => false
true
false
true
!c # => true

Crystal の標準ライブラリは Ruby と完全互換ではない

Crystal の標準ライブラリは Ruby のそれに近いですが、完全互換ではなく「似ている」くらいの互換性しかありません。

Dir クラスで見てみると、Dir.glob、Dir.entries はどちらにもあって互換性もあるようですが、each_child, chilren は Crystal にはありません。

現在のディレクトリを得るメソッドですが、Crystal は current ですが、Ruby は pwd あるいは getwd です。

ユーザディレクトリを得る home というメソッドは Ruby にしかありません。

ディレクトリを変更するメソッドは Crystal では cd ですが、Ruby では chdir です。

全般的に Ruby のほうがメソッドの数は多く「かゆいところに手が届く」ようなメソッドがありますが、Crystal では「これがあればどうにかなる」というメソッドしかありません。

Ruby と互換性を持たせたければ、メソッドを自作する必要があります。

Crystal の文字列

Crystal の文字列は UTF-8 でエンコードされている。そして、String クラスのインスタンスである。

次に String クラスのメソッドの使用例を示す。

# String: https://crystal-lang.org/api/String.html
s = "あいうえ"
p! typeof(s)  # String
p! typeof("abcd")  # 半角文字だけでも String 型
p! s  # 文字列の内容
s = s + "オ"  # 結合
p! s  # 結合されたか確認
p! s[2]  # 文字を抽出
p! typeof(s[2]) # Char 型
p! s.size  # 文字数
p! s.bytesize  # バイト数
p! s == "あいうえオ"  # 比較
p! s <=> "あいうえオ"  # 比較
p! s[9]?  # []? を使うとインデックスが不正な場合は Nil になる。
p! s[1..3]?  #[]? は文字列スライスを返す。(レンジが不正な場合は Nil)
p! s[1, 3]  # スライスの開始位置と長さを指定して切り出す。
p! s =~ /う/  # 文字列に文字や部分文字列が含まれている場合は、その位置を返す。(正規表現が使える)
s1 = "A"*4  # 繰り返し
p! s1
n = "1234".to_i32  # 整数に変換
p! typeof(n), n
p! n.to_s  # 文字列へ変換
# 結合演算子
s2 = String.build do |str|
  str << "hello "
  str << 1
end
p! s2
# 文字列のメソッド
p! s2.starts_with?("hel")  # 先頭の文字列を判別
p! s2.ends_with?("3")  # 終わりの文字列を判別
p! s2.index("ll")  # 部分文字列の位置
p! s2.rindex('o')  # 後ろから部分文字列の位置を探す
p! s2.reverse()  # 文字を逆順にする。
p! s2.upcase()  # アルファベットを大文字にする。
p! "\tstrip".strip()  # 空白文字を取り除く
p! s2.gsub('l', 'L')  # 文字や文字列を置換する
p! s2.sub('l', 'L')  # 最初に見つかった文字や文字列を置換する
a = "1,2,3".split(',')  # 文字列を分割した配列を得る。
p! a
p! a.join(":")  # 文字列配列の要素を文字や文字列で結合する。

このプログラムの実行例を下に示す。

$ ./bin/utf8string
typeof(s) # => String
typeof("abcd") # => String
s # => "あいうえ"
s # => "あいうえオ"
s[2] # => 'う'
typeof(s[2]) # => Char
s.size # => 5
s.bytesize # => 15
s == "あいうえオ" # => true
s <=> "あいうえオ" # => 0
s[9]? # => nil
s[1..3]? # => "いうえ"
s[1, 3] # => "いうえ"
s =~ (/う/) # => 2
s1 # => "AAAA"
typeof(n) # => Int32
n         # => 1234
n.to_s # => "1234"
s2 # => "hello 1"
s2.starts_with?("hel") # => true
s2.ends_with?("3") # => false
s2.index("ll") # => 2
s2.rindex('o') # => 4
s2.reverse() # => "1 olleh"
s2.upcase() # => "HELLO 1"
"\tstrip".strip() # => "strip"
s2.gsub('l', 'L') # => "heLLo 1"
s2.sub('l', 'L') # => "heLlo 1"
a # => ["1", "2", "3"]
a.join(":") # => "1:2:3"

Crystal の配列

Crystal の配列は Array クラスのインスタンスです。長さは可変長で要素の型は複数指定可能です。下に Array の使用例を示します。

# 配列 (Array): https://crystal-lang.org/api/Array.html
a = [1, 2, 3]
p a
b = [0, "Data", 1.23, nil]  # 型の異なる要素からなる配列
p b
c = Array(Int32).new  # 整数配列
p c.size  # 長さ
# 要素を追加する。
c << 9
c << -6
c << -5
p c
# 整数配列に型の違う要素を追加する。
# c << "String" はコンパイルエラーになる。
# a << "String" コンパイルエラーになる。
b << "String"  # OK
p b
# 整数と文字列を許容する配列
d = Array(Int32 | String).new
d << 0
d << "any"
p d

この例には長さ 0 の配列が含まれていません。他の言語では [] と書くことが多いですが、Crystal では型を指定する必要があるので、そういう書き方はエラーになります。つまり、次のような感じで書きます。

a = Array(Int32).new あるいは [] of Int32

以下にこの実行例を示します。

$ ./bin/array
[1, 2, 3]
[0, "Data", 1.23, nil]
0
[9, -6, -5]
[0, "Data", 1.23, nil, "String"]
[0, "any"]

Array は可変長ですが、固定長の StaticArray クラスもあります。下に StaticArray の使用例を示します。

# 固定長配列 (StaticArray): https://crystal-lang.org/api/StaticArray.html
a = StaticArray(Int32, 4).new(0)
p! a
p! a.size
a[0] = 1
a[3] =10
p! a
b = StaticArray['A', 'B', 'C']
p! b
c = b.clone
p! c == b
b[1] = 'a'
p! b != c
d = b.sort
p! d

この実行例を以下に示します。

$ ./bin/static_array
a # => StaticArray[0, 0, 0, 0]
a.size # => 4
a # => StaticArray[1, 0, 0, 10]
b # => StaticArray['A', 'B', 'C']
c == b # => true
b != c # => true
d # => StaticArray['A', 'C', 'a']

配列のソートについては「Crystal: ソート」の記事参照。

Crystal の標準ライブラリ

Crystal は独自の標準ライブラリを持ちますが、Ruby とは完全互換ではないので注意が必要です。

そもそも、Crystal は静的型付け言語であり、動的に変数の型が変わる Ruby との互換性を保つことはできません。

また、Ruby と比べライブラリの規模は大きくないので、求める関数が見つかるとは限りません。

Crystal 標準ライブラリ (v1.9.2) の構成は次のようになっています。

ここで必要なモジュールが見つからない場合は、ググって見つけるしかないようです。現在 (2023) のところ、RubyGems のようなものはないようです。

ArgumentError
Array
Atomic
    Flag
Base64
    Error
Benchmark
    BM
        Job
        Tms
    IPS
        Entry
        Job
BigDecimal

(以下略)

Crystal: Shards とは何か?

Crystal には shards というコマンドが付属している。このコマンドは次のようなサブコマンドを持っており、様々な機能がある。

shards build: 実行ファイルをビルドする。
shards check: 依存関係をチェックする。
shards init: 新しい shard.yml を作成する。
shards install: 依存するパッケージをインストールする。
shards list: インストールされたパッケージ一覧を表示する。
shards prune: 使われていないパッケージを削除する。
shards update: パッケージを更新する。
shards version: バージョンを表示する。

shards コマンドの主要機能は、Crystal 標準ライブラリに含まれていない外部パッケージを管理することである。

その管理を行うためのファイルが shard.yml というファイルであり、外部パッケージの場所の情報とそれらを使ったビルド情報が含まれる。

次の例は標準ライブラリに含まれていない SQLite3 のコネクタを使うプロジェクトで使う shard.yml の例である。

name: funa
version: 0.1.0
 
authors:
  - your-name-here <your-email-here>
 
targets:
  funa:
    main: src/funa.cr
 
dependencies:
  sqlite3:
    github: crystal-lang/crystal-sqlite3
 
crystal: 1.9.2
 
license: MIT

この shard.yml をどんな風に使うかと言うと、次のような感じで使用する。

1.プロジェクトを作成する。

この例では、crystal コマンドでサブコマンド init app を使って funa というプロジェクトを作成している。

$ crystal init app funa
    create  /home/user/workspace/Crystal/funa/.gitignore
    create  /home/user/workspace/Crystal/funa/.editorconfig
    create  /home/user/workspace/Crystal/funa/LICENSE
    create  /home/user/workspace/Crystal/funa/README.md
    create  /home/user/workspace/Crystal/funa/shard.yml
    create  /home/user/workspace/Crystal/funa/src/funa.cr
    create  /home/user/workspace/Crystal/funa/spec/spec_helper.cr
    create  /home/user/workspace/Crystal/funa/spec/funa_spec.cr
Initialized empty Git repository in /home/user/workspace/Crystal/funa/.git/
$

2.ビルド

ソースファイル src/funa.cr を編集したら、次のようにビルドを行う。shard.yml に記述した依存関係が間違っておらず、編集後のソースが正しければ次のように表示されるはずである。

$ shards build funa
Resolving dependencies
Fetching https://github.com/crystal-lang/crystal-sqlite3.git
Fetching https://github.com/crystal-lang/crystal-db.git
Installing db (0.12.0)
Installing sqlite3 (0.20.0)
Writing shard.lock
Building: funa
$

2 回目からは sqlite3 パッケージのダウンロードが終わっているため次のようになる。

$ shards build funa
Dependencies are satisfied
Building: funa
$

参考までに funa.cr の内容を次に示す。

require "db"
require "sqlite3"
 
DB.open("sqlite3://:memory:") do |db|
  db.close()
end

Crystal: 文字列リテラル

複数行文字列

文字列リテラル “….” には改行が含まれてもよい。

# 文字列リテラルは複数行に渡ってもよい。
str1 = "Hello
   World!"
puts str1

これを実行すると、下のように表示される。

Hello
   World!

値の埋め込み

“…..” の中の #{var} は特別に扱われ、変数 var の値がその文字列に埋め込まれる。

i = 1200
str2 = "i = #{i}"
puts str2

これを実行すると、下のように表示される。

i = 1200

パーセント文字列リテラル

% 文字列リテラルは %[….] などの形式の文字列で、この形式の文字列は二重引用符 ” はそのまま文字として使える。%[….] のカッコはカギカッコでなくてもよい。つまり、文字列内にカッコが含まれる場合、別のカッコを使用できる。また % の代わりに %q でも同じである。

puts %(hello ("world")) # => "hello (\"world\")"
puts %[hello ["world"]] # => "hello [\"world\"]"
puts %{hello {"world"}} # => "hello {\"world\"}"
puts %<hello <"world">> # => "hello <\"world\">"
puts %|hello "world"|   # => "hello \"world\""

パーセント文字列配列リテラル

%w 文字列配列リテラルを使うと、文字列配列を見やすく定義できる。

puts %w(foo bar baz)  # => ["foo", "bar", "baz"]
puts %w(foo\nbar baz) # => ["foo\\nbar", "baz"]
puts %w(foo(bar) baz) # => ["foo(bar)", "baz"]

バックスラッシュで複数文字列を結合する

複数の文字列リテラルをバックスラッシュで結合できる。

str2 = "<html>\n" \
"  <head><title>Title</title></head>\n" \
"  <body></body>\n" \
"</html>\n"
puts str2

これを実行すると、下のように表示される。

<html>
  <head><title>Title</title></head>
  <body></body>
</html>

ヒアドキュメント

Ruby や Perl 風のヒアドキュメントも書くことができる。これは <<-SYMBOL で始まり、SYMBOL で終わる文字列リテラルで、バックスラッシュや改行など任意の文字がそのまま使用できる。

heredoc = <<-STRING
<html>
  <head><title>heredoc</title></head>
  <body>
    <h1>HereDoc</h1>
  </body>
</html>
STRING
puts heredoc

これを実行すると、下のように表示される。

<html>
  <head><title>heredoc</title></head>
  <body>
    <h1>HereDoc</h1>
  </body>
</html>

Crystal: SQLite3 の使い方

Crystal の標準ライブラリではデータベースはサポートされていない。

そのため、shards コマンドを使って github から SQLite3 モジュールをインポートする必要がある。

その具体的な方法は別の投稿である「Shards とは何か?」に記載してある。

その内容でインポートする部分は shart.yml の中の次のところである。

dependencies:
  sqlite3:
    github: crystal-lang/crystal-sqlite3

shards コマンドを使ってこのプロジェクトをビルド (shards build) すると、この YAML ファイルを読み込んで指定されたリポジトリから必要なモジュールをダウンロードしてプロジェクト内のディレクトリにセットアップしてくれる。
shards コマンドは内部で crystal build コマンドを起動してターゲットソースをコンパイルして実行ファイルが作成される。

$ shards build
Dependencies are satisfied
Building: funa
$

以下に、そのときの SQLite3 を使ったサンプルプログラムを示す。sqlite3://%3Amemory%3A はメモリ上にデータベースを作成することを意味する。%3A は : のアスキーコードである。(URL内では : は特別な文字であるため)

require "db"
require "sqlite3"
 
# メモリ上のデータベースに接続する。
DB.open("sqlite3://%3Amemory%3A") do |db|
  # テーブルを作成する。
  db.exec "CREATE TABLE members (id integer not null primary key, name text not null, age integer not null, info text)"
  # テーブルの3つのデータを挿入する。
  db.exec "INSERT INTO members VALUES(?, ?, ?, ?)", 1, "Abe", 15, "080-2586-8756"
  db.exec "INSERT INTO members VALUES(?, ?, ?, ?)", 2, "Aizawa", 15, "-"
  db.exec "INSERT INTO members VALUES(?, ?, ?, ?)", 3, "Aoki", 14, "080-3310-1721"
  # データの挿入が正しく行えたかクエリーを行って確認する。
  db.query "SELECT * FROM members" do |rs|
   # カラム名を表示する。
    puts "#{rs.column_name(0)} #{rs.column_name(1)} #{rs.column_name(2)} #{rs.column_name(3)}"
    # データを表示する。
    rs.each do
      puts "#{rs.read(Int32)} #{rs.read(String)} #{rs.read(Int32)} #{rs.read(String)}"
    end
  end
  # 接続を閉じる。
  db.close()
end

これを実行すると次のようになる。

$ ./bin/funa
id name age info
1 Abe 15 080-2586-8756
2 Aizawa 15 -
3 Aoki 14 080-3310-172
$

Crystal: MySQL の使い方

MySQL も他のデータベース同様、使用するためには shard コマンドでライブラリをプロジェクトにインストールする必要がある。

まず、crysla init コマンドを使ってプロジェクトを作成する。この例では koi という名前である。

$ crystal init app koi

プロジェクトが作成されたら shard.yml を開いて次の内容を targets ブロックの後に追加する。

dependencies:
  mysql:
    github: crystal-lang/crystal-mysql

プロジェクトフォルダ内で shard install コマンドを実行して crystal-mysql をそのプロジェクトにインストールする。

Crystal ソースファイル src/koi.cr を編集する。

次の例は test というデータベース上の products というテーブルにクエリーを行い、内容を表示するものである。

DB.open で指定している接続文字列の形式は次のようになっている必要がある。ただし、データベースは後からも指定できる。

mysql://ユーザ:パスワード@ホスト/データベース

# koi.cr https://crystal-lang.org/reference/1.9/database/index.html
require "db"
require "mysql"
 
module Koi
  extend self
  VERSION = "0.1.0"
  SELECT = "SELECT * FROM products"
  # MySQL Test
  def mysqltest()
    # 接続文字列を使って DB を開く。
    db = DB.open "mysql://user:password@localhost/test"
    begin
      # SELECT クエリーを行う
      db.query SELECT do |rs|
        # カラム名を表示する。
        puts "#{rs.column_name(0)} #{rs.column_name(1)} #{rs.column_name(2)} #{rs.column_name(3)}"
        # クエリー結果を表示する。
        rs.each do
          id = rs.read(Int32)
          product = rs.read(String)
          amount = rs.read(Int32)
          price = rs.read(Float64)
          puts "#{id}, #{product}, #{amount}, #{price}"
        end
      end
    ensure
      db.close  # 接続を閉じる。
    end
  end
end

モジュール関数 mysqltest を呼び出す。

Koi.mysqltest()

この例では、SELECT 文しか使っていないが、INSERT などを使う場合は、exec メソッドを使う。

(例) db.exec “INSERT INTO members VALUES(?, ?, ?, ?)”, 1, “Abe”, 15, “080-2586-8756”

このサンプルプログラムの実行例を下に示す。

$ ./koi
id product amount price
1, orange juice, 10, 120.0
2, apple juice, 10, 120.0
3, grape juice, 10, 120.0
4, soda, 15, 100.0
5, cola, 20, 110.0
6, Dr.Pepper, 0, 0.0
$

Crystal: 独自ライブラリを作って GitHub でホストする

独自ライブラリを作って github でホストすれば、どこでもそれを利用できる。

次のサンプルは HTML を生成するライブラリで名前は Kingyo である。

まず、crystal コマンドを使ってプロジェクトを作成する。

$ crystal init lib kingyo

README.md を更新する。例えば、次のように

## kingyo

Kingyo is the library to generate HTML.

### Installation

1. Add the dependency to your `shard.yml`:

   ```yaml
   dependencies:
     kingyo:
       github: your-github-user/kingyo
  1. Run shards install

Usage

require "kingyo"

TODO: Write usage instructions here

### Development

TODO: Write development instructions here

### Contributing

1. Fork it (<https://github.com/your-github-user/kingyo/fork>)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request

### Contributors

- [your-name-here](https://github.com/your-github-user) - creator and maintainer

次のコードは src/kingyo.cr の内容である。

# TODO: Write documentation for `Kingyo`
module Kingyo
  extend self
  VERSION = "0.1.1"
 
  # html
  def html(head, body, lang="en")
    result = "<!DOCTYPE html>\n"
    result += "<html lang=\"#{lang}\">\n"
    result += head
    result += body
    result += "</html>\n"
  end
 
  # body
  def body(content, css_class="")
    result = "<body>\n"
    if css_class != ""
      result = "<body class=\"#{css_class}\">\n"
    end
    result += "  #{content}\n</body>\n"
  end
   
  # head
  def head(title, charset="utf-8", content="")
    result = "<head>\n"
    result += "  <meta charset~\"#{charset}}\" />\n" 
    result += "  <title>#{title}</title>\n"
    if content != ""
      result += "  #{content}\n"
    end
    result += "</head>\n"
  end
end

このプロジェクトは依存するパッケージはないし、ビルドもしないので shard.yml は特に変更する必要はない。(バージョンと作者くらいは変更しておいたほうが良い)

準備ができたら GitHub にリポジトリ kingyo を作ってコミットした内容をアップロードする。

これでこの kingyo パッケージがいつでも利用できる。試しに test_kingyo という app プロジェクトを作ってこの kingyo パッケージが使えるか確認する。

$ crystal init app test_kingyo

ソース src/test_kingyo.cr を次のように更新する。

# TODO: Write documentation for `TestKingyo`
require "kingyo"
 
VERSION = "1.0.0"
 
def main()
    puts "TestKingyo.VERSION = #{VERSION}"
    head = Kingyo.head(title="test_kingyo")
    body = Kingyo.body("<h1>test_kingyo</h1>")
    html = Kingyo.html(head, body)
    puts html
end
 
main()

shard.yml に依存情報を追加する。

name: test_kingyo
version: 0.1.1
 
authors:
  - makandat
 
targets:
  test_kingyo:
    main: src/test_kingyo.cr
 
dependencies:
  kingyo:
    github: makandat/kingyo
 
crystal: 1.9.2
 
license: MIT

プロジェクトフォルダで shards コマンドにより kingyo をビルドする。

$ shards build

GitHub から kingyo パッケージがインストールされて実行可能ファイルが bin/test_kingyo として作られるはずである。

そして bin/test_kingyo を実行すると次のように表示される。

$ bin/test_kingyo 
TestKingyo.VERSION = 1.0.0
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset~"utf-8}" />
  <title>test_kingyo</title>
</head>
<body>
  <h1>test_kingyo</h1>
</body>
</html>
$

Crystal: ファイルやディレクトリの操作

ファイル操作は File クラスまたは FileUtils モジュールのメソッドを使って行うことができる。一方、ディレクトリの操作は Dir クラスまたは FileUtils モジュールのメソッドを使って行うことができる。

FileUtils モジュールのメソッドは File または Dir クラスのメソッドの別名であり、Unix (Linux) のファイル操作コマンドと同じような使い方で操作できる。

File と Dir のメソッドを使う場合、require は不要だが、FileUtils のメソッドを使うときは require “FileUtils” が必要である。

次に、ファイルやディレクトリ操作の簡単な例を示す。

# ファイル操作
require "file_utils"
 
FileUtils.cd("/home/user/bin")  # ディレクトリの移動
p! FileUtils.pwd  # 現在のディレクトリを表示
 
TEMPDIR1 = "/home/user/temp/dir1"
TEMPDIR2 = "/home/user/temp/dir2"
FileUtils.mkdir(TEMPDIR1)  # ディレクトリの作成
p! Dir.exists?(TEMPDIR1)  # ディレクトリが作成できたか確認
FileUtils.mv(TEMPDIR1, TEMPDIR2)  # ディレクトリを移動
p! Dir.exists?(TEMPDIR2)  # ディレクトリが移動できたか確認
FileUtils.rmdir(TEMPDIR2)  # ディレクトリを削除
p! Dir.exists?(TEMPDIR2)  # ディレクトリが削除できたか確認
FileUtils.cp("/home/user/.profile", "/home/user/temp/profile")  # ファイルのコピー
p! File.exists?("/home/user/temp/profile")  # ファイルのコピーができたか確認
File.delete("/home/user/temp/profile")  # ファイルを削除
p! File.exists?("/home/user/temp/profile")  # ファイルの削除ができたか確認

この実行例を下に示す。

$ ./bin/file_opr
FileUtils.pwd # => "/home/user/bin"
Dir.exists?(TEMPDIR1) # => true
Dir.exists?(TEMPDIR2) # => true
Dir.exists?(TEMPDIR2) # => false
File.exists?("/home/user/temp/profile") # => true
File.exists?("/home/user/temp/profile") # => false

Crystal: ファイル入出力

ファイルの入出力は File クラスのメソッドによって行うことができる。

もっとも簡単なのは、文字列全体のファイルへの書き込みと読み出しで write(), read() メソッドだけで行うことができる。下にその例を示す。

# file 入出力
#  コマンドパラメータでファイルパスを得る。
if ARGV.size() == 0
  STDERR.puts("No arguments")
  exit 1
end
 
filepath = ARGV[0]
 
hello = "Hello
  World!
"
begin
  # ファイルに文字列を書く。
  File.write(filepath, hello)
  puts "Write OK"
  # ファイル内容をすべて読み込む。
  s = File.read(filepath)
  puts s
rescue e
  puts e.message
end
 
puts ".. Done."

また、行単位での読み込みを行う際には、read_line() メソッドや read_lines() メソッドを使う。下にその例を示す。

# ファイル入出力 行ごと
#  コマンドパラメータでファイルパスを得る。
if ARGV.size() == 0
  STDERR.puts("No arguments")
  exit 1
end
 
filepath = ARGV[0]
 
# 行ごとにファイルを読む。
begin
  File.each_line(filepath) do |line|
    puts line
  end
  puts ".. each_line OK"
rescue e
  puts e.message
end
 
# 行ごとにファイルを読んで配列に格納する。
begin
  strarray = File.read_lines(filepath)
  strarray.each do |line|
    puts line
  end
  puts ".. read_lines OK"
rescue e
  puts e.message
end

Crystal: ディレクトリの内容を取得する

ディレクトリの内容を読み取るには Dir クラスの glob() や entries() メソッドを使う。glob はクラスメソッドであり、entries はインスタンスメソッドである。

まず、 glob の使用例を示す。この例は単純にすべてのディレクトリの内容を読み取るものであるが、glob には読み取りに関し詳細な設定を行えるオーバーロードメソッドもある。

files = Dir.glob("/home/user/*")
files.each do |f|
  puts f
end

entries メソッドはインスタンスメソッドなので Dir.new(path) によりインスタンスを得る必要がある。

下の例はもっとも簡単な使い方であるが、entries は glob のようにオプションを様々に設定できる機能はない。

# ディレクトリの内容一覧
if ARGV.size() == 0
  STDERR.puts("No arguments")
  exit 1
end
 
path = ARGV[0]
 
if !Dir.exists?(path)
  STDERR.puts("#{path} does not exists.")
  exit 1
end
 
list = Dir.new(path).entries()
list.each do |x|
  puts x
end

Crystal: ファイルパスの操作

ファイルパスの操作には、File クラスのメソッドと Path 構造体のメソッドを使う方法がある。

下にその簡単な使用例を示す。

# ファイルパス
filepath = "/home/user/temp/hello.txt"
puts File.basename(filepath)  # ファイル名のみ
puts File.dirname(filepath)  # ディレクトリ部分
puts File.extname(filepath)  # 拡張子 (ドットを含む)
p! File.join(["~/bin", "build.sh"])  # ファイルのパスを構成する。
p! File.expand_path("path.cr")  # 絶対パスを返す。
 
# Path 構造体を使う例
p! Path[filepath].basename  # ファイル名のみ
p! Path[filepath].dirname  # ディレクトリ部分
p! Path[filepath].extension  # 拡張子 (ドットを含む)
p! Path[filepath].parent  # 親のディレクトリ名 (Path のインスタンス)
# インスタンス化しても使用できる。
path = Path.new(filepath)
# パスの部分に分ける。
path.each_part do |x|
  puts x
end

このサンプルの使用例を下に示す。

$ ./bin/path
hello.txt
/home/user/temp
.txt
File.join(["~/bin", "build.sh"]) # => "~/bin/build.sh"
File.expand_path("path.cr") # => "/home/user/workspace/Crystal/path.cr"
Path[filepath].basename # => "hello.txt"
Path[filepath].dirname # => "/home/user/temp"
Path[filepath].extension # => ".txt"
Path[filepath].parent # => Path["/home/user/temp"]
/
home
user
temp
hello.txt

Crystal: 日付時刻

Crystal では日付時刻の処理は Time 構造体のメソッドによって行うことができる。

次に Time の簡単な使用例を示す。

# 日付時刻 https://crystal-lang.org/api/1.9.2/Time.html
 
# 現在の時刻
p! Time.utc
p! Time.local
 
# 時刻値を作る。
t1 = Time.local(2020, 2, 22, 20, 20, 20)
p! t1
 
# 時刻の部分を得る。
p! t1.year
p! t1.second
 
# 文字列との変換
now = Time.local
s = now.to_s("%Y-%m-%d %H:%M:%S")  # 文字列にする。
puts s
JST = Time::Location.load("Asia/Tokyo")
t2 = Time.parse(s, "%Y-%m-%d %H:%M:%S", JST)  # 文字列から時刻にする。
p! t2
 
# 時間間隔の計算
span = Time.utc(2020, 2, 28) - Time.utc(2020, 1, 1)
p! span.days  # 元旦から2月28日までの日数
p! Time.utc(2020, 1, 1) + Time::Span.new(days:5)  # 5日後
 
# 時間計測
t3 = Time.monotonic # 現在の CPU 時間
sleep 1  # 1秒
t4 = Time.monotonic
puts (t4 - t3).to_s  # 約 1 秒

このコードの実行例を下に示す。

$ ./bin/time
Time.utc # => 2023-09-19 23:31:31.564272781 UTC
Time.local # => 2023-09-19 23:31:31.565349765 +00:00
t1 # => 2020-02-22 20:20:20.0 +00:00
t1.year # => 2020
t1.second # => 20
2023-09-19 23:31:31
t2 # => 2023-09-19 23:31:31.0 +09:00 Asia/Tokyo
span.days # => 58
(Time.utc(2020, 1, 1)) + Time::Span.new(days: 5) # => 2020-01-06 00:00:00.0 UTC
00:00:01.019929696
$

Crystal: 正規表現

Crystal では Regex クラスのメソッドを使って正規表現処理を行うことができる。

このクラスでは、=~ や $~ のような Perl 風の演算子や特殊変数も使用できる。

下に Regex クラスの簡単な使用例を示す。

# 正規表現 Regex https://crystal-lang.org/api/1.9.2/Regex.html
 
str = "無職転生S2"
p /転生/ =~ str  # 見つかった位置を表示 (バイト位置でなく文字列の位置)
if match_data = /S\d/.match(str)
  p match_data.string  # 検索した文字列
  p match_data[0]  # 見つかった文字列
  p! $~  # Perl 風の特殊変数も使える。
end
 
/\d/.match_at_byte_index("F15J F35B", 3)  # 数字を 3 バイト目から検索する。
p! $~[0]  # 見つかったバイト位置

このコードの実行例を下に示す。

$ ./bin/regex
2
"無職転生S2"
"S2"
$~ # => Regex::MatchData("S2")
$~[0] # => "3"
$

Crystal: JSON

Crystal では JSON モジュールのメソッドを使うと、JSON を簡単に扱うことができる。

ただし、このモジュールを使うときは require “json” が必要である。

下に JSON モジュールの簡単な使用例を示す。

# JSON https://crystal-lang.org/api/1.9.2/JSON.html
require "json"
 
json_text = %([1, 2, 3])  # %文字列でデータを定義
a = Array(Int32).from_json(json_text) # JSON を配列に変換
 
json_text = %({"x": 1, "y": 2})  # %文字列でデータを定義
h = Hash(String, Int32).from_json(json_text) # JSON を連想配列に変換
 
# 逆変換
puts a.to_json
puts h.to_json
 
# JSON を作成する。
string = JSON.build do |json|
  json.object do
    json.field "name", "foo"
    json.field "values" do
      json.array do
        json.number 1
        json.number 2
        json.number 3
      end
    end
  end
end
 
puts string

このコードの実行例を下に示す。

$ ./bin/json
[1,2,3]
{"x":1,"y":2}
{"name":"foo","values":[1,2,3]}
$

Crystal: 環境変数

Crystal では ENV モジュールの ENV という連想配列 (ハッシュテーブル、辞書) を使うと簡単に環境変数にアクセスできる。

下に ENV の簡単な使用例を示す。

# 環境変数 ENV https://crystal-lang.org/api/1.9.2/ENV.html
 
#  環境変数一覧を表示する。
ENV.each do |kv|
  puts "#{kv[0]}:#{kv[1]}"
end
 
# 環境変数 PYTHONPATH の値を表示する。
puts ENV["PYTHONPATH"]

このコードの使用例を下に示す。

$ ./bin/env
SHELL:/bin/bash
PWD:/home/user/workspace/Crystal
LOGNAME:user
XDG_SESSION_TYPE:tty
HOME:/home/user
.........
.........
.:/home/user/lib/Python

Crystal: Fiber とは何か?

Crystal の Fiber とは、簡単に言えば軽量なスレッドのようなものです。

ただし、使用できるメモリが 8MB だけ (v1.9.2 の場合) しかないので重い処理には向きません。

Fiber との通信には Channel という軽量なメカニズムを使います。Channel により複雑な同期処理などが簡単にできてスレッドより使いやすいです。

次に Fiber の簡単な使用例を示します。この例では、2つの Fiber を起動し、2つが終了するまで待ってからメインプロセスを終了させます。

Fiber が終わったかどうかは Channel から送られてくる信号で確認しています。

もし、この処理がないと、メインプロセスが Fiber を起動した後、すぐに終了してしまい何も表示されません。

# Fiber: 軽量なスレッドのようなもの https://crystal-lang.org/reference/1.9/guides/concurrency.html#fibers
 
channel1 = Channel(Int32).new
channel2 = Channel(Int32).new
 
#  Fiber1 を開始する。
spawn do
  sleep 1.second
  puts "fiber1"
  channel1.send(1)
end
 
#  Fiber2 を開始する。
spawn do
  sleep 2.second
  puts "fiber2"
  channel2.send(2)
end
 
# Channel の値を受け取るまでメインの実行はブロッキングされる。
puts "The fibers are blocking .."
p channel1.receive
p channel2.receive
puts " .. Done."

このコードの実行例を下に示します。

$ ./bin/fiber
The fibers are blocking ..
fiber1
1
fiber2
2
 .. Done.
$

Mutex を使いリソースの競合を回避

Crystal にも Mutex があって、共有リソースを複数のファイバーどうしが同時に使用できないようにすることができる。

次の例では1つのファイバーとメインが標準出力へ文字列を同時に出力できないようにしている。

# Mutex https://crystal-lang.org/api/1.9.2/Mutex.html
mutex = Mutex.new
puts "Start .."
# 直ちに fiber が開始される。
spawn do
  mutex.lock  # stdout をロック
  sleep 2.second
  puts "filber 1 done."
  mutex.unlock
end
 
sleep 1.second  # fiber が動作する前に終わらないようにする。
mutex.lock
puts "main done."
mutex.unlock

この実行例を示す。

$ ./bin/mutex
Start ..
filber 1 done.
main done.

Crystal: タプル

タプルは配列に似ているが、固定長、書き換え不可、スタック上に割り当てられるという特徴がある。

また、メンバーは同じで型でなくてもよい。

タプルには普通のタプル (Tuple) と名前付きタプル (NamedTuple) がある。

タプルのメンバーは配列のようにインデックスで指定するが、名前付きタプルはキーで指定する。キーは文字列またはシンボルのどちらかである。

次にタプルの簡単な使用例を示す。

# Tuple
#   https://crystal-lang.org/api/1.9.2/Tuple.html
#   https://crystal-lang.org/api/1.9.2/NamedTuple.html,
 
# タプルを返す関数
def one_and_hello
  {1, "hello"}
end
 
# タプル
tuple = {1, "hello", 'x'} # Tuple(Int32, String, Char)
p! tuple[2]  # 要素の取得
# イテレータを使う
tuple.each do |x|
  p! x
end
# タプルを返す関数を使う。
one, hello = one_and_hello
 
# 名前付きタプル
language = {name: "Crystal", year: 2011} # NamedTuple(name: String, year: Int32)
 
p! language[:name]  # => "Crystal"
p! language[:year]  # => 2011
p! language["year"] # キーには文字列も使える。
# language[:other] # 存在しないキーを使うとコンパイル時にエラーとなる。
p! language["other"]?  # ? を付けると存在しないキーを使うと nil を返す。
p! language.fetch(:other, 0)  # fetch を使うと存在しないキーを指定してもエラーにならずデフォルト値を返す。
p! language.has_key?(:other) # キーが存在するかチェックする。
p! language.keys  # キー一覧を得る。
p! language.sorted_keys  # ソートされたキー一覧を得る。
p! language.size  # メンバーの数を得る。

下にこの実行例を示す。

$ ./bin/tuple
tuple[2] # => 'x'
x # => 1
x # => "hello"
x # => 'x'
language[:name] # => "Crystal"
language[:year] # => 2011
language["year"] # => 2011
language["other"]? # => nil
language.fetch(:other, 0) # => 0
language.has_key?(:other) # => false
language.keys # => {:name, :year}
language.sorted_keys # => {:name, :year}
language.size # => 2

Crystal: 列挙型 (Enum)

列挙型 (Enum) は case 文などで番号 (数) の代わりに使うもので、コードを読みやすくするためによく用いられる。

次に列挙型の使用例を示す。

# 列挙型 https://crystal-lang.org/api/1.9.2/Enum.html
 
# 列挙型 Color の宣言
enum Color
  Red   # 0
  Green # 1
  Blue  # 2
end
 
# 列挙型の値は整数
p! Color::Green.value
 
a = Color::Red  # 代入
 
# 判別
case a
when Color::Red
  puts "Red"
when Color::Green
  puts "Green"
when Color::Blue
  puts "Blue"
else
  puts "Not defined."
end

この実行例を下に示す。

$ ./bin/enum
Color::Green.value # => 1
Red
$

Enum は単なる型ではなくて、abstruct struct であり、実体化したものが enum である。

Enum は struct なので型としてだけでなく、メソッドなどを持つ。

Crystal: 連想配列 (Hash)

連想配列は場合によって「辞書」「ハッシュテーブル」「マップ」などとも呼ばれる。

Crystal では Hash クラスとして実装されている。

使い方は他の言語と基本的に同じであるが、インスタンス化するときにキーと値の型を指定する必要がある。

ハッシュのリテラルは Ruby と同じように key => value を使う。

(例){"A" => 10, "B" => 20}

次に連想配列の使用例を示す。

# Hash https://crystal-lang.org/api/1.9.2/Hash.html
 
# 初期化
lepin = Hash(String, Int32).new
lepin["Lepin"] = 32
lepin["Jigen"] = 40
lepin["Goemon"] = 28
lepin["Zenigata"] = 45
lepin["Fujiko"] = 27
 
# キー一覧
p! lepin.keys
 
# メンバー一覧を表示
lepin.each do |t|  # t はタプル
  p t[0], t[1]
end
 
# キーの有無をチェックする
p! lepin.has_key?("Goemon")
p! lepin.has_key?("Ishikawa")
 
# 値の変更とその値の確認
lepin["Lepin"] = 31
p! lepin["Lepin"]

この実行例を下に示す。

$ ./bin/hash
lepin.keys # => ["Lepin", "Jigen", "Goemon", "Zenigata", "Fujiko"]
"Lepin"
32
"Jigen"
40
"Goemon"
28
"Zenigata"
45
"Fujiko"
27
lepin.has_key?("Goemon") # => true
lepin.has_key?("Ishikawa") # => false
lepin["Lepin"] # => 31
$

Crystal: 集合 (Set)

集合は Set 構造体によって実装されている。

集合は配列と違って、同じ要素は1つだけ格納できる。例えば整数の集合の場合、100 というメンバーがあった場合、後から 100 を追加しても無視される。

次に集合の簡単な使用例を示す。

# Set https://crystal-lang.org/api/1.9.2/Set.html
# 初期化
s1 = Set{1, 2, 3}
s2 = [1, 2, 3].to_set
s3 = Set.new [-1, -2, -3]
# == 演算子
p! s1 == s2
p! s2 == s3
# 要素の追加
s1.add(4)
p! s1
s1 << 5  # add(x) と同じ
p! s1
s3.concat [-4, -5]  # 結合
p! s3
# 要素が含まれているか?
p! s2.includes?(3)
p! s2.includes?(4)
# 空集合か?
p! s2.empty?
# 部分集合か?
p! s2.subset_of?(s1)
p! s1.subset_of?(s2)
# 要素の削除
p! s3.delete(-5)
p s3
# 変換
p! s1.to_s  # 文字列化
p! s1.to_a  # 配列化

この実行例を以下に示す。

$ ./bin/set
s1 == s2 # => true
s2 == s3 # => false
s1 # => Set{1, 2, 3, 4}
s1 # => Set{1, 2, 3, 4, 5}
s3 # => Set{-1, -2, -3, -4, -5}
s2.includes?(3) # => true
s2.includes?(4) # => false
s2.empty? # => false
s2.subset_of?(s1) # => true
s1.subset_of?(s2) # => false
s3.delete(-5) # => true
Set{-1, -2, -3, -4}
s1.to_s # => "Set{1, 2, 3, 4, 5}"
s1.to_a # => [1, 2, 3, 4, 5]

Crystal: クラス (Class) とモジュール (Module)

クラスは抽象クラスの Class クラスのインスタンスとして定義される。

Class は Object から派生した Value を継承している。よって、これらのクラスのメソッドも使用できる。

クラスの継承

次にクラスの使用例を示す。クラスの使い方は Ruby とだいたい同じだがサポートされていない機能もある。

# class https://crystal-lang.org/api/1.9.2/Class.html
module Koi
  # コイ科クラス
  class Koika
    def initialize()
      @name = ""
      @size = 0
      @bunpu = Array.new
    end
     
    def size
      @size.to_s + "cm"
    end
    # 下のような書き方はエラーになる。
    #attr_reader: name
     
    def name
      @name
    end
     
    def dist
      @bunpu
    end
  end
   
  # ニゴイクラス
  class Nigoi < Koika
    def initialize()
      @name = "ニゴイ"
      @size = 60
      @bunpu = ["本州", "四国", "九州"]
    end
     
    # クラスメソッドの定義
    def self.kanji()
      "似鯉"
    end
  end
 
  # マゴイクラス
  class Magoi < Koika
    def initialize()
      @name = "マゴイ"
      @size = 100
      @bunpu = ["北海道", "本州", "四国", "九州"]
    end   
 
    # クラスメソッドの定義
    def self.kanji()
      "真鯉"
    end
  end
 
end

これらのクラスの使用例を示す。

# test_class
require "./class.cr"
 
# モジュール Koi に含まれる Magoi クラスをインスタンス化
magoi = Koi::Magoi.new
# モジュール Koi に含まれる Nigoi クラスをインスタンス化
nigoi = Koi::Nigoi.new
# Nigoi クラスの基底クラス Koika のメソッドを使用する。
p nigoi.name
p nigoi.size
# Magoi クラスの基底クラス Koika のメソッドを使用する。
p magoi.dist
# Magoi クラスのクラスメソッドを使用する。
p Koi::Magoi.kanji
# すべてのクラスの基底クラス Object のメソッドを使用する。
p magoi.hash

この実行例を下に示す。

$ ./bin/test_class
"ニゴイ"
"60cm"
["北海道", "本州", "四国", "九州"]
"真鯉"
12321634533550542469

プロパティ

Ruby ではプロパティを定義するのに attr_accessor などを使うが、Crystal では property などを使う。

  • property
  • getter
  • setter

クラスメソッド

クラスメソッドの定義には self. を使用する。

def self.do_this
  ...
end

Ruby ではクラスメソッドのコール時に :: を使うが、Crystal では . を使う。

(Ruby) Class1::do_this => (Crystal) Class1.do_this

モジュールの include

クラスは他のクラスの継承だけでなく、モジュールを include して機能を拡張できる。次の例は Crystal のドキュメントに出ているサンプルである。

この例ではモジュールに含まれるメソッドを単純にクラスに include している。

module ItemsSize
  def size
    items.size
  end
end
 
class Items
  include ItemsSize
 
  def items
    [1, 2, 3]
  end
end
 
items = Items.new
items.size # => 3

モジュールの extend

クラスにモジュールを include するのに似ている extend というのもある。include が単純にモジュールの内容をクラスにインクルードするのに対し、extend はクラスメンバーとして機能拡張するというイメージである。

次の例は Crystal ドキュメントに出ているサンプルである。extend により size というメソッドは Items のクラスメソッドになる。

module SomeSize
  def size
    3
  end
end
 
class Items
  extend SomeSize
end
 
Items.size # => 3

モジュール自体のメソッドなどを公開する場合も extend が必要になる。その場合は、extend self とする。下にその例を示す。

module MyModule
    extend self
    def init
       ...
    end
    ....
end

Generics

Crystal は静的型付け言語なので C# のような Generics の機能がある。

これは、クラスで使われる変数の型を指定できるものである。

次の例で、KVPair というクラスは key と value というプロパティを持つ。このクラスは Generic クラスなのでこれらのプロパティの型を制限できる。

# Generics
class KVPair(K, V)
  def initialize(@key : K, @value : V)
  end
 
  def key
    @key
  end
   
  def value
    @value
  end
end
 
a = KVPair(String, String).new("All", "全部")
p a.key, a.value

このコードの実行例を示す。

$ ./bin/generics
"All"
"全部"
$

Generics については他にも様々な機能がある。詳細は Crystal のドキュメントを参照のこと。

Crystal: 構造体とレコード (Struct, record)

Cystal の構造体 (Struct) は C の構造体とは違いクラスに近い。

そして、クラス同様に継承などもでき、Struct 自体も Value を継承している。なお、Value は Object を継承している。

次に Struct の使用例を示す。

# Struct https://crystal-lang.org/api/1.9.2/Struct.html
 
# Point 構造体の定義
struct Point
  # コンストラクタでメンバー変数を初期化する必要がある。
  def initialize(@x : Int32, @y : Int32)
  end
   
  # アクセサメソッド x
  def x
    @x
  end
   
  # アクセサメソッド y
  def y
    @y
  end
end
 
# インスタンス化
p1 = Point.new 1, 2
p2 = Point.new 1, 2
p3 = Point.new 3, 4
 
#  アクセサメソッドを定義しないとこういう書き方はできない。
p p1.x
p p1.y
 
# 演算子メソッド ==
p p1 == p2 # => true
p p1 == p3 # => false
 
# Object を継承しているので Object クラスのメソッド hash が使える。
p p3.hash

この実行例を下に示す。

$ ./bin/struct
1
2
true
false
12286184678607581374

record マクロは Struct を使いやすくしたもので、Struct をベースにしている。

# record マクロは Struct をベースにしている。
record Point, x : Int32, y : Int32
 
p = Point.new 1, 2 # => #<Point(@x=1, @y=2)>
p! p.x                # => 1
p! p.y                # => 2

この実行例を示す。

p.x # => 1
p.y # => 2

Crystal: ログを取るには

古いバージョンの Crystal には Logger というパッケージがあって、ググるとそのサンプルがよく出てくる。

しかし、2023 年時点ではバージョンが 1.9.2 になっており、その標準ライブラリには Log と言うパッケージに変更されており、以前の Logger とは互換性がないようである。

これについては、ググってもあまり記事がヒットしない。

Crystal のドキュメントには次のようなコードが最初の方に出てくる。
ここで、Log.info のパラメータがブロックになっていることに注意。
つまり、ブロックで出力内容に何らかの加工ができるということを意味する。

require "log"
 
Log.info { "Program started" }

これを実行すると、コンソール画面にこの文字列が表示される。つまり、デフォルトではログの出力先が標準出力になっていることを意味する。

とりあえず、デバッグのためにはファイルへログを出力したいので、標準出力から指定したファイルにログの出力先を変えたい。

これをするには、Backend で出力先を作り、それをログにバインドするとよいらしい。

具体的には次のようなちょっと面倒なコードが必要になった。

ここでログ出力メソッド (例えば Log.error) のパラメータは文字列でなくブロックであることに注意。このブロック内で処理が可能で、最終的に文字列を作るというシナリオのようである。

::Log.setup のブロックでは、この例では1つのみだが、ソース名や出力先を変えたりして複数のログを定義できる。

# Log https://crystal-lang.org/api/1.9.2/Log.html
require "log"
 
# ログの出力先を "log.txt" というファイルにする。
backend = ::Log::IOBackend.new(File.new("./log.txt", "w"))
# ログの出力先 (backend) をバインドする。
::Log.setup do |c|
  c.bind("*", :debug, backend)  # 任意のログソース "*" に対してバインドを行う。
end
 
::Log.for("*")  # 任意のログソース "*" に対するコンストラクタ
# デフォルトのログソース Log に対してログ操作を行う。
Log.debug {"Log start .." }
Log.info {"Log info"}
Log.error{"Log error"}
Log.warn {"Log warn"}
Log.fatal {"Log fatal"}
Log.debug {" .. Done."}
 
# ./bin/log;cat ./log.txt;rm ./log.txt

このコードの実行例を示す。

$ ./bin/log;cat ./log.txt;rm ./log.txt
2023-09-21T04:37:43.969739Z  DEBUG - Log start ..
2023-09-21T04:37:43.969742Z   INFO - Log info
2023-09-21T04:37:43.969742Z  ERROR - Log error
2023-09-21T04:37:43.969743Z   WARN - Log warn
2023-09-21T04:37:43.969743Z  FATAL - Log fatal
2023-09-21T04:37:43.969744Z  DEBUG -  .. Done
$

Crystal: コマンドを実行するには

コマンドを実行するには Process クラスを使う。

このモジュールには、多くのメソッドがあるが単純にコマンドを実行するだけなら run メソッドが便利である。

def self.run(command : String, args = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false, input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String | Nil = nil) : Process::Status

このメソッドにはブロック付きのバージョンもあり次のようになっている。

def self.run(command : String, args = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false, input : Stdio = Redirect::Pipe, output : Stdio = Redirect::Pipe, error : Stdio = Redirect::Pipe, chdir : Path | String | Nil = nil, &)

このメソッドのうち、command にコマンド文字列を指定する。args はパラメータのタプルで空白で区切られた文字列単位で指定する。

あと、注意しないこととして output パラメータを STDOUT に設定しないと、コマンドの出力は標準出力に表示されない。また、デフォルトで shell パラメータが false になっているので、シェル経由でコマンドを実行する場合は、shell:true にする。

次に簡単な例を示す。

# クラスメソッド run でコマンドを実行する。
sts = Process.run("python3", {"-V"}, output:STDOUT)  # python3 のバージョンを表示。python3 コマンドはシェルスクリプトでないので shell:true にしなくてよい。
p! sts.exit_status
 
# Ubuntu のバージョンを表示するシェルスクリプトを実行する。
Process.run("~/bin/ver", shell:true, output:STDOUT)  # シェル機能 (ここでは ~) を使う場合は shell:true にしないとエラーになる。
 
# ブロック付きバージョンの run
Process.run("perl", {"-v"}) { |pr|
  puts "pid = #{pr.pid} OK"
}

これを実行すると、次のようになる。

Python 3.11.4
sts.exit_status # => 0
Distributor ID: Ubuntu
Description:    Ubuntu 23.04
Release:        23.04
Codename:       lunar
pid = 18601 OK

Process クラスをインスタンス化してコマンドを実行することもできる。この場合、インスタンス化が成功すると、ただちにコマンドの実行が開始される。

コマンドが終了するまで待つには、wait メソッドを使う。これがないと次の例ではメインプログラムがコマンドの終了を待たずに終了してしまう。

# プロセスオブジェクトを作成する。(そして直ちに実行される)
p1 = Process.new("ls", {"-l", "/"}, shell:true, output:STDOUT)  # ルートディレクトリ / の内容一覧を表示
p1.wait  # プロセスが終わるまで待つ。

コマンドが単純に実行されすぐ終わるものでなく、プロセス間通信などを行う場合は、こちらの方法がよい。プロセスオブジェクトにはいろいろなメソッドがあるので様々な操作ができる。

次にこの実行例を示す。

$ ./bin/process
合計 2097232
lrwxrwxrwx 1 root root 7 5月 13 2022 bin -> usr/bin
drwxr-xr-x 4 root root 4096 9月 21 13:57 boot
drwxr-xr-x 2 root root 4096 5月 13 2022 cdrom
drwxr-xr-x 19 root root 4840 9月 21 12:43 dev
drwxr-xr-x 141 root root 12288 9月 21 13:56 etc
drwxr-xr-x 4 root root 4096 7月 24 11:46 home

Crystal で C の関数を使うには

Crystal で C の関数を使うのは簡単である。

LibC という Crystal のモジュールは Crystal::System 名前空間に統合されているので、glib の関数をそのまま使える。

glibc の関数がそのまま使えれば、それらの関数を直接呼び出す Crystal 関数を作れば、C で関数を作るのと同じことである。

ただ、残念なことにすべての glibc 関数が使えるわけではない (サンプルのため) ようである。しかし、テンプレートに「自分でfunctionを記述することもできる」らしい。

Qiita: CrystalでC言語のバインディングを自動生成する crystal_lib

他の言語ではポインタを扱えないものもあるが、Crystal ではそれも可能である。

次の例は、C の関数 strlen() で文字列の長さを取得するものである。LibC は System に統合されているので require は不要である。

n = LibC.strlen("Hello, World!")
p n

その他の C ライブラリ関数を使う場合は、GitHub でホストされており shards コマンドでインストールできるものもあるが、一般的には独自に LibC 相当のものを作る必要がある。

詳しい解説は Specification: C bidings に出ている。(下記)

C bindings
    lib
    fun
    struct
    union
    enum
    Variables
    Constants
    type
    alias
    Callbacks
Unsafe code

LibC は glibc の一部の関数しか使えないので、その他の関数を使いたい場合は自分でライブラリを定義すればよい。

上の C bindings に方法が出ている。具体的なサンプルを下に示す。これは math ライブラリ (-lm) の関数を使う例である。

# libm
@[Link("m")]
lib LibM
  fun cos(value: Float64) : Float64
  fun sin(value: Float64) : Float64
  fun tan(value: Float64) : Float64
end
 
fun radian(deg: Float64): Float64
  deg / 180.0 * 3.1415926
end
 
p LibM.cos(radian(0.0))
p LibM.sin(radian(90.0))
p LibM.tan(radian(45.0))

これをビルドして実行すると下のようになる。

$ ./bin/libm
1.0
0.9999999999999997
0.9999999732051038
$

Void* 型のパラメータ

C の関数には void* 型のパラメータを渡す必要があるものがある。このようなパラメータは Box(T) クラス を使用することで実現できる。

Crystal: require について

require はファイルを読み込んで、プロジェクトに統合する。

もし、ファイル名の拡張子が .cr の場合は、拡張子は書かなくてよい。(通常は書かない)

ファイルの場所は、まず Crystal の標準ライブラリ、プロジェクト内の lib フォルダ、CRYSTAL_PATH 環境変数の内容である。カレントディレクトリにあるモジュールを読み込む場合は、”./” を先頭に付ける必要がある。

lib フォルダは通常、shards コマンドで外部のモジュールをインストール (shards install) すると自動的に作られる。

その際、外部モジュールの場所を、shards init で作られる shard.yml の “dependencies” セクションに記述しておく必要がある。

対象のファイルが、サブフォルダにある場合は “/” を使ってファイルシステムのツリー記法のように記述する。

複数のファイルをまとめて指定することもできる。その場合は、ディレクトリ名を指定する。その時のツリー構造は決まりがあり、一例として次のように書く。

  • project
    • src
      • file
        • sub1.cr
        • sub2.cr
      • file.cr (requires "./file/*")

詳細は Requiring files を参照のこと。

Crystal: HTTP の簡単な例

HTTP モジュールを使うと

  • HTTP クライアント
  • HTTP サーバ
  • Web ソケット

を簡単に作ることができる。

このモジュールには次のようなサブモジュールがある。

Client
    BodyType
    Response
    TLSContext
CompressHandler
Cookie
    SameSite
Cookies
ErrorHandler
FormData
    Builder
    Error
    FileMetadata
    Parser
    Part
Handler
    HandlerProc
Headers
LogHandler
Params
Request
Server
    ClientError
    Context
    RequestProcessor
    Response
StaticFileHandler
    DirectoryListing
Status
WebSocket
    CloseCode
WebSocketHandler

次に簡単な HTTP サーバの例を示す。この例ではすべてのリクエストに対して “Hello world! The time is 現在の時刻” という文字列を返す。

# HTTP Server https://crystal-lang.org/api/1.9.2/HTTP/Server.html
require "http/server"
 
# HTTP サーバを作成しブロックを実行する。
server = HTTP::Server.new do |context|
  # 応答を返す。
  context.response.content_type = "text/plain"
  context.response.print "Hello world! The time is #{Time.local}\n"
  # リクエスト元を表示する。
  if context.request.hostname.nil?
    puts "> Nil"
  else
    puts "> #{context.request.hostname}"
  end
end
 
# TCP Server を作成し、IP アドレスとポートをバインドする。
address = server.bind_tcp("127.0.0.1", 8080)
puts "Listening on http://#{address}"
# リクエストを待つ
server.listen

次に 簡単な HTTP クライアントの例を示す。

# http/client
require "http/client"
 
# localhost:8080 にリクエストを送り、応答を得る。
response = HTTP::Client.get "http://localhost:8080"
# 応答のステータスコードを表示
puts response.status_code.to_s
# 応答本体の先頭行を表示
puts response.body.lines.first

これらを実行する例を示す。

サーバ側

$ ./bin/http-server
Listening on http://127.0.0.1:8080
> localhost
> localhost
> localhost

クライアント側

$ ./bin/http-client
200
Hello world! The time is 2023-09-22 07:43:16 +00:00
$ ./bin/http-client
200
Hello world! The time is 2023-09-22 07:43:19 +00:00
$ ./bin/http-client
200
Hello world! The time is 2023-09-22 07:43:20 +00:00
$

Crystal: 型と関数

パラメータと関数値の型指定

Crystal は Ruby と違い、静的型付け言語である。そのため、関数定義においても型の指定ができる。

次の例で関数 distance は2つの Float64 型のパラメータを取り、関数値として Float64 を返す。

(注意) 型指定の : Float64 でコロンの前後には空白が必要である。

# def1.cr
def distance(x : Float64, y : Float64) : Float64
  Math.sqrt(x * x + y * y)
end
 
p! distance(12.0, 4.0)

次の例のように型を指定せずに関数を定義することもできる。この場合は、型が自動変換されて計算される。

# def2.cr
def distance(x, y)
  Math.sqrt(x * x + y * y)
end
 
p! distance(2_u8, 4_u16)

次の関数は2つの文字列を連結する。仮にパラメータとして整数を与えると文字列でないのでコンパイル時にエラーとなる。

# def3.cr
def concat(s1 : String, s2 : String) : String
  s1 + s2
end
 
p concat("0", "1")
# p concat(0, 1) はコンパイルエラーになる。

ブロック

他の言語ではブロックと無名関数は別の意味であることが多いが、Crystal や Ruby ではまとめてブロックと呼ばれる。

そして、Crystal や Ruby では関数パラメータとしてブロックを渡すことができる。Ruby は動的型指定なのでブロックの型指定はできないが、Crystal の場合は型指定ができる。

次の例は32ビット整数のパラメータを受け取り、32ビット整数を返すブロックをパラメータを持つ関数の例である。

def int_to_int(&block : Int32 -> Int32)
  block
end
 
proc = int_to_int { |x| x + 1 }
n = proc.call(1) # => 2
puts n.to_s

次の例はブロックの戻り値の型を指定しない例である。この例だと戻り値が32ビット整数と文字列を使っている。

def some_proc(&block : Int32 -> _)
  block
end
 
proc = some_proc { |x| x + 1 }
y = proc.call(1) # 2
p y
 
proc = some_proc { |x| x.to_s }
y = proc.call(1) # "1"
p y

Proc

Proc は無名関数を表すオブジェクトである。無名関数は -> block の形式で作成する。下に、無名関数を変数に代入し、call メソッドで実行する例を示す。

# arrow.cr
itos = ->(x: Int32) { x.to_s }
p itos.call(100)

オーバーロード

関数名が同じでパラメータや戻り値の型が違う関数をオーバーロードという。

Crystal は型指定が可能なので、関数のオーバーロードが可能である。

次にオーバーロードの例を示す。

# This methods greets *recipient*.
def say_hello(recipient : String)
  puts "Hello #{recipient}!"
end
 
# This method greets *times* times.
def say_hello(times : Int32)
  puts "Hello " * times
end
 
say_hello "World"
say_hello 3

Crystal: ポインタ

Crystal ではポインタがサポートされているが、唯一の unsafe なオブジェクトとされている。

そのため、ポインタは積極的に使うものではない。

使う必要があるのは、C 関数のパラメータとしてぐらいだと思われる。

C のライブラリ関数にはパラメータとしてポインタを渡す必要があるので必ず必要になる。

ここでは、ポインタの簡単な使用例を下に示す。

# Pointer https://crystal-lang.org/api/1.9.2/Pointer.html
 
# 10バイトの0クリアされたメモリを確保する。
p0 = Pointer.malloc(10, 0_u8)
p1 = p0
p p1.value  # ポインタ先頭の値
p1.value = 0x30  # ポインタ先頭の値を書き換え
p p1.value  # 書き換えがうまくいったかチェック
# 続く4つの値を書き換え
p1 += 1
p1.value = 0x31
p1 += 1
p1.value = 0x32
p1 += 1
p1.value = 0x33
 
# バッファの内容を全部表示
puts "Contents .."
10.times do |i|
  p1 = p0 + i
  p p1.value
end
# メモリは自動的に解放される。
puts ".. Done."

このサンプルの実行例を下に示す。

$ ./bin/pointer
0
48
Contents ..
48
49
50
51
0
0
0
0
0
0
.. Done.
$

Crystal: CSV

CSV クラスは CSV データを扱うクラスである。このクラスを使うと、CSV データの扱いが非常に楽になる。

次のコードは Crystal のドキュメントに出ているサンプルにコメントを追加したものである。

# CSV
require "csv"
 
# コンストラクタ CSV.new(sio : String|IO, headers=false, strip=false, separator : Char = DEFAULT_SEPARATOR, quote_char : Char = DEFAULT_QUOTE_CHAR)
csv = CSV.new("Name, Age\nJohn, 20\nPeter, 30", headers: true)
csv.next # => true 次の行へ (最初の行はタイトル行)
 
# データの先頭行
csv["Name"]  # => "John"
csv[0]       # => "John"
csv[-2]      # => "John"
csv[/name/i] # => "John"
 
csv["Age"] # => " 20"
 
csv.row.to_a # => ["John", " 20"]   配列に変換
csv.row.to_h # => {"Name" => "John", "Age" => " 20"}  連想配列に変換
 
# データ行の2行目
csv.next    # => true
csv["Name"] # => "Peter"
 
csv.next # => false  データ行は2行だけなので false になる。

Crystal: Iterator

Iterator モジュールを使うと様々な反復子 (iterator) を容易に作ることができる。

Range は最初から要素が決まっているが、Iterator の場合は要求があるごとに値を決めるところが違い、より柔軟性がある。

次にいくつかの Iterator の作り方を示す。

# Iterator https://crystal-lang.org/api/1.9.2/Iterator.html
 
# 元のレンジ
range1 = 1..100
 
# 範囲を指定して選択
iter1 = range1.select(30..33)
p iter1.to_a
 
# 条件を指定して選択
iter2 = range1.each.select ->(x : Int32) {x > 50 && x % 10 == 0}
p iter2.to_a
 
# 条件を指定して除外
iter3 = range1.each.reject ->(x : Int32) {x > 5} 
p iter3.to_a
 
# 値を指定してステップ
iter4 = range1.each.step(30)
p iter4.to_a
 
# 範囲をスキップしてからステップ
iter5 = range1.each.skip(60).step(10)
p iter5.to_a

これを実行すると次のようになる。

$ ./bin/iterator
[30, 31, 32, 33]
[60, 70, 80, 90, 100]
[1, 2, 3, 4, 5]
[1, 31, 61, 91]
[61, 71, 81, 91]
$

ステップに関してはさらに柔軟性のあるステップが可能である。詳しくは Steppable モジュール参照。

次の例は Crystal のドキュメントに出ているサンプルを少し変更してコメントを追加したものである。このようにクラスを定義するとより柔軟性のある反復子を作ることができる。

# iterator2.cr
 
# クラスの定義
class Step2Iterator
  include Iterator(Int32)  # Iterator モジュールをクラスに含める。
 
  # コンストラクタ
  def initialize(@size : Int32)
    @produced = 0
  end
 
  # 次の値を返すオーバーライドメソッド
  def next
    if @produced < @size
      @produced += 2  # ステップ 2 で値を生成
    else
      stop  # @size を超えたら終わり
    end
  end
end
 
# 結果を表示
it = Step2Iterator.new(10)
p it.to_a

この実行例を下に示す。

$ ./bin/iterator2
[2, 4, 6, 8, 10]
$

Iterator も Range 同様に each メソッドなどを使って反復処理に利用する。

iter.each do |x|
  ....
end

Crystal: Spec によるテスト

Crystal には Spec モジュールと言うコードテスト用のモジュールがあり、これを使って自動テストができる。

Spec のサンプル

次の例は Testing Crystal Code というページに出ているサンプルである。

require "spec"
 
describe Array do
  describe "#size" do
    it "correctly reports the number of elements in the Array" do
      [1, 2, 3].size.should eq 3
    end
  end
 
  describe "#empty?" do
    it "is true when no elements are in the array" do
      ([] of Int32).empty?.should be_true
    end
 
    it "is false if there are elements in the array" do
      [1].empty?.should be_false
    end
  end
end

describe は何のテストかと言うコメントみたいなもので、テスト用のブロックを伴い、その中に describe が入れ子になっていてもよい。

it が実際にテストを行う部分で「~は~であるべきだ。」という書き方をする。

プロジェクトでの使用例

crystal init lib コマンドでライブラリ用のプロジェクトを作ると、そのプロジェクト内に ./spec フォルダが作成される。

この中には、次のファイルが自動的に作成される。この例で spectest はプロジェクト名なのでプロジェクトごとに変わる。

spec_helper.cr
spectest_spec.cr

spec_helper.cr はテストが実行される前に実行され、テスト準備などを行う。デフォルトでは次のような内容になっている。ただし、spectest はプロジェクト名であるのでプロジェクトごとに変わる。

require "spec"
require "../src/spectest"

次にこの例でテストの対象となる src/spectest.cr の内容を示す。このモジュールには radian() と cosdeg() という関数が含まれる。

# TODO: Write documentation for `Spectest`
module Spectest
  extend self
  VERSION = "0.1.0"
 
  # 度をラジアンに変換する。
  def radian(theta)
    theta / 180.0 * Math::PI
  end
 
  # 角度を「度」で与える cos 関数
  def cosdeg(theta : Float64) : Float64
    Math.cos(radian(theta))
  end
 
end

このモジュールのテストを行う spectest_spec.cr の例を示す。この例では 90.0度に対する cosdeg() 関数のテストを行っているが、cosdeg(90.0) は完全な 0 ではないのでエラーが表示される。

require "./spec_helper"
 
describe Spectest do
  # cosdeg() のテスト
  it "cosdeg() works" do
    Spectest.cosdeg(90.0).should be >= 0.0 # OK
    Spectest.cosdeg(90.0).should be < 1.0e-16 # OK
    Spectest.cosdeg(90.0).should eq(0.0)  # 誤差があるため完全な 0.0 にはならない。
  end
end

このテストを実行するには、つぎのように crystal spec コマンドを実行する。

$ crystal spec
F

Failures:

  1) Spectest works
     Failure/Error: Spectest.cosdeg(90.0).should eq(0.0)

       Expected: 0.0
            got: 6.123233995736766e-17

     # spec/spectest_spec.cr:9

Finished in 191 microseconds
1 examples, 1 failures, 0 errors, 0 pending

Failed examples:

crystal spec spec/spectest_spec.cr:6 # Spectest works

Crystal に for 文がない! (Ruby との違い)

たいていのプログラミング言語では for 文というのがあって、繰り返し処理に使われる。

しかし、Crystal には for 文がない。

それではどうするかというと、Ruby にもある each メソッドなどを使うことで代用できる。

それ以外にも Ruby との違いが多くある。それらの詳細は Crystal for Rubyists に記載がある。

次にいくつかの例を示す。

型の指定

Ruby は動的に型が決まるので変数の型指定はできない。Crystal では静的に型を決めるので明示的に型指定を行うことができる。(普通は型推論で型が決められる)

例) n = 0_i16 # 16ビット整数
あるいは
n : Int16 = 0 # 16ビット整数

メソッド名の違いや別名の有無

次にこの問題の例を示す。

Ruby では配列長は .size だけでなく .length や .count で取得できる。一方、Crystal では .size のみである。

Ruby の Enumerable.detect メソッドは、Crystal では Enumerable.find になる。

文字型の存在

Ruby は動的に型が決まるので、文字列リテラルは ‘…’ と書くこともできる。しかし、Crystal では文字列と文字は別の型なので、文字列リテラルを ‘…’ と書くことはできない。

配列などの []

配列などの要素を取得するときに鍵かっこでそのインデックスを指定するが、Ruby では存在しないインデックスを指定すると nil か返される。

一方、Crystal では例外が発生する。

もし、Crystal で Ruby のように nil を受け取りたい場合は、[]? を使用する。

名前付きパラメータの使用

次のような名前付きパラメータは Ruby ではエラーになるが、Crystal では問題なく動作する。

def process_data(a, b)
  # do stuff...
end

process_data(b: 2, a: "one")

クラスのプロパティの指定方法

クラスのプロパティ指定方法が両者で次のように異なる。

  • attr_accessor => property
  • attr_reader => getter
  • attr_writer => setter

さらに Crystal では nil の可能性がある場合やブール型のプロパティには ? を付ける。

property?
getter?

クラスメソッドの使用

Ruby ではクラスメソッドの使用時、:: を使用するが、Crystal では . を使用する。

Ruby の場合 File::exists?(path)
Crystal の場合 File.exists?(path)

プライベートメソッド

Crystal ではプライベートなメソッドは下の例のように private を付ける。

private def method
  42
end

疑似定数

FILE などの疑似定数に違いがある。詳細は Pseudo Contants 参照。

$ で始まる特殊変数は Crystal では使えないものがある。

例として、正規表現関連に関しては $~ や $1, $2 などは使えるが、その他は使用できない。

プロセスの終了コード $? は使えるが、パラメータの最後のインデックス $# は使えない。

rescue 句の書き方

例外処理の rescue 句だが、Ruby では rescue XXXError => e のように書くが、Crystal では rescue e: XXXError のように書く。

Crystal: ソート

Array または StaticArray の要素を並べ替えるには、sort やその系統のメソッドを使用する。

  • sort 要素を昇順に並べ替え結果を返す。元の配列は変化しない。
  • sort! 元の配列の要素を昇順に並べ替える。
  • ブロック付きの sort 比較関数を定義し、その結果 (-1, 0, 1) によりソートする。
  • sort_by 要素そのものでなく要素の属性などで比較してソートする。

ハッシュ(連想配列)には並べ替えのメソッドがないので配列に変換してから並べ替えを行う。

次に簡単なソートの例を示す。

# sort.cr
# 数値配列のソート
a = [5, 2, 0, 4]
b = a.sort
p! a  # a は変化しない。
p! b  # ソート結果
a.sort!
p! a # a が変化する。
c = a.sort {|x, y| y <=> x}  # 逆順でソート
p! c
# StaticArray のソート
sa = StaticArray[5, 2, 0, 4]  # StaticArray でも同様にできる。
sb = sa.sort
p! sb
# 文字列配列のソート
astr = ["Crystal", "is", "cool"]
bstr = astr.sort  # 普通のソートだと辞書順
cstr = astr.sort_by {|x| x.size}  # 長さ順にソート
p! bstr
p! cstr
# ハッシュのキーを並べ替える。
hash = {Indonesia: "Jakarta", Thailand: "Bangkok", Philippines: "Manila", Vietnam: "Hanoi"}.to_h # キーがシンボルに変換される。
p! hash
k = hash.keys.sort
p! k

このサンプルの実行例を示す。

$ ./bin/sort
a # => [5, 2, 0, 4]
b # => [0, 2, 4, 5]
a # => [0, 2, 4, 5]
c # => [5, 4, 2, 0]
sb # => StaticArray[0, 2, 4, 5]
bstr # => ["Crystal", "cool", "is"]
cstr # => ["is", "cool", "Crystal"]
hash # => {:Indonesia => "Jakarta", :Thailand => "Bangkok", :Philippines => "Manila", :Vietnam => "Hanoi"}
k # => [:Indonesia, :Philippines, :Thailand, :Vietnam]
$

Crystal: スタックなどとして使える Deque

Deque は配列に様々な機能を追加してものである。

このオブジェクトは例えば

  • スタック
  • キュー
  • ローテーションバッファ

などとして使える。

次の例は様々なメソッドを使って Deque を操作するものである。

# Deque https://crystal-lang.org/api/1.9.2/Deque.html
 
q1 = Deque.new(3, 0_i32)  # 長さ 3 の 0 で初期化された Deque
q2 = Deque.new([1, 2, 3])  # 配列で初期化された Deque
q3 = Deque{5, 6, 7, 8, 9}  # 直接、要素を指定して初期化 
p! q1
p! q2
p! q3
q1.push(10) # 最後に追加
p! q1
q1 << 11  # << は push と同じ
p! q1.size  # 要素数
p! q1[0]  # 先頭の要素
p! q1[q1.size-1] # 最後の要素
v1 = q1.pop  # 最後を取り出す
p! v1 # 最後の要素の確認
p! q1  # q1 の内容確認
v2 = q2.shift  # 先頭を取り出す。
p! v2 # 先頭の要素の確認
p! q2 # q2 の内容確認
q3.rotate!  # 要素全体を回転
p! q3
q3.insert(1, 10)  # 指定位置に要素を挿入
p! q3
q3.reject! {|x| x >= 10}  # 条件を指定して要素を削除
p! q3

この実行例を下に示す。

$ ./bin/deque
q1 # => Deque{0, 0, 0}
q2 # => Deque{1, 2, 3}
q3 # => Deque{5, 6, 7, 8, 9}
q1 # => Deque{0, 0, 0, 10}
q1.size # => 5
q1[0] # => 0
q1[q1.size - 1] # => 11
v1 # => 11
q1 # => Deque{0, 0, 0, 10}
v2 # => 1
q2 # => Deque{2, 3}
q3 # => Deque{6, 7, 8, 9, 5}
q3 # => Deque{6, 10, 7, 8, 9, 5}
q3 # => Deque{6, 7, 8, 9, 5}
$

Crystal: 複素数

Crystal では標準で複素数がサポートされている。

ただし、複素数を使うには require “complex” が必要である。

Complex 構造体では基本的な演算や関数がサポートされているが、算術関数などは Math モジュールでサポートされている。

次に複素数の使用例を示す。

# Complex https://crystal-lang.org/api/1.9.2/Complex.html
require "complex"
 
# 作成
# z0 = -1.0 + 1.0i  <== エラーになる。
z1 = Complex.new(1, 2) # インスタンス化
p! z1
p! 1.to_c  # Number.to_c メソッド
p! 1.i  # Number.i メソッド
p! Complex.zero  # 0+0i
 
# 演算
z2 = Complex.new(-2.4, 3.6)
p! z1 + z2
p! z1 - z2
p! z1 * z2
p! z1 / z2
 
# 比較
p! z1 == z2
p! z1 == z1.clone
 
# 関数
p! z1.abs  # 絶対値
p! z1.conj  # 共役複素数
p! z1.real # 実部
p! z1.imag # 虚部
p! z1.inv # 反転
p! z1.polar # 極形式
p! z1.phase # 位相

この実行例を下に示す。

$ ./bin/complex
z1 # => (1.0 + 2.0i)
1.to_c # => (1.0 + 0.0i)
1.i # => (0.0 + 1.0i)
Complex.zero # => (0.0 + 0.0i)
z1 + z2 # => (-1.4 + 5.6i)
z1 - z2 # => (3.4 - 1.6i)
z1 * z2 # => (-9.6 - 1.1999999999999997i)
z1 / z2 # => (0.25641025641025644 - 0.4487179487179486i)
z1 == z2 # => false
z1 == z1.clone # => true
z1.abs # => 2.23606797749979
z1.conj # => (1.0 - 2.0i)
z1.real # => 1.0
z1.imag # => 2.0
z1.inv # => (0.2 - 0.4i)
z1.polar # => {2.23606797749979, 1.1071487177940904}
z1.phase # => 1.1071487177940904
$

Crystal: 数学関数

Crystal の数学関数は Math モジュールに含まれる。

その特徴としては算術関数だけでなく一部の特殊関数もサポートされていることである。

さらに、複素数にも対応している。

なお、abs などの関数は Int32 や Float64 などのメソッドである。

次に一部の関数の使用例を示す。

# Math https://crystal-lang.org/api/1.9.2/Math.html
require "complex"
 
# 平方根
p! Math.sqrt(2.0)  # Float64
p! Math.isqrt(65)  # Int32 (最も近い整数になる)
z1 = Complex.new(2.0, 1.0)
p! Math.sqrt(z1)  # Complex
 
# 三角関数
x = 45.0/180.0*Math::PI
p! Math.sin(x)
p! Math.cos(x)
p! Math.tan(x)
 
# 逆三角関数
p! Math.asin(0.7071)
p! Math.acos(0.7071)
p! Math.atan(1.0)
 
# 指数関数、対数関数
p! Math.exp(0.0)
p! Math.exp2(0.0)
p! Math.log(2.0)
p! Math.log2(2.0)
p! Math.log10(10.0)
 
# 双曲線関数
p! Math.tanh(1.0)
 
# 特殊関数
p! Math.erf(-2.0) # 誤差関数
p! Math.gamma(1.0)  # ガンマ関数

この実行例を下に示す。

$ ./bin/math
Math.sqrt(2.0) # => 1.4142135623730951
Math.isqrt(65) # => 8
Math.sqrt(z1) # => (1.455346690225355 + 0.34356074972251244i)
Math.sin(x) # => 0.7071067811865475
Math.cos(x) # => 0.7071067811865476
Math.tan(x) # => 0.9999999999999999
Math.asin(0.7071) # => 0.7853885733974476
Math.acos(0.7071) # => 0.785407753397449
Math.atan(1.0) # => 0.7853981633974483
Math.exp(0.0) # => 1.0
Math.exp2(0.0) # => 1.0
Math.log(2.0) # => 0.6931471805599453
Math.log2(2.0) # => 1.0
Math.log10(10.0) # => 1.0
Math.tanh(1.0) # => 0.7615941559557649
Math.erf(-2.0) # => -0.9953222650189527
Math.gamma(1.0) # => 1.0
$

Crystal: 例外処理

Crystal での例外処理は begin..rescue..ensure.. を使う所は Ruby と似ているが、異なる部分も多い。例えば

  • rescue の書き方
  • else がある

である。

Ruby では rescue は rescue XXXError => e のように書くが、Crystal では rescue e: XXXError と書く。

また、Crystal には else 句があり、これは例外が起きなかった時の処理を記述する。

次に例外処理の例を示す。

# 例外処理 https://crystal-lang.org/reference/1.9/syntax_and_semantics/exception_handling.html
#        https://crystal-lang.org/api/1.9.2/Exception.html
 
# begin..rescue の使用例
begin
  a = Array(Int32).new  # 長さ0の整数配列
  puts a[1].to_s
rescue e  # 例外時に実行される
  puts e.message
end
 
# begin..rescue..else の使用例
begin
  a = Array(Int32).new(2, 0)  # 長さ2の整数配列
  puts a[1].to_s
rescue e
  puts e.message
else  # 例外が起きなかったときに実行される。
  puts "OK"
end
 
# ensure の使用例
begin
  raise("ensure test")
rescue e
  puts e.message
ensure  # 例外の有無に関わらず実行される。
  puts "OK"
end
 
# 例外ごとの処理
a = {A:0, B:2}
begin
  a["A"]
  puts "OK"
rescue e: KeyError
  puts e.message
rescue e: RuntimeError
  puts e.message
rescue
  puts "Fatal error"
end
 
# 独自例外
class MyError < Exception
  def initialize
    super
    @message = "MyError"
  end
end
 
begin
  raise(MyError.new)
rescue e
  puts e.message
end

この実行例を下に示す。

$ ./bin/exception
Index out of bounds
0
OK
ensure test
OK
OK
MyError
$

Crystal: マクロ

マクロを使うと同じような機能の関数をいくつも作らず、名前の一部を置き換えることによりひとつの関数にまとめることができる。

まず、例を挙げる。

この例は HTML タグを作る関数を作るマクロである。

ここには2つのマクロを定義しており、define_html_tag(name) は h1 タグのようなタグ内容を持つもの、そして、define_html_single_tag(name) は br タグのようなタグ内容を持たないタグを生成するマクロである。

タグ名は name としてマクロパラメータとして与えるが、その他の要素、ここでは class とタグ内容 content は関数のパラメータとして与えている。

マクロのパラメータは {{…}} で囲むことにより文字列の一部に埋め込まれる。関数パラメータは文字列の埋め込みなので #{…} で文字列内に埋め込んでいる。

# macro https://crystal-lang.org/reference/1.9/syntax_and_semantics/macros/index.html
 
macro define_html_tag(name)
  def {{name}}(content, classes="")
    if classes == ""
      %(<{{name}}>#{content}</{{name}}>)
    else
      %(<{{name}} class="#{classes}">#{content}</{{name}}>)
    end
  end
end
 
macro define_html_single_tag(name)
  def {{name}}(classes="")
    if classes == ""
      %(<{{name}}>)
    else
      %(<{{name}} class="#{classes}">)
    end
  end
end
 
define_html_tag h1
define_html_tag h2
define_html_single_tag br
p h1("H1")
p h2("H2")
p h2("H2", "text-center")
p br
p br("p-1")

この実行例を下に示す。

$ ./bin/macro
"<h1>H1</h1>"
"<h2>H2</h2>"
"<h2 class=\"text-center\">H2</h2>"
"<br>"
"<br class=\"p-1\">"
$

Crystal: アノテーション

アノテーション (Annotation) は クラス、モジュール、メソッド、パラメータなどのメタデータを定義するメカニズムである。

アノテーションは単なるヒントとして使うこともできるし、値を定義してその値を処理中に使用することもできる。

アノテーションの名前は次のように定義する。

annotation AnnotationName
end

この名前をクラス、モジュール、メソッドなどの先頭に定義して使用する。メソッドのパラメータの場合はパラメータ名の直前に定義する。

値を持たないアノテーションは単なるヒントであり、次のように使用する。

@[AnnotationName]
class Class1
  ....
end

値を持つアノテーションは () 内に値を定義する、値は複数でもよくカンマで区切って使用する。また、名前付きタプルの形式でもよい。

@[AnnotationName(x, y)]
def func()
...
end

次にアノテーションの使用例を示す。この例では HTML タグで内容を持たないタグのみを受け入れる関数を定義している。

# annotation https://crystal-lang.org/reference/1.9/syntax_and_semantics/annotations/index.html
annotation TagType
end
 
# HTML タグで内容を持たないタグのみを受け入れる
@[TagType(tagtype: :Single)]
def html_tag(name)
  if {{ @def.annotation(TagType)[:tagtype] }} == :Single
    %(<#{name}>)
  end
end
 
p html_tag("br")

次の例は、HTTP ハンドラの例で、受付できる HTTP メソッドをアノテーションで定義している。

#  HTTP メソッドの GET または POST を受け入れるハンドラ
@[HttpMethod(:GET, :POST)]
def handler(req, res)
  if ! ({{ @def.annotation(HttpMethod)[0] }} == req.method || {{ @def.annotation(HttpMethod)[1] }} == req.method)
    raise "Bad method."
  end
  # ハンドラの処理
  200
end
 
p handler(req, res)

既定のアノテーション

Link アノテーションは標準ライブラリに含まれるアノテーションである。これはこのアノテーションが付いた関数のビルドに必要な外部ライブラリを指定する。

これにより、ビルド時にそのライブラリがリンクされ、その関数が正しくリンクできる。例えば、次のように Link アノテーションを付けておくとライブラリ pcre がリンクされる。

@[Link(ldflags: "-lpcre")]

また、次のように Link アノテーションを付けておくと、共有ライブラリ pcre を探してリンクしてくれる。

@[Link("pcre")]

C のライブラリ関数を使用する際に、ヘッダファイル (*.h) が必要になるが、そのヘッダファイルについての情報は Include アノテーションを使うと解決できる。Include アノテーションの例は下記の Qiita の記事に出ている。

CrystalでC言語のバインディングを自動生成する crystal_lib

その一部の例を下に挙げる。

@[Include("event.h", prefix: %w(event_ EVENT_ Event))]

Crystal: Colorize で表示をカラフルに

Colorize モジュールのメソッドを使うと、カラー対応端末 (アプリケーション、例えば Tera term Pro) で表示文字列に色を付けたり、下線などの文字飾りをつけることができる。

簡単な Colorize の使用例を示す。

# colorize  https://crystal-lang.org/api/1.9.2/Colorize.html
require "colorize"
 
puts "Green".colorize(:green)
puts "After Green"
puts "Blue".colorize.blue
puts "Red Underline".colorize.fore(:red).mode(:underline)
puts "Light Yellow Backgound White".colorize.fore(:light_yellow).back(:dark_gray)
puts "Reset".colorize.default

Crystal: Slice

Slice は指定した型の指定したサイズのバッファの先頭を指すポインタである。

ただし、Pointer とは異なり unsafe ではない。

Slice は 抽象クラス IO で入出力バッファとしてよく使用される。

次に Slice の一部のメソッドの使用例を示す。

# slice https://crystal-lang.org/api/1.9.2/Slice.html
 
buf = Slice.new(4, sizeof(Int8))
buf.fill(0)  # 0 フィル
buf[0] = 8  # 値の変更
buf[1] = 16  # 値の変更
p! buf[1]  # 値の確認
buf.each do |b|  # 内容をすべて表示
  p b
end
ub = buf.to_unsafe  # ポインタへ変換
p typeof(ub)
buf2 = Slice.new(4, sizeof(Int8))
buf.copy_to(buf2) # buf の内容を buf2 へコピー
p buf2.to_a  # 配列に変換
buf2.reverse! # 要素を逆順にする。
p buf2

このサンプルの実行例を下に示す。

$ ./bin/slice
buf[1] # => 16
8
16
0
0
Pointer(Int32)
[8, 16, 0, 0]
Slice[0, 0, 16, 8]
$

Crystal: プロセスとパイプ

Crystal でプロセスを起動するには、Process クラスを利用する。

Process クラスのコンストラクタは次のようになっている。コンストラクタを実行すると Process オブジェクトが作成されて、直ちに実行が始まる。

new(command : String, args = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false, input : Stdio = Redirect::Close, output : Stdio = Redirect::Close, error : Stdio = Redirect::Close, chdir : Path | String | Nil = nil)

プロセスが実行されて終了するまえに、起動したプログラムが終了しないようにするためには、wait メソッドを実行する。

コンストラクタでプロセスオブジェクトを作らず、単純に他のコマンドを実行する場合は、クラスメソッドの run を使用するほうが簡単である。

次に Process クラスの簡単な使用例を示す。

# プロセス https://crystal-lang.org/api/1.9.2/Process.html
 
# プロセスオブジェクトを作成する。(そして直ちに実行される)
p1 = Process.new("ls", {"-l", "/"}, shell:true, output:STDOUT)  # ルートディレクトリ / の内容一覧を表示
p1.wait  # プロセスが終わるまで待つ。
 
# クラスメソッド run でコマンドを実行する。
sts = Process.run("python3", {"-V"}, output:STDOUT)  # python3 のバージョンを表示。python3 コマンドはシェルスクリプトでないので shell:true にしなくてよい。
p! sts.exit_status
 
# Ubuntu のバージョンを表示するシェルスクリプトを実行する。
Process.run("~/bin/ver", shell:true, output:STDOUT)  # シェル機能 (ここでは ~) を使う場合は shell:true にしないとエラーになる。
 
# ブロック付きバージョンの run
Process.run("perl", {"-v"}) { |pr|
  puts "pid = #{pr.pid} OK"
}

IO.pipe を使ってリダイレクトもできる。

# pipe
 
# プロセスにパイプで入力し、結果を標準出力に出力する。
r, w = IO.pipe(false, false)
w.puts "from the pipe."
w.close
Process.new("cat", shell:true, input:r, output:STDOUT).wait
 
# プロセスの出力だけをパイプで受け取る。
r2, w2 = IO.pipe(false, false)
Process.new("./hello_world.cgi", shell:true, input:r2, output:w2).wait
w2.close
r2.gets_to_end.lines.each do |line|
  puts line
end

Crystal: Signal

Signal は POSIX システムのシグナルを安全に取り扱える列挙型 (enum) である。

そして、シグナルはプロセス間で信号を送るときに使用される。

この列挙型には次の既定のシグナルが定義されている。

    INT = 2
    ILL = 4
    FPE = 8
    SEGV = 11
    TERM = 15
    ABRT = 6
    HUP = 1
    QUIT = 3
    TRAP = 5
    IOT = 6
    KILL = 9
    BUS = 7
    SYS = 31
    PIPE = 13
    ALRM = 14
    URG = 23
    STOP = 19
    TSTP = 20
    CONT = 18
    CHLD = 17
    TTIN = 21
    TTOU = 22
    IO = 29
    XCPU = 24
    XFSZ = 25
    VTALRM = 26
    USR1 = 10
    USR2 = 12
    WINCH = 28
    PWR = 30
    STKFLT = 16
    UNUSED = 31

このうち、INT は「割り込み」を意味し、INT シグナルはデフォルト動作としてアプリケーションを終了させる。

これはキーボードで Ctrl+C を押したとき発生する。

シグナルをトラップするには、trap メソッドを使う。このメソッドのブロックでそのシグナルのデフォルト動作を変更する。

デフォルト動作に戻すには、reset メソッドを使用する。

次に、シグナルの簡単な使用例を示す。

# Signal https://crystal-lang.org/api/1.9.2/Signal.html
puts "Ctrl+C を押すとプログラムが終了します。(デフォルト動作 3 秒間)"
sleep 3
# 3秒後に Ctrl+C のトラップが有効になる。
Signal::INT.trap do
  puts "\nCtrl+C がトラップされました!"  # Ctrl+C を入力すると、このメッセージが表示される。
end
puts "\nCtrl+C のトラップが有効になりました。(3秒間)\n"
sleep 3
# さらに 3 秒待つとトラップがリセットされる。
Signal::INT.reset
puts "\nCtrl+C がデフォルト動作に戻りました。"
sleep 3
puts ".. Done."

このプログラムの実行例を下に示す。

$ ./bin/signal
Ctrl+C を押すとプログラムが終了します。(デフォルト動作)

Ctrl+C のトラップが有効になりました。(3秒間)
^C
Ctrl+C がトラップされました!
^C
Ctrl+C がトラップされました!
^C
Ctrl+C がトラップされました!
^C
Ctrl+C がトラップされました!
^C
Ctrl+C がトラップされました!

Ctrl+C がデフォルト動作に戻りました。
^C
$

Crystal: OptionParser

コンソールアプリケーションを作るときに、機能やオプションをいろいろ指定する必要があることがある。

そのような場合、OptionParser クラスを使うと、複雑なオプションを実現するのが非常に楽である。

OptionParser では次のような形式のコマンドオプションが基本とされる。(これとは別の GNU 形式のオプションにも対応している)

コマンド名 サブコマンド名 オプション(複数) コマンド引数(複数)

次の例は OptionParser の説明のところに出ているサンプルに足りない以下の機能を追加したものである。

  • 不正なオプションがあったときの処理
  • コマンド引数の取得
# OptionParser https://crystal-lang.org/api/1.9.2/OptionParser.html
require "option_parser"
 
# サブコマンドフラグを初期化
verbose = false
salute = false
welcome = false
name = "World"
 
# ARGV に対いてパーサを構築
parser = OptionParser.new do |parser|
  # ヘルプ用のバナー
  parser.banner = "Usage: example [subcommand] [arguments]"
  # サブコマンドハンドラ
  parser.on("salute", "Salute a name") do
    salute = true
    parser.banner = "Usage: example salute [arguments]"
    parser.on("-t NAME", "--to=NAME", "Specify the name to salute") { |_name| name = _name }
  end
  # サブコマンドハンドラ
  parser.on("welcome", "Print a greeting message") do
    welcome = true
    parser.banner = "Usage: example welcome"
  end
  # オプションハンドラ
  parser.on("-v", "--verbose", "Enabled verbose output") { verbose = true }
  parser.on("-h", "--help", "Show this help") do
    puts parser
    exit
  end
end
 
# サポートしていないオプションが使われたときはエラーメッセージを表示する。
parser.invalid_option do |flag|
  STDERR.puts "ERROR: #{flag} is not a valid option."
  STDERR.puts parser
  exit(1)
end
 
# ファイル名などのメインパラメータ配列
parser.unknown_args do |a|
  p a
end
 
# ARGV を解析する。
parser.parse
 
# サブコマンドフラグに対応した処理
if salute
  STDERR.puts "Saluting #{name}" if verbose
  puts "Hello #{name}"
elsif welcome
  STDERR.puts "Welcoming #{name}" if verbose
  puts "Welcome!"
else
  puts parser
  exit(1)
end

この例では、サブコマンドが salute, welcome であり、そのぞれ異なるメッセージを表示する。

また、オプションは verbose と help があり、それぞれ短い形式 (-v, -h) と長い形式 (–verbose, –help) をサポートしている。

この実行例を示す。

$ bin/option_parser -h
Usage: example [subcommand] [arguments]
    salute                           Salute a name
    welcome                          Print a greeting message
    -v, --verbose                    Enabled verbose output
    -h, --help                       Show this help
$ $ bin/option_parser salute --verbose
[]
Saluting World
Hello World
$ $ bin/option_parser salute arg1 arg2
["arg1", "arg2"]
Hello World
$ bin/option_parser welcome arg1
["arg1"]
Welcome!
$

Crystal: 既定の例外クラス

例外が発生すると、例外クラスがインスタンス化され rescue 句でキャッチできる。

すべての例外の基底クラスは Exception であり、このクラスから派生した多数のクラスが存在する。

以下にそれらを挙げる。

  • ArgumentError
  • Channel/ClosedError
  • CSV/MalformedCSVError
  • Digest/FinalizedError
  • DivisionByZeroError
  • Enumerable/EmptyError
  • Enumerable/NotFoundError
  • File/AccessDeniedError
  • File/AlreadyExistsError
  • File/BadExecutableError
  • File/BadPatternError
  • File/NotFoundError
  • Server/ClientError
  • IndexError
  • InvalidByteSequenceError
  • IO.EOFError
  • IO.TimeoutError
  • JSON/SerializableError
  • KeyError
  • NilAssertionError
  • NotImplementedError
  • OpenSSL/Digest/UnsupportedError
  • OverflowError
  • RuntimeError
  • Socket/ConnectError
  • System/Group/NotFoundError
  • System/User/NotFoundError
  • SystemError
  • Time/FloatingTimeConversionError
  • Time/Location/InvalidLocationNameError
  • Time/Location/InvalidTimezoneOffsetError
  • Time/Location/InvalidTZDataError
  • WinError

これらを rescue 句で使う場合、Ruby と書き方が異なるので注意すること。下に例を示す。

a = {A:0, B:2}
begin
  a["A"]
  puts "OK"
rescue e: KeyError
  puts e.message
rescue e: RuntimeError
  puts e.message
rescue
  puts "Fatal error"
end

Crystal: 無名関数

他の言語ではたいていブロックは独自のスタックフレームを持つコードの塊りという扱いである。

一方、Crystal (Ruby も) では独自のスタックフレームを持つコードの塊りであることには変わりないが、外部からパラメータを渡し、最後に実行された式の値を返すことできる。

他の言語で言えば、無名関数や匿名関数、クロージャと呼ばれるものに近い。

そして、ブロックは他の関数のパラメータとして渡すことができるのも無名関数と同じである。

ブロックがパラメータであることを示すにはパラメータの名前に & を付ける。

次の例は、ブロックを関数パラメータとして渡すものである。

例1 ブロックを実行する関数 int_to_int を変数 proc に代入して call メソッドで実行する。

def int_to_int(&block : Int32 -> Int32)
  block
end
 
proc = int_to_int { |x| x + 1 }
n = proc.call(1) # => 2
puts n.to_s

例2 ブロックをコールバックするイベントハンドラ on_save を定義し、それをイベント発生関数 save により動作させる。

class Model
  def on_save(&block)
    @on_save_callback = block
  end
 
  def save
    if callback = @on_save_callback
      callback.call
    end
  end
end
 
model = Model.new
model.on_save { puts "Saved!" }
model.save # prints "Saved!"

次の例はアロー関数を変数 procdef に代入し、それをブロックをパラメータとする関数 some_proc に渡した関数を変数 proc に代入し、call メソッドで呼び出して使用する。

例3

def some_proc(&block : Int32 -> Int32)
  block
end
 
x = 0
procdef = ->(i : Int32) { x += i }
p procdef
proc = some_proc(&procdef)
p proc
y = proc.call(1)  # => 1
p y
y = proc.call(10) # => 11
p x               # => 11
p y

例4 自分で定義したブロック付きメソッドでブロックを呼び出すには yield を使う。

def foo
  yield(1,2)
end
 
foo {|a,b|
  p a + b

Crystal: Nilable な変数

Crystal には nil を取るかもしれない変数を定義できる。例えば、HTTP::Request.body は次のような型を持つ。これは Nil かもしれない IO 型という意味である。

def body: IO | Nil

そのため、直接、IO のメソッドを使うことができない。

しかし、nil でないことを確認してから、次の例のように as を使って強制的に型を IO にすることで IO のメソッドが使用できる。

unless response.body.nil?
   bs = response.body.as(IO).getb_to_end
end

Type refrection には他にも型に関するいろいろなメソッドの説明がある。

  • is_a? s.is_a?(String) は s が文字列なら true を返す。
  • nil? a.nil? は a が nil なら true を返す。
  • respond_to? a.response_to(:abs) は変数 a が abs というメソッドを持てば、true を返す。
  • as response.body.as(IO) は nilable な変数を nilable でなくする。
  • as? as と機能的に同じだが、キャストできない時は nil を返す。
  • typeof typeof(a) は変数 a の型を返す。例えば、a が32ビット整数なら Int32 を返す。
9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?