コンテンツへスキップ
GitHubリポジトリ フォーラム RSS-ニュースフィード

Crystalお誕生日おめでとう!

Ary Borenzweig

昨日、Crystalは1歳になりました。やったー! :-)

このかなり野心的なプロジェクトを開始して以来、多くのことが起こり、まだやるべきことがたくさんあります。

構文はRubyと非常によく似ていますが、多くの違いがあり、毎日その差は大きくなっています。

現在の言語の機能の概要を以下に示します。

効率的なコード生成

Crystalは**インタプリタではありません**。仮想マシンもありません。コードはLLVMを使用してネイティブマシンコードにコンパイルされます。

静的にコンパイルされる言語で通常行われるように、変数、インスタンス変数、またはメソッド引数の型を指定する必要はありません。代わりに、Crystalは可能な限りスマートになり、型を推論しようとします。

プリミティブ型

プリミティブ型はネイティブマシン型にマップされます。

true    # Bool
1       # Int32
1_u64   # UInt64
1.5     # Float64
1.5_f32 # Float32
'a'     # Char

ASCII文字列

Rubyと同様に、さまざまな種類があり、補間もサポートしています。

a = "World"
b = "Hello #{a}" #=> "Hello World"

異なるエンコーディングを処理する最良の方法をまだ決定する必要があるため、これは一時的な実装にすぎません。

StringがCrystal自体で実装されていることをご存知でしたか?サイズと文字バッファへのポインタを持たせるための非常に小さな魔法がありますが、それ以外はすべてその上に構築されています。

シンボル

:foo

実行時には、各シンボルは一意の整数で表されます。Symbol#to_sの実装のために整数から文字列へのテーブルが構築されます(ただし、現在String#internを行う方法はありません)。

ユニオン型

変数の型を指定する必要はありません。複数の型が割り当てられている場合、コンパイル時にはそれらの型を持ち、実行時には1つだけを持ちます。

if some_condition
  a = 1
else
  a = 1.5
end

# Here a can be an Int32 or Float64

a.abs  # Ok, both Int32 and Float64 define the 'abs' method without arguments
a.succ # Error, Float64 doesn't have a 'succ' method

「is_a?」を使用して型を確認できます。

if a.is_a?(Int32)
  a.succ # Ok, here a can only be an Int32
end

「responds_to?」を使用することもできます。

if a.responds_to?(:succ)
  a.succ # Ok
end

メソッド

Crystalのメソッドはオーバーロードできます。オーバーロードは、引数の数、型制限、メソッドの*yieldness*によって異なります。

# foo 1
def foo(x, y)
  x + y
end

# foo 2
def foo(x)
end

# foo 3
def foo(x : Float)
end

def foo(x)
  yield
end

foo 1, 1      # Invokes foo 1
foo 1         # Invokes foo 2
foo 1.5       # Invokes foo 3
foo(1) { }    # Invokes foo 4

実行時にメソッドの引数の数、ブロックが与えられたかどうか、引数の型が何であるかをチェックする必要があることと対照的に、これははるかに読みやすく効率的であると考えています。

また、実行時に「引数の数が間違っています」という例外がスローされることはありません。Crystalでは、コンパイル時のエラーです。

メソッドがyieldするかどうかを基にした最後のビットのオーバーロードはおそらく変更され、再考する必要があります。

クラス

インスタンス変数の型を指定する必要はありませんが、インスタンス変数に割り当てられたすべての型により、その変数はユニオン型になります。

class Foo
  # We prefer getter, setter and property over
  # attr_reader, attr_writer and attr_accessor
  getter :value

  # Note the @value at the argument: this is similar to Coffeescript
  # and we think it's a nice syntax addition.
  def initialize(@value)
  end
end

foo = Foo.new(1)
foo.value.abs # Ok

# At this point @value is an Int32

foo2 = Foo.new('a')

# Because of the last line, @value is now an Int32 or Char.
# Char doesn't have an 'abs' method, so a compile time error is issued.

@valueに異なる型を持つ異なるFooクラスが本当に必要な場合は、ジェネリッククラスを使用できます。

class Foo(T)
  getter :value

  def initialize(@value : T)
  end
end

foo = Foo.new(1)    # T is inferred to be an Int32, and foo is a Foo(Int32)
foo.value.abs       # Ok

foo2 = Foo.new('a') # T is inferred to be a Char, and foo2 is a Foo(Char)
foo2.value.ord            # Ok

# You can also explicitly specify the generic type variable
foo3 = Foo(String).new("hello")

ArrayとHashもジェネリッククラスですが、リテラルを使用して構築することもできます。要素が指定されている場合、ジェネリック型変数は推論されます。要素が指定されていない場合は、Crystalにジェネリック型変数を伝える必要があります。

a = [1, 2, 3]            # a is an Array(Int32)
b = [1, 1.5, 'a']        # b is an Array(Int32 | Float64 | Char)
c = [] of String         # c is an Array(String), same as doing Array(String).new

d = {1 => 2, 3 => 4}     # d is a Hash(Int32, Int32)
e = {} of String => Bool # e is a Hash(String, Bool), same as doing Hash(String, Bool).new

そして、はい、ArrayHashは完全にCrystalで実装されています。これにより、誰でもこれらのクラスに簡単に協力できます。

私たちは本当に型変数を指定する必要性を避けたいと思っていました。実際、ジェネリッククラスと非ジェネリッククラスを区別する必要性を避けたいと思っていました。それを**効率的に**機能させるために長い時間(おそらく3か月間?)を費やしましたが、できませんでした。効率的な解決策がないのかもしれません。少なくとも、それを実行できた人を発見できませんでした。ジェネリック型は小さな犠牲ですが、その代わりにコンパイル時間が大幅に短縮されます。

モジュール

もちろん、モジュールもCrystalに存在し、ジェネリックにすることもできます。

ブロック

現時点では、ブロックを変数に保存したり、別のメソッドに渡したりすることはできません。つまり、クロージャはまだ不足しています。スマートな型推論が必要な場合、これは簡単ではないため、検討する必要があります。

Cへのバインディング

CrystalでCへのバインディングを宣言できます。Cを使用したり、ラッパーを作成したり、別の言語を使用したりする必要はありません。たとえば、これはSDLバインディングの一部です。

lib LibSDL("SDL")
  INIT_TIMER       = 0x00000001_u32
  INIT_AUDIO       = 0x00000010_u32
  # ...
  struct Rect
    x, y : Int16
    w, h : UInt16
  end
  # ...
  union Event
    type : UInt8
    key : KeyboardEvent
  end
  # ...
  fun init = SDL_Init(flags : UInt32) : Int32
end

value = LibSDL.init(LibSDL.INIT_TIMER)

ポインタ

メモリを割り当て、言語に型としてポインタを持つことでCとインターフェースできます。

values = Pointer(Int32).malloc(10) # Ask for 10 ints

正規表現

正規表現は、現時点ではPCREライブラリへのCバインディングを使用して実装されています。Regexpは完全にCrystalで記述されています。

"foobarbaz" =~ /(.+)bar(.+)/ #=> 0
$1                           #=> "foo"
$2                           #=> "baz

範囲

繰り返しますが、Crystalで実装されています。

例外

例外を発生させ、救助できます。libunwindを使用して実装されています。スタックトレースには行番号とファイル名がまだ不足しています。

C関数のエクスポート

Cにエクスポートする関数を宣言できるため、CrystalコードをコンパイルしてCで使用できます(オブジェクトファイルを作成するコンパイラフラグはまだありませんが、簡単に実装できるはずです)。

fun my_c_function(x : Int32) : Int32
  "Yay, I can use string interpolation and call it #{x} times from C"
end

マクロ

これは、ASTノードからソースコードを生成できる言語の実験的な機能です。

macro generate_method(name, value)
  "
  def #{name}
    #{value}
  end
  "
end

generate_method :foo, 1

puts foo # Prints: 1

getter、setter、propertyマクロは同様の方法で実装されていますが、同じことを達成するためのより強力で簡単な方法を検討しているため、この機能は消える可能性があります。

スコープ付きYield

yieldに似ていますが、ブロックの暗黙的なスコープを変更します。

def foo
  # -1 becomes the default scope where methods
  # are looked up in the given block
  -1.yield(2)
end

foo do |x|
  # Invokes "abs" on -1
  puts abs + x #=> 3
end

これにより、オーバーヘッドゼロの強力なDSLを記述できます。割り当てやクロージャは関与しません。

Rubyではinstance_eval(&block)で同様のことが実現できますが、現時点ではこの方法で実装する方が簡単であり、おそらく使いやすいため、この方法を選択しています。

スペック

RSpecの非常に小さなクローンを構築し、標準ライブラリと新しいコンパイラをテストするために使用しています。Arrayクラスのサンプルスペックを以下に示します。

require "spec"

describe "Array" do
  describe "index" do
    it "performs without a block" do
      a = [1, 2, 3]
      a.index(3).should eq(2)
      a.index(4).should eq(-1)
    end

    it "performs with a block" do
      a = [1, 2, 3]
      a.index { |i| i > 1 }.should eq(1)
      a.index { |i| i > 3 }.should eq(-1)
    end
  end
end

そのため、Crystalを使用すると、テストを非常に簡単に記述でき、同時に型安全性も得られます。両方の世界の最良の部分です。

ロードマップ

実装するべきことがまだたくさんあり、試すべきアイデアもたくさんあります。

  • ガベージコレクタが必要であり、効率的で同時実行性の高いものを求めています。
  • ErlangやGoと同様の同時実行プリミティブが必要です。
  • より優れたメタプログラミングが必要です。
  • Cバインディングだけでなく、効率的なラッパーを作成し、メモリの割り当てを少なくするために、構造体を使用する可能性があります。
  • タプル、名前付きタプル、名前付き引数が必要です。

しかし、現在最も欲しいものは、コンパイラをCrystalで記述することです。これを行うと、Rubyは不要になります。コンパイラの2つの実装を維持する必要がなくなります。コンパイル時間は大幅に短縮されます(願っています!)。

また、このため、言語は大幅に成長する可能性が高く、Crystalでのプログラミングの感触と改善が必要な点(デバッグとプロファイリングが思い浮かびます)を多く学ぶことになります。彼らはそれを「ドッグフーディング」と呼んでいます :-)