2020-12-23

Steep by Steep(Day2)

 この記事は、ケーシーエスキャロット Advent Calendar 2020 23日目の記事です。


昨日は、naokishi の aws cli と jq コマンド でした。

今、AWS使ってるんですねー。もうWindowsアプリは開発していないのかな…?


さて、前回はエラーが増えたところで終わりました。

一度、リポジトリ にあるサンプルコードを実行してみようか…

ということで、コピペで実行してみると…



$ bundle exec steep check --log-level=fatal
lib/phone.rb:9:2: MethodBodyTypeMismatch: method===, expected=bool, actual=(bool | nil) (def ==(other))
  (bool | nil) <: bool="" false="" nil="" true="="> nil 
<: bool="" false="" nil="" true="=">
エラーが出ました... が、一昨日出ていたエラーとは違うし、
このメソッド以外のチェックは通っているようなので、
このエラーは置いておいて、メソッドの定義について確認してみます。

うーん…(コードを見比べている)

!!!

メソッドのパラメータだ!!!!
で、以下のようにコードを修正して実行してみると…

class User
  def initialize(no:, name:)                                                                                                
    @no = no
    @name = name
    @scores = Array.new
  end
  attr_reader :no, :name, :scores

  def add(score:)
    @scores << score
  end
end

$ bundle exec steep check --log-level=fatal
bin/main.rb:20:2: NoMethodError: type=singleton(::File), method=open (File.open(fpath, "w") {|fd| fd.write data })
bin/main.rb:22:24: ArgumentTypeMismatch: receiver=singleton(::Analyzer), expected={ :path => untyped }, actual=::String (fpath)
lib/analyzer.rb:6:2: MethodArityMismatch: method=(self) (def self.load(path))
lib/analyzer.rb:15:2: MethodArityMismatch: method=(self) (def self.parse_row(row))
lib/analyzer.rb:16:8: IncompatibleArguments: receiver=singleton(::User), method_type=(name: untyped, no: untyped) -> ::User (User.new(row["No"], row["Name"]))

メソッドの定義がSteepで解析されて、Userのエラーが消えました!!!

同じように、Analyzerも修正すると、

bin/main.rb:20:2: NoMethodError: type=singleton(::File), method=open (File.open(fpath, "w") {|fd| fd.write data })
bin/main.rb:22:24: ArgumentTypeMismatch: receiver=singleton(::Analyzer), expected={ :path => untyped }, actual=::String (fpath)
lib/analyzer.rb:16:8: IncompatibleArguments: receiver=singleton(::User), method_type=(name: untyped, no: untyped) -> ::User (User.new(row["No"], row["Name"]))

エラーが更に減った〜

出ているエラーが全部違うので、ここからは1つずつ確認していこう…


NoMethodError

これはRubyを使っているとよく見るエラーですが、RubyのNoMethodErrorではなく、TypeInference::MethodCall::NoMethodError です。

エラーとして検出されている場所は、`File.open()` のRubyのクラス。
Rubyのクラス検知して、エラーにされている???

うーん...  steep の `--verbose` オプション付けて、ログ出力してみるか…


bundle exec steep check --log-level=fatal --verbose --log-output="./check.log"

出力されたログを確認してみると、、、
あぁ、なるほど、ここのファイル読み込んでる。
で、File.rbs を見てみると…

なるほど、確かに `File.open()` が定義されていない…。
initialize が定義されているので、 `File.new()` に変えてみて、実行してみる。

bin/main.rb:22:2: UnexpectedBlockGiven: method_type=((::string | ::_ToPath | ::int), ?(::string | ::int), ?::int) -> ::File (File.new(fpath, "w") {|fd| fd.write data })
bin/main.rb:25:24: ArgumentTypeMismatch: receiver=singleton(::Analyzer), expected={ :path => untyped }, actual=::String (fpath)
lib/analyzer.rb:16:8: IncompatibleArguments: receiver=singleton(::User), method_type=(name: untyped, no: untyped) -> ::User (User.new(row["No"], row["Name"]))
お、エラーが変わった。(行数が変わっているのは、元のコードをコメントアウトして残しているため)

UnexpectedBlockGiven は、`File.new()` の定義は、Fileクラスを返す定義になっていて、blockを渡す定義がされていないからだな。なるほど、なるほど。

`File.open()` の行を以下のように結局書き換える。

   # rbs に File.open()が定義されていない
   # File.open(fpath, "w") {|fd| fd.write data }
   fd = File.new(fpath, "w")
   fd.write data
   fd.close
 

$ bundle exec steep check --log-level=fatal
bin/main.rb:26:24: ArgumentTypeMismatch: receiver=singleton(::Analyzer), expected={ :path => untyped }, actual=::String (fpath)
lib/analyzer.rb:16:8: IncompatibleArguments: receiver=singleton(::User), method_type=(name: untyped, no: untyped) -> ::User (User.new(row["No"], row["Name"]))
やった! main.rb で出ていた、Fileクラスのエラーが消えました!

ArgumentTypeMismatch, IncompatibleArgument


この2つのエラーですが、キーワード引数に変更した時の呼び出し側の修正をしていなかったことで出ていたエラーでした。

bin/main.rb 実行したらエラーになったので、修正したら、上記エラーはでなくなりました。(テスト書いてないから、こういうことになるんだよ…テスト大事)


エラーが出なくなったから、終わり!!!にはなりません。


なぜなら、rbsファイルには、`untyped` の宣言しかしていないのだから…

本番はこれからです。

rbsファイルに型を定義する

rbs ファイルを以下のように修正しました。
(Structを使ったクラスはとりあえず、untypedのまま)

class User
  @no: Integer
  @name: String
  @scores: Array[untyped]
  def initialize: (no: Integer, name: String) -> untyped
  def add: (score: untyped) -> untyped                                                                                      
end

class Analyzer
  def self.load: (path: String) -> Array[User]
  def self.parse_row: (row: CSV::Row) -> User
end

これで実行すると、、、

$ bundle exec steep check --log-level=fatal
sig/sample.rbs:11:28...11:36	UnknownTypeNameError: name=::CSV::Row

CSVクラスは、標準添付ライブラリのため、requireして使っているので、Steepfile のlibrary で追加します。

target :app do
  signature "sig"

  check "bin"
  check "lib"

  library "csv"                                                                                                             
end
そして実行すると、、、

$ bundle exec steep check --log-level=fatal
lib/analyzer.rb:9:30: IncompatibleAssignment: lhs_type=::CSV::Row, rhs_type=::Array[(::String | nil)] (row)
  ::Array[(::String | nil)] <: ::basicobject="" ::csv::row="=" ::object=""> ::BasicObject <: ::csv::row="" code="" does="" hold="" not="">
やっとそれっぽいエラーが出てきました。

エラーメッセージに、lhs_type, rhs_typeが出ていますが、lhs_type がrbsで定義している型、rhs_typeが想定している型が表示されています。

CSV.foreach() のブロック引数のrow は、実行すると、CSV::Rowのインスタンスとなり、parse_row()のパラメータに設定されるのですが、rbsのCSV.foreach() では、Array[String?] で定義されているようです。

rbsに合わせて、Analyzer.parse_row() の引数をArray[String?] とすると、今度はrow.headers でエラーになってしまう…

悩ましい…


そこで、CSVは、rbsを参考に使用しているメソッドの以下の定義を sig/sample.rbsに追加することにしました。(Steepfileのlibrary定義はコメントアウトしました)

class CSV < Object
  def self.foreach: [U] (String, ?::Hash[Symbol, U] options) { (CSV::Row arg0) -> void } -> void
end

class CSV::Row < Object
  alias [] field                                                                                                                                                               
end
実行してみると、、、
エラーがなくなり、Steepでの型解析は正常となったようです。

使ってみての感想

Rubyのクラスについては、rbsの定義を参照しているので、rbsに定義されていない型だとエラーになってしまうようです。
そのため、普段書き慣れている記述でエラーになるケースもあり、この小さなプログラムでも確認したりするのに、結構時間を使いました。

使ってみての感想は、「やっぱり型書くのは大変だな…」という印象が強いのですが、
IDEを使ってコードを書く時に型情報までSuggest表示されれば、
Rubyの経験があまりない人でも書きやすかったり、大人数のプロジェクトなどでは、プログラムの記述が揃うし、確かにバグは少なくなるような気がします。

でも、やっぱりRubyは自由に書きたい…。

2020-12-18

Steep by Steep

 この記事は、ケーシーエスキャロット Advent Calendar 2020 17 日目の記事です。(ちょっと過ぎちゃったけど)

Ruby 3の静的解析機能の1つ。steep を使ってみようと思います。

 (styleが消えてコードが見にくい…)

リポジトリは、こちら

タイトルの Steep by Steep は、中学の頃流行った New Kids On The Block の "Step By Step" にかけてみました。

 


インストール

Rubyは、3.0.0-preview2を使っています。

steepのinstallをします。gem でinstallします。



$ gem install steep
Successfully installed steep-0.38.0
Parsing documentation for steep-0.38.0
Installing ri documentation for steep-0.38.0
Done installing documentation for steep after 2 seconds
1 gem installed
サンプルで使うコードです。 色々な型を試してみたかったので、設計面は無視したコードになっています。

lib/user.rb


class User                                                                                                                                                                             
  def initialize(no, name)
    @no = no
    @name = name
    @scores = Array.new
  end
  attr_reader :no, :name, :scores

  def add(score)
    @scores << score
  end
end

lib/analyzer.rb


require 'csv'                                                                                                                                                                          
  
class Analyzer
  @@score = Struct.new("Score", :subject, :score)

  def self.load(path)
    users = Array.new
    CSV.foreach(path, headers: true) do |row|
      users << parse_row(row)
    end

    users
  end

  def self.parse_row(row)
    u = User.new(row["No"], row["Name"])

    row.headers[2..-1].each do |subject|
      u.add @@score.new(subject.downcase, row[subject].to_i)
    end

    u
  end
end

bin/main.rb


LOAD_PATH << File.join(File.dirname(File.expand_path(__FILE__)), "../lib")                                                                                                            
  
require 'user'
require 'analyzer'

# 使用するデータ
data = <<-EOS
No,Name,English,History,Science
1,Akina,85,88,87
2,Bob,78,95,85
3,Candy,83,80,92
4,David,85,83,88
5,Emily,78,93,85
6,Fumiya,80,82,89
7,George,92,88,79 
EOS

それでは、実際に使ってみます



$ steep init
と入力すると、Steepfile が作成されるので、ここに定義をしていきます。

Steepfile


target :app do
  signature "sig"

  check "bin"
  check "lib"
end
これだけ定義して、まず実行してみます。

$ bundle exec steep check --log-level=fatal
bin/main.rb:20:2: NoMethodError: type=singleton(::File), method=open (File.open(fpath, "w") { |fd| fd.write data })
lib/user.rb:7:2: NoMethodError: type=self, method=attr_reader (attr_reader :no, :name, :scores)
エラーが出ました。
main.rbはおいておいて、Userクラスで、attr_reader に定義をしていないことを指摘しているようなので、定義を追加してみます。
定義ファイルは、sigフォルダの中に `.rbs`ファイルで書くとあるので、 sig/sample.rbs を作成して以下のように定義してみました。

class User                                                                                                                                                     
  @no: Integer
  @name: String
  @scores: Array[Score]
end
もう1度実行してみると、

sig/user.rbs:4:17...4:22        UnknownTypeNameError: name=::Score
エラーが変わりました。
ScoreというTypeがわからないというエラーが出ました。 

これは、Analyzerクラスの中でStructで定義しているからかな… ひとまず、先に進めるために `untyped` として、もう1回実行してみると、次のエラーに変わりました。

lib/user.rb:2:2: MethodArityMismatch: method=initialize (def initialize(no, name))
initializeの定義をしていないので、次はメソッドの定義をしていきます。


こんな感じかな(とりあえずuntyped)

class User
  @no: Integer
  @name: String
  @scores: Array[untyped]
  def initialize: (no: untyped, name: untyped) -> untyped                                                           
  def add: (score: untyped) -> untyped
end
実行してみると、、、

lib/user.rb:2:2: MethodArityMismatch: method=initialize (def initialize(no, name))
lib/user.rb:9:2: MethodArityMismatch: method=add (def add(score))
エラーになってしまいました… 


Analyerクラスが定義されていないことによる影響かな…と頭をよぎったので、Analayzerクラスも定義してみます。

class User
  @no: Integer
  @name: String
  @scores: Array[untyped]
  def initialize: (no: untyped, name: untyped) -> untyped
  def add: (score: untyped) -> untyped
end

class Analyzer
  def self.load: (path: untyped) -> Array[User]
  def self.parse_row: (row: untyped) -> User
end
実行してみると、、、

$ bundle exec steep check --log-level=fatal
bin/main.rb:20:2: NoMethodError: type=singleton(::File), method=open (File.open(fpath, "w") {|fd| fd.write data })
bin/main.rb:22:24: ArgumentTypeMismatch: receiver=singleton(::Analyzer), expected={ :path => untyped }, actual=::String (fpath)
lib/analyzer.rb:6:2: MethodArityMismatch: method=(self) (def self.load(path))
lib/analyzer.rb:15:2: MethodArityMismatch: method=(self) (def self.parse_row(row))
lib/analyzer.rb:16:8: IncompatibleArguments: receiver=singleton(::User), method_type=(name: untyped, no: untyped) -> ::User (User.new(row["No"], row["Name"]))
lib/user.rb:2:2: MethodArityMismatch: method=initialize (def initialize(no, name))
lib/user.rb:9:2: MethodArityMismatch: method=add (def add(score))
エラー増えた… 


リポジトリ内の sig/project.rbi も確認してみたのですが、定義の仕方がおかしいわけではなさそうです… 

もう少し中身を確認した方がよさそうなので、日を改めて取り組んでいきたいと思います。

2020-12-13

Rails Girls Gathering JapanのKeynoteで話しました

この記事は、Rails Girls Japan Advent Calendar 2020 の13日目の記事です。

昨日は、Rails Girls Gathering Japan でした。

Rails Girls  Gathering Japanとは

新型コロナウィルス感染症の影響で、2020年のRails Girls イベントは、2月以降開催することが難しい状況となっている中で、オンラインで何かできることを…ということで、これまでRails Girls に参加されたGirlsの皆さんや、コーチ、スポンサー、オーガナイザーとスタッフの方々によるLT大会です。


昨日は、裏で忙しくしていたこともあり、改めてハッシュタグ #rg_gathering_jp を追いましたが、皆さんに楽しんで頂けたようで、とても嬉しく思います。
正直こんなにたくさんの方に参加して頂けるとは思っていませんでした。
改めて、Rails Girlsというコミュニティは、みんなに愛されているんだなぁと感じました。


このイベントの企画をした経緯などは、スタッフのAdvent Calendarの記事で話してもらえるかもしれないので、ここでは省略します。


今回、初めてキーノートで話す機会を頂き、「As You Like It」というタイトルでお話ししたので、その話をしたいと思います。

As You Like It

タイトルにした、"As You Like It" という言葉は、今でも仲良くしている大学時代の先生から卒業前最後の授業で贈られた言葉です。
キーノートで何を話そうかと考えた時に、これから社会へ出ていく私達に先生が贈ってくれたこの言葉を伝えたいと頭に浮かびました。


今年は、新型コロナ感染症の影響で外出なども制限され、会いたい人にも会えない期間が長く続き、自分でも気づかぬうちにストレスが溜まっていたりします。
私自身もお家時間を楽しんではいますが、今度いつ仲間と会えるんだろう…と考えると、今の先が見えない状況では、やはりネガティブな気持ちになってしまうことがあります。

SNSで発信される誰かのアウトプットが、自分はできていないというプレッシャーに感じてしまったり、「今日も私は何もしなかった」という罪悪感のような感情を感じてしまうこともありますが、だからこそ、「As You Like It」(お好きにどうぞ)という言葉を思い出し、
「私の人生だから、私らしく、私の好きなように」と思うようにしています。

「強い」とかに縛られず「自分らしく、好きなように、楽しんでもらいたい」という気持ちをうまく伝えられていたらと思います。

私にとってのRails Girls 

参加する人が私からどんな話を聞きたいのか、想像がつかなかったので、Asakusa.rbで @neko314 に聞いてみたところ、「Rails Girls のサポートをやっていくモチベーションがどこにあるのかを聞いてみたい」と言われました。
当日、@cobachieにも同じく聞いてみたら、同じ返答でした。(みんな不思議に思ってるんですね…)

それで、昔昔の私がRubyを書き始めた頃の話などをしました。
あの頃の私のように、今もどこか倉庫の片隅で、一人でRubyを書いてる人がいるかもしれない…と思うことが、モチベーションにもなっている部分もあります。

Rails Girlsを通して私が伝えていきたいこと 

Rubyコミュニティの素晴らしさは、たくさんありますが、
Rails Girls などのRuby歴が浅い人には、
案外世界は狭い(一歩勇気を出せば、超えていける)ということを
是非知ってもらいたいと思っているので、そのお話をしました。

(キーノートをお願いした松田さんに交番までついて行ってもらったり、角谷さんに
財布拾ってもらったりしました…ありがとうございます!)

最後に 

初キーノート、うまく伝えたいことが伝えられたかどうかわからないけれど、
Rails Girls Gathering Japanは、楽しんでもらえたようで、本当によかったです。

LTに登壇して頂いた方々、ありがとうございました!
みんな素晴らしいLTでした!
久しぶりにお会いできた方もいて、とても嬉しかったです。