Crystal書いてて気づいたこと

今日はuser agent parserのruby版をcrystalに移植していた。

github.com

crystalはv0.18.7を使っているのだけど書いてて気づいたことがあるのでメモ

not nilの推論はインスタンス変数には効かない

crystalは型に厳しいです。変数fooがStringあるいはNil型であるときはどちらの型にも存在するメソッドしか呼べません。
Stringにしかないメソッドを呼ぶときには条件分岐でnot nilであることを保証させます。

# foo : String?とする
if foo.nil?
  # ここではfooはNil型
else
  # ここではfooはnot nilなのでString型
end

ローカル変数ならこれでおkですがインスタンス変数はこれだとダメです。

class Klass
  @foo : String?
  
  def size
    if @foo.nil?
      0
    else
      @foo.size
    end
  end
end

puts Klass.new.size

# Error in line 13: instantiating 'Klass#size()'
# 
# in line 8: undefined method 'size' for Nil (compile-time type is (String | Nil))

いちどローカル変数にとれば通るみたいです。インスタンス変数だと関数スコープ外から変更可能だからかな、って推測しています。

class Klass
  @foo : String?
  
  def size
    bar = @foo
    if bar.nil?
      0
    else
      bar.size
    end
  end
end

puts Klass.new.size

#to_sと#inspectのoverride

Object#to_sObject#inspect はoverrideするべきではないらしい(usually MUST NOT)
ref: https://crystal-lang.org/api/0.18.7/Object.html#to_s-instance-method

代わりに #to_s(io : IO)#inspect(io : IO) に対してやれとのこと。

def Klass
  def to_s(io : IO)
    "this is Klass".to_s(io)
  end
end

puts Klass.new.to_s #=> "this is Klass"
puts "#{Klass.new}" #=> "this is Klass"

*argsはTuple型

調べればわかるんだけど調べたのでメモ。型違うのでちょっと注意。

def foo(*args)
  puts args.class
end

foo(1, 2, 3) #=> Tuple(Int32, Int32, Int32)
foo("bar", "foobar") #=> Tuple(String, String)