跳至內容
GitHub 儲存庫 論壇 RSS 新聞訂閱

Crystal 生日快樂!

Ary Borenzweig

昨天是 Crystal 一歲生日。耶! :-)

自從我們開始這個相當有雄心的專案以來,發生了很多事情,而且還有很多事情要做。

雖然它的語法與 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)。

聯集類型

您無需指定變數的類型。如果它被賦予多種類型,它將在編譯時擁有這些類型。在執行時,它只會有一種類型。

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 中,方法可以多載。多載來自引數的數量、類型限制和方法是否 *yield*。

# 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 中實作。這使得任何人都可以輕鬆地在這些類別上進行協作。

我們真的希望避免必須指定類型變數。事實上,我們希望避免必須區分泛型類別和非泛型類別。我們花了很長的時間(也許三個月?)試圖使其高效地運作,但我們無法做到。也許它沒有有效的解決方案。至少我們沒有找到任何能夠做到這一點的人。泛型類型是一個小的犧牲,但作為回報,我們可以獲得更快的編譯時間。

模組

當然,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

正規表示式

正規表示式目前使用 C 綁定到 PCRE 程式庫來實作。同樣,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,且沒有任何額外負擔:不涉及配置或閉包。

可以使用 instance_eval(&block) 在 Ruby 中實現類似的功能,但目前我們發現以這種方式實現它更容易,而且可能更容易使用。

規格

我們建立了一個 非常小的 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 了。我們不再需要維護編譯器的兩個實作。編譯時間將會大幅縮短(我們希望!)。

而且,很可能該語言會因此而大幅成長,而且我們將會學到很多在 Crystal 中編程的感覺以及需要改進的地方(除錯和分析出現在我們的腦海中)。他們稱之為「自我體驗」:-)