dely Tech Blog

クラシル・TRILLを運営するdely株式会社の開発ブログです

Ruby 3.0へ向けて、型周りをさわってみた

f:id:mochizuki_pg:20201126153958j:plain

はじめに


こんにちは! 

delyサーバーサイドエンジニアの望月 (@0000_pg)です
クラシルのアプリを中心にサーバーサイドを担当しています

今年もdelyのアドベントカレンダーが始まりました 🎉


adventar.org

adventar.org

今年は開発部の人数も増えてきたので
カレンダーを1と2にわけて行うことになりました

去年は2日目だったので
今年はトップバッターをやることにしました💪


本日公開された dely #2 Advent Calendar 2020のほうの記事は
デザイナーのsakoさんの
ノンデザイナーでも大丈夫!見やすいプレゼン資料をつくる6つの手順 です!
note.com

これをみれば、誰でもイケてる資料がつくれるようになっています😎✨
とても勉強になりました!


さて、dely #1 Advent Calendar 2020 1日目の記事は
Ruby 3.0へ向けて、型周りをさわってみた ことを書きたいと思います

Ruby 3.0


Ruby 3.0.0 Preview 1 に関しては現時点で触れるようになっています

f:id:mochizuki_pg:20201122134259p:plain

Ruby 3.0 はいまから触るRBSや、並列処理のRactorなど
多数の新機能が追加される予定です

型周りの機能は、すでにgemとして配布されているので
必ずしも3.0を使う必要はありません

準備

レシピ決め

一番重要なことは、今日作るレシピを決めることです

クリスマスが近づいてくるので クリスマスっぽいレシピにしましょう🎄

いちごで作る サンタクロース🎅

f:id:mochizuki_pg:20201122134122p:plain:w300

www.kurashiru.com

\ めちゃかわ💖 /
\ かわいいの暴力です💖 /

これにしましょう!!🎅🎄

コード

レシピが決まったので、買い物に行きつつ
コードをつくっていきます

Gemfile
source 'https://rubygems.org'

gem 'rbs'
gem 'typeprof'
gem 'steep'

(※あえてgemとして記載してあります)

recipe.rb
class Recipe
  attr_reader :title

  def initialize(title:, ingredients:, instructions:)
    @title = title
    @ingredients = ingredients
    @instructions = instructions
  end

  def ingredients
    Ingredient.summary(@ingredients)
  end

  def instructions
    Instruction.summary(@instructions)
  end
end


ingredient.rb
class Ingredient
  attr_reader :name, :quantity, :unit

  def initialize(name:, quantity:, unit:)
    @name = name
    @quantity = quantity
    @unit = unit
  end

  class << self
    def summary(ingredients)
      ingredients_text = "材料\n"
      ingredients.each do |ingredient|
        ingredients_text << <<-INGREDIENTS_TEXT
          #{ingredient.name}  #{ingredient.quantity}#{ingredient.unit}
        INGREDIENTS_TEXT
      end
      ingredients_text
    end
  end
end


instruction.rb
class Instruction
  attr_reader :text

  def initialize(text:)
    @text = text
  end

  class << self
    def summary(instructions)
      instructions_text = "手順\n"
      instructions.each do |instruction|
        instructions_text << <<-INSTRUCTIONS_TEXT
          #{instruction.text}
        INSTRUCTIONS_TEXT
      end
      instructions_text
    end
  end
end


app.rb
require_relative 'recipe'
require_relative 'ingredient'
require_relative 'instruction'


title = 'いちごで作る サンタクロース'

ingredients = [
  Ingredient.new(name: 'いちご', quantity: 2, unit: ''),
  Ingredient.new(name: 'ホイップクリーム', quantity: 10, unit: 'g'),
  Ingredient.new(name: 'チョコレートペン (黒)', quantity: 1, unit: '')
]

instructions = [
  Instruction.new(text: 'チョコレートペンは湯煎にかけて溶かしておきます。 ホイップクリームは絞り袋に入れておきます。'),
  Instruction.new(text: 'いちごはヘタを切り落とします。'),
  Instruction.new(text: 'ヘタの部分から2/3のところを切ります。'),
  Instruction.new(text: 'ヘタの部分を下にして切り口にホイップクリームを絞り、挟みます。上にホイップクリームを直径5mm程絞り、帽子をつくります。'),
  Instruction.new(text: 'チョコレートペンで顔とボタンを描いて完成です。')
]

recipe = Recipe.new(title: title, ingredients: ingredients, instructions: instructions)

puts recipe.title
puts recipe.ingredients
puts recipe.instructions


$ bundle exec ruby app.rb 

いちごで作る サンタクロース
材料
      いちご  2個
      ホイップクリーム  10g
      チョコレートペン (黒)  1本
手順
      チョコレートペンは湯煎にかけて溶かしておきます。 ホイップクリームは絞り袋に入れておきます。
      いちごはヘタを切り落とします。
      ヘタの部分から2/3のところを切ります。
      ヘタの部分を下にして切り口にホイップクリームを絞り、挟みます。上にホイップクリームを直径5mm程絞り、帽子をつくります。
      チョコレートペンで顔とボタンを描いて完成です。

本題

我々はいちごでサンタさんをつくりながら
型と向き合っていかないといけません🎅🎄

rbs でも雛形はつくれるのですが

github.com

 $ rbs prototype rb recipe.rb 


今回は typeprof をつかっていきます

typeprof

github.com

雛形をつくります (色々とオプションはありますが割愛します)

$ typeprof lib/recipe.rb -o sig/recipe.rbs    
$ typeprof lib/ingredient.rb -o sig/ingredient.rbs    
$ typeprof lib/recipe.rb -o sig/recipe.rbs


sig/recipe.rbs
# Classes
class Recipe
  @ingredients : untyped
  @instructions : untyped
  attr_reader title : untyped
  def initialize : (title: untyped, ingredients: untyped, instructions: untyped) -> untyped
  def ingredients : -> untyped
  def instructions : -> untyped
end


sig/ingredient.rbs
# Classes
class Ingredient
  attr_reader name : untyped
  attr_reader quantity : untyped
  attr_reader unit : untyped
  def initialize : (name: untyped, quantity: untyped, unit: untyped) -> untyped
  def self.summary : (untyped) -> String
end
sig/instruction.rbs
# Classes
class Instruction
  attr_reader text : untyped
  def initialize : (text: untyped) -> untyped
  def self.summary : (untyped) -> String
end


余談ですが、rbirbs について
itoさんのブログに記載がありました
koic.hatenablog.com

steep

今回はsteep をつかって型チェックをしていきます

github.com

準備

Steepfile をつくります

 $ steep init


# target :lib do
#   signature "sig"
#
#   check "lib"                       # Directory name
#   check "Gemfile"                   # File name
#   check "app/models/**/*.rb"        # Glob
#   # ignore "lib/templates/*.rb"
#
#   # library "pathname", "set"       # Standard libraries
#   # library "strong_json"           # Gems
# end

# target :spec do
#   signature "sig", "sig-private"
#
#   check "spec"
#
#   # library "pathname", "set"       # Standard libraries
#   # library "rspec"
# end

今回はこうしました

target :lib do
  check "lib"
  signature "sig"
end

型定義

先程のrbs に型を定義していきます

sig/recipe.rbs
# Classes
class Recipe
  @ingredients : Array[Ingredient]
  @instructions : Array[Instruction]
  attr_reader title : String
  def initialize: (title: String, ingredients: Array[Ingredient], instructions: Array[Instruction]) -> void
  def ingredients: -> String
  def instructions: -> String
end


sig/ingredient.rbs
# Classes
class Ingredient
  attr_reader name : String
  attr_reader quantity : Integer
  attr_reader unit : String
  def initialize: (name: String, quantity: Integer, unit: String) -> void
  def self.summary: (Array[Ingredient]) -> String
end
sig/instruction.rbs
# Classes
class Instruction
  attr_reader text : String
  def initialize: (text: String) -> void
  def self.summary: (Array[Instruction]) -> String
end
sig/app.rbs
# Classes
class Recipe
  attr_reader title : String
  def initialize : (title: String, ingredients: Array[Ingredient], instructions: Array[Instruction]) -> void
  def ingredients : -> String
  def instructions : -> String
end

class Ingredient
  attr_reader name : String
  attr_reader quantity : Integer
  attr_reader unit : String
  def initialize : (name: String, quantity: Integer, unit: String) -> void
  def self.summary : (Array[Ingredient]) -> String
end

class Instruction
  attr_reader text : String
  def initialize : (text: String) -> String
  def self.summary : (Array[Instruction]) -> String
end

型チェック

$ bundle exec steep check

(色々とオプションはありますが割愛します)

失敗してみる

title の型をInteger にし、
recipe.instructions の戻り値を Integer にしてみます

sig/recipe.rbs
# Classes
class Recipe
  @ingredients: Array[Ingredient]
  @instructions: Array[Instruction]
+  attr_reader title: Integer
  def initialize: (title: String, ingredients: Array[Ingredient], instructions: Array[Instruction]) -> void
+  def instructions: -> Integer
end


lib/recipe.rb:5:4: IncompatibleAssignment: lhs_type=::Integer, rhs_type=::String (@title = title)
lib/recipe.rb:14:2: MethodBodyTypeMismatch: method=instructions, expected=::Integer, actual=::String (def instructions)

最後に

明日の dely #1 Advent Calendar 2020
GENさんの 木も見て森も見るための Athena(Presto) 集計術 です!
お楽しみに!

delyではRailsエンジニアを募集しています!

サーバーサイドのカジュアル面談は自分が担当しています!
少しでも興味があれば、お気軽にお話ししましょう〜

join-us.dely.jp

また、定期的に開発組織についてイベントを行っています!
こちらもカジュアルに話を聞きにきてもらえればと思います〜

bethesun.connpass.com