20201201のRubyに関する記事は30件です。

30代からのプログラミング勉強 6日目【Udemyコース/Ruby on Rails】

本日は比較的短めのコース視聴を実施。
分からないなりにはなりますが、コースレビューの様なものを書いていきます。

学習内容

【3日でできる】はじめての Ruby on Rails 4 入門(Rubyから学べる)

今回はこちらのUdemyコースを視聴。
率直な感想としては「自分にはまだ早かったけど見る価値はあった」です。

1から10まで丁寧に、というよりはwebアプリケーションが出来るまでの流れを簡単な解説を挟みつつ見ていくといった印象でした。

またRuby、Railsの導入についてはターミナルへの入力を真似していけばいいだけなので分かりやすいですが、動画内で解説されているバージョンは最新のものではないので注意が必要かもしれません。

説明は
「○○という記述には××という効果があり、例えばこんな時に使います」
ではなく
「××したいので○○と書きます」
というものが多く完全に知識がない状態だと聞き慣れない単語も多かったので、ある程度勉強した後に見るといいかもしれません。

所感

はじめての、とはありますが説明少なめでサクサク進んでいく感じの講座なので(フォーラムで質問しながらやる様説明はありますが)、個人的には超入門編を完了後にもう一度流れを見て復習する為に活用したいなと思いました。

ただ、Webアプリケーションがどんどん出来上がっていく様を見るのは面白かったので、あまり理解は出来なくともプログラミングってこんな感じで組み上げられていくんだなぁ〜って言うのをサラッと流し見していくのも最初の一歩としてはアリなのではないでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails初心者向け】ログインユーザーの最新投稿を取得する方法

本のレビューアプリ作成中、ログインユーザーの最新投稿を取得する方法が複数あったので、それぞれの簡単な使い方と違いについて備忘録としてまとめました。

【前提】

  • rails_6.0.0_
  • mysql2
  • gem 'devise'を使用してログイン機能を実装済み

【目的】

  • current_userの最新投稿を取得すること
  • ビューで最終投稿日時を表示すること

【結論】 : current_userに紐づく投稿を配列取得し、「.last」で取り出す

contoroller.rb
@last_review = Review.where(user_id: current_user.id).last

whereメソッドでcurrent_userに関連する投稿を取得して、lastメソッドで最後の投稿のみを切り出します。

html.erb
<%= @last_review.created_at %>

コントローラーで定義したインスタンス変数@last_reviewに対して、created_atカラムの情報を表示させます。

正直、自分が考えうる方法ではこの方法が最も記述が少なく、処理も早いので、今後これ以外使うことはなさそうです。
それでは、時系列順にこの結論に至った経緯を記載します。(1〜3まで)

1 : 配列のまま取得して、eachで1周だけ回す

はじめに思いついたのがこの方法です。

controller.rb
@last_reviews = Review.where(user_id: current_user.id).order(created_at: :desc).limit(1)

whereメソッドで投稿者がcurrent_userである投稿を配列で取得
.order(created_at: :desc)の部分で降順(新しい順)に並べ替え
.limit(1)で配列の先頭1つに限定
という流れです。

html.erb
<% @last_reviews.each do |last-review| %>
 <%= last_review.created_at %>
<% end %>

取得した配列をeachで一覧表示します。今回は中身が一つだけなので、一回の繰り返しです。

.limit(1)で最新の投稿を取得するこの方法だと、配列ごと取得する形になるので、ビューファイルでeach文を使う必要があります。
最新の投稿5件を表示させたい時などはこの方法を使う事になりそうですが、今回は1件だけなので正直、長い&読みにくいと感じました。

2 : .sliceを使用して、配列から投稿を1つだけ切り取る

controller.rb
@last_review = Review.where(user_id: current_user.id).order(created_at: :desc).slice(0)

先ほどの.limit(1)の部分を.slice(0)に変更して、配列から先頭の1要素を切り出します。
配列ではなく最新の投稿情報のみをインスタンス変数に定義しており、これならばeach文を用いる事なく投稿情報をビューファイルに表示する事ができます。

html.erb
<%= @last_review.created_at %>

先ほどよりはスッキリしましたが、「引数に、切り出す配列の要素の番号や範囲を指定する」というsliceの性質上、配列の中身を降順に並べ替える必要があります。

3 : .lastを使用して、配列の末尾の要素を取り出す

controller.rb
@last_review = Review.where(user_id: current_user.id).last 

.lastを用いる事で配列の末尾の要素のみを取り出す事ができます。
.slice(0)との違いとして、要素の範囲指定をしないので降順に並べ替えるが必要がなく、より短い&読みやすい記述になります。
(先頭の要素を取り出したい場合は.firstを使用します)

html.erb
<%= @last_review.created_at %>

ビューファイルは先ほどと同じです。

【まとめ】

ログインユーザーの最新投稿を取得して表示させる方法を3つご紹介しました。
1. 配列のまま取得して、eachで1周だけ回す
2. sliceを使用して、配列から投稿を1つだけ切り取る
3. lastを使用して、配列の末尾の要素を取り出す

今回は最新投稿の1件以外は不要、且つ、並べ替えの記述を省くために、最後に紹介した.lastを採用しました。(私が思いつかなかっただけで他にも方法は様々あると思います... )

【最後に】

個人的な感想ですが、いろいろなやり方がある中でも自分なりに最適な方法を選ぶ過程はプログラミングの楽しみの一つだなと実感しました。今後も、何か発見できたらその都度アウトプットしていきたいと思っています。

ここまでお付き合いいただき、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

配列に対してメソッドを使う時気を付けること(RubyonRails)

私のメモ用ですが、どなたかのヒントになれば幸いです

概要

配列に対して処理を使う際、ひとつひとつに対して処理を適用しなければならない。
結論はmapメソッドを使えば配列それぞれに処理を適用できます。

実例

下記の例では配列Arrayに対してメソッドの確認を行おうとしておりますが、当然これでは確認はできません。

if Array.respond_to?(:content_type)
# => false

mapメソッドを使えば、配列のひとつひとつに対して処理を行えます。

if Array.map {|a| a.respond_to?(:content_type)}
# => true(Arrayにcontent_typeメソッドがあれば)

まとめ

結論、mapメソッドの使い方の話になりますが、これに気付かず一日ハマってしまいました。
扱っているものが単体なのか配列なのかをしっかり意識すべきだと思いました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails video_tagで動画の埋め込み

Ruby on Railsで動画を埋め込みたいと思い、video_tagを使ったので復習

まず動画を置く場所はpublic/videos/動画ファイル

ソースコード

<%= video_tag("/videos/record.mp4", autoplay: :true, loop: :true, muted: :true, class: 'video-content') %>

これでうまく読み込めました。

video_tagのよく使う属性

属性 意味
autoplay ページの読み込み後自動再生
loop 繰り返し再生
controls コントローラーを表示
muted 音を出すかどうか
size 動画サイズ(幅*高さ)

まとめ

最初はapp/assets/videos/動画ファイルとしてましたが読み込まれず404エラーになってしまいました。
調べてみると、video_tagはpublicフォルダを探しにいくらしいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsのパフォーマンス向上

はじめに

Railsのパフォーマンスの向上に関して、少しまとめます。

いろいろ、誤解や間違いがあるかもしれないので、問題ありましたら指摘していただけると助かります。

N+1(includes)

N+1は100件のデータを取得する場合に、101回SQLが走る様な場合の問題のことです

例えば、

userモデルとuser_profileモデルが一対一の場合に

@users = User.all

@users.each do |user|
  user.user_profile.comment
end

の様にすると

user_profile.commentの取得時に毎回SQLが走ってN+1が走ります。

このN+1の回避方法としては下記の様にincludesを使うことで回避できます。

@users = User.includes(:user_profile).all

@users.each do |user|
  user.user_profile.comment
end

N+1(select)

includesで基本的なN+1は回避することができるのですが、下記の様な場合N+1は回避することができません。

userモデルとarticleモデルが一対多の場合

@users = User.includes(:article).all

@users.each do |user|
  user.articles.count
end

みたいにするとincludesをしていても↓の様にsqlがN+1回分走ります。

   (0.2ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 1]]
   (0.1ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 2]]
   (0.1ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 3]]
   (0.1ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 4]]
   (0.1ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 5]]
   (0.1ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."user_id" = ?  [["user_id", 6]]

その場合は、自分はselectメソッドの中にサブクエリを記載します

@users = User.all.select('(SELECT COUNT(*) FROM articles WHERE articles.user_id = users.id) article_count')

@users.each do |user|
  user.article_count
end

この様にすることで、単純にincludesするだけては解決できない、N+1も解決できます。

exists

存在するかどうかのみを確認したい場合はpresent?よりexists?を使う方がパフォーマンスが良いです

発行されるSQLは下記の様に異なります。

User.all.exists?
=> User Exists? (0.2ms)  SELECT 1 AS one FROM "users" LIMIT ?
User.all.present?
=> User Load (0.4ms)  SELECT "users".* FROM "users"

present?の場合は、実際にusersのデータを全て取ってきてしまうのに対して、exists?は存在したら1を返すだけて、余計なデータを取ってくることはないので、早いです。

pluck

自分の場合データのidのみを取ってきたい場合にpluckを使う
pluckを使わず、 map(&:id)みたいにするのと比べると、pluckはSQLで必要なカラムのみ取ってくるのと、ActiveRecordのオブジェクトを作ることもしないので早い

実際に発行されるSQL↓

User.all.pluck(:id)
=> (0.3ms)  SELECT "users"."id" FROM "users"
User.all.map(&:id)
=> User Load (0.6ms)  SELECT "users".* FROM "users"

eachとfind_each

eachの場合は前件取ってきて、データをメモリに置くのに対して、
find_eachの場合は1000件ずつ持ってくる

件数が多い場合データをeachでループする場合はfind_eachを使う方が良いっぽい

終わりに

とりあえず、思いつくものを記載しました。
また、何か思いついたら追記していきます。

パフォーマンスに関してですが、基本的に

  • selectやpluckを使ってSQLで取ってくるカラムを制限する
  • N+1が起きない様にする

位で十分な気がしているんですが、どうなんでしょうか?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

駆け出し新卒エンジニアによるOSSコードリーディングのすすめ(Ruby編)

Increments × cyma (Ateam Inc.) Advent Calendar 2020 の2日目は、株式会社エイチーム EC事業本部の島袋(@shimabee)が担当します。

はじめに

駆け出し新卒エンジニアがRubyで書かれたOSSコードを読んで行く中で得られたことをまとめました。
OSSコードリーディングの導入としてOpenStructというRubyの標準ライブラリの一部を読むパートもありますので、是非とも最後までお読みください。

なぜ、OSSコードリーディングを始めたのか

OSSコードリーディングを始めた目的は、コードリーディングスキルの向上です。
コードリーディングスキルはプロのエンジニアには重要なスキルです。なぜなら、自分だけで好き勝手に開発していた学生の頃とは異なり、エンジニアとして働き始めてからは他人の書いたコードを読み解き、その上で実装を考えることが殆どだからです。
このコードリーディングスキルを上達させる方法をを上司や先輩に相談したところ、勧められたのがOSSのコードリーディングでした。

特に有名なOSSのソースコードは多くの優秀なエンジニアが関わっているのでコードの質も担保されていますし、何より無料で見放題です。同じようにコードリーディングスキル課題を抱えている方も是非試していただきたいです。

OSSコードリーディングで得たもの

OSSコードリーディングで得たものは大きく二つあります。
1. コードリーディングスキル
2. 仕組みを知りに行く習慣

コードリーディングによって、数多くのRubyのメソッドとその使用ケースを見て知ることができ、徐々にではありますが、今までよりも正確に速くコードを読むことができるようになっています。
ただ、コードリーディングのスキルはいきなり上がるものではないので、地道に続ける必要があるということも実感済みです。

仕組みを知りに行く習慣というのは、ライブラリやフレームワークを使用する際に使い方だけを知りに行くのではなく、その実装を確認しにいく習慣のことです。
仕組みを知るとそのライブラリやフレームワークをより適切に使用することができます。
それだけでなく、頻繁に実装を確認しに行くキッカケにもなるので知識のインプットの習慣化にも繋がっています。

実際に読んでみよう!

実際にやってみて体験した方が分かると思うので、ここからは実際にコードを読んでいこうと思います。
今回読んでいくのは、Rubyの標準ライブラリであるOpenStructです。

なぜ、OpenStructなのか

私がOSSコードリーディングで、2番目に読んだのがこのOpenStructです。
その際にOpenStructを選んだ理由は二つで、実装がシンプルなこととRubyの標準ライブラリであることです。
シンプルなものを選んだ理由としては、駆け出しの私でも構えることなく、気軽にコードリーディングができると考えたからです。
標準ライブラリである理由は、コードの質が良いからです。Rubyの標準ライブラリと言うことはRubyを作っている方々が書いたコードと言うことになるので、参考にするにはもってこいかと思います。

OpenStructとは

さあ、では早速読んでいきましょうと言いたいところですが、
コードリーディングをする前に、OpenStructがどのようなものかについて見ていきます。
機能や使い方としっかりと抑えてから、仕組みを知りに行くことが大切です。

概要

要素を動的に追加・削除できる手軽な構造体を提供するクラスです。
OpenStruct のインスタンスに対して未定義なメソッド x= を呼ぶと、 OpenStruct クラスの BasicObject#method_missing で捕捉され、そのインスタンスにインスタンスメソッド x, x= が定義されます。この挙動によって要素を動的に変更できる構造体として働きます。

Ruby 2.7.0 リファレンスマニュアルより

使い方

インスタンス作成後に、任意のインスタンスメソッドを動的に定義できます。

person = OpenStruct.new
person.name = "John Smith"
person.age  = 70

person.name      # => "John Smith"
person.age       # => 70
person.address   # => nil

初期値にハッシュを渡すこともできます。

person = OpenStruct.new({ name: "John Smith", age: 70 })

person.name      # => "John Smith"
person.age       # => 70

コードリーディング

使い方が分かったところで、コードリーディングに移っていきます。

リファレンスマニュアルに大まかな仕組みが書いてありますね!

OpenStruct のインスタンスに対して未定義なメソッド x= を呼ぶと、 OpenStruct クラスの BasicObject#method_missing で捕捉され、そのインスタンスにインスタンスメソッド x, x= が定義されます。

上記をもとに実装を追っていきましょう!
今回見ていくソースは、コチラ です。

未定義なメソッドの捕捉

未定義なメソッドの捕捉には、BasicObject#method_missingを利用します。
method_missingは、呼びだされたメソッドが定義されていなかった時、Rubyインタプリタに呼び出されるメソッドであるため、これをオーバーライドすることにより、未定義なメソッドが呼び出された際に捕捉することができます。
OpenStructではこの仕組みを利用し、method_missingをオーバーライドする中でset_ostruct_member_value!を呼び、未定義なメソッドを定義していくわけです。

private def method_missing(mid, *args) # :nodoc:
  len = args.length
  if mname = mid[/.*(?==\z)/m]
    if len != 1
      raise! ArgumentError, "wrong number of arguments (given #{len}, expected 1)", caller(1)
    end
    set_ostruct_member_value!(mname, args[0])
  elsif len == 0
  else
    begin
      super
    rescue NoMethodError => err
      err.backtrace.shift
      raise!
    end
  end
end

捕捉した未定義メソッドを定義する。

では、set_ostruct_member_value!の中身も見ていきましょう。
set_ostruct_member_value!内では、method_missingで受け取ったメソッド名をシンボルに変換し、new_ostruct_memberへ渡しています。

def []=(name, value)
  name = name.to_sym
  new_ostruct_member!(name)
  @table[name] = value
end
alias_method :set_ostruct_member_value!, :[]=
private :set_ostruct_member_value!

new_ostruct_member!内で読み込み、書き込みのメソッドをdefine_singleton_methodを使い、特異メソッドとして定義します。

def new_ostruct_member!(name) # :nodoc:
  unless @table.key?(name) || is_method_protected!(name)
    define_singleton_method!(name) { @table[name] }
    define_singleton_method!("#{name}=") {|x| @table[name] = x}
  end
end

未定義なメソッドを動的に定義する仕組みが見えましたね!
思ったよりも読む部分が少なくて、分かり易かったのではないでしょうか?

まとめ

OpenStructの実装の一部を見ていきましたが、いかがでしたでしょうか?
全然難しいことはなかったと思います。それにmethod_missingをオーバーライドする辺りはRubyの面白さが見えるところではないかとも思います。
この記事をキッカケに様々なOSSを読んで、仕組みを知りに行く面白さやRubyという言語の面白さを体感してみてください。

次のステップ

次のステップとしては、gemやフレームワークの一部の機能について見ていくと良いかもしれません。
個人的なおすすめとしては、settingslogicやRailsのstrong_parametersです。
どちらもコード量は少ないですし、仕組みも分かりやすい部類に入るかと思います。是非お試しください。

最後まで読んでいただきありがとうございました。
ご意見、ご感想があればコメントいただきたいです。

以上、駆け出し新卒エンジニアによるOSSコードリーディングのすすめ(Ruby編)でした。

Increments × cyma (Ateam Inc.) Advent Calendar 2020 の3日目は、Increments株式会社の @degudegu2510 がお送りします!お楽しみに!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プッシュ時にRSpec+Rubocopを自動でするようにCircleCIでbuildする【Rails】

はじめに

Railsでアプリを作っている初学者です。
アプリ内にCircleCIを使った自動テストを入れたい!と思い、導入してみましたので、その時に詰まったことをまとめました。

同じようにアプリにCircleCIを入れたいなと思っている方の参考になれば嬉しいです!!

また、間違い等がありましたらご指摘いただけますと幸いです。

バージョン

Ruby:2.6.6
Rails:5.2.4
Mysql:5.7
環境構築にDocker使用

前提

以下は題名のことをしたい場合&CircleCIを理解する上で必要です!

・Dockerを使用したことがある
・Rspecを導入済
・Rubocopを導入済
・Githubアカウントがある

まず、CircleCIの公式入門ガイドをすることをおすすめします!自分は実際の挙動があまりイメージ出来なかったのですが、これでざっくり理解することができました。

CircleCI入門ガイド:https://circleci.com/docs/ja/2.0/getting-started/

そして入門ガイドをしたところで、CircleCIをアプリに加えてみます。
ルートディレクトリに .circleci フォルダを作成してコンフィグを作る。
そしてデータベースを作る、とできるはず!

うまくいかなかったパターン

参考:CircleCIでSystemSpec(RSpec)とRubocopを走らせる
https://qiita.com/YK0214/items/bbed63ea7ca5367dae2f

まず、こちらのQiitaの記事を参考にしましたが、自分の環境ではうまくいきませんでした。
ですが、こちらの経緯も記録しておきます。

config.ymlを作る

.circleci\config.yml
version: 2.1

orbs:
  ruby: circleci/ruby@1.1.0

jobs:
  build:
    docker:
      - image: circleci/ruby:2.6.6-node-browsers
        environment:
          BUNDLER_VERSION: 2.1.4
    steps:
      - checkout
      - ruby/install-deps

 test:
    parallelism: 3
    docker:
      - image: circleci/ruby:2.5.1-node-browsers
        environment:
          DB_HOST: 127.0.0.1
          RAILS_ENV: test
          BUNDLER_VERSION: 2.1.4
      - image: circleci/mysql:5.7
        command: --default-authentication-plugin=mysql_native_password
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
          MYSQL_ROOT_HOST: '%'
    steps:
      - checkout
      - ruby/install-deps
      - run: mv config/database.yml.ci config/database.yml
      - run:
          name: Wait for DB
          command: dockerize -wait tcp://localhost:3306 -timeout 1m
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load
      # Run rspec in parallel
      - ruby/rspec-test
      - ruby/rubocop-check

workflows:
  version: 2
  build_and_test:
    jobs:
      - build
      - test:
          requires:
            - build

引用:CircleCIでSystemSpec(RSpec)とRubocopを走らせる
https://qiita.com/YK0214/items/bbed63ea7ca5367dae2f

database.yml.ciを作る

すでにconfig内にdatabase.ymlがあるけど、それとは別で新規で作ります。
新規のDockerfileを使うのでデータベースも新しくなります。

config\database.yml.ci
test:
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: 'root'
  port: 3306
  host: '127.0.0.1'
  database: ci_test

引用:CircleCIでSystemSpec(RSpec)とRubocopを走らせる
https://qiita.com/YK0214/items/bbed63ea7ca5367dae2f

Githubへプッシュして、マージするとCircleCIで自動でBuildされます
失敗している!!!

Qiita用画像CircleCI1.png

Bundleのバージョンが違うみたい。

ちょっと変更

.circleci\config.yml
version: 2.1

orbs:
  ruby: circleci/ruby@1.1.0

jobs:
  build:
    docker:
      - image: circleci/ruby:2.6.6-node-browsers
        environment:
          BUNDLER_VERSION: 1.17.2
    steps:
      - checkout
      - ruby/install-deps

  test:
    parallelism: 3
    docker:
      - image: circleci/ruby:2.5.1-node-browsers
        environment:
          DB_HOST: 127.0.0.1
          RAILS_ENV: test
          BUNDLER_VERSION: 2.1.4
      - image: circleci/mysql:5.7
        command: --default-authentication-plugin=mysql_native_password
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
          MYSQL_ROOT_HOST: '%'
    steps:
      - checkout
      - ruby/install-deps
      - run: mv config/database.yml.ci config/database.yml
      - run:
          name: Wait for DB
          command: dockerize -wait tcp://localhost:3306 -timeout 1m
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load
      # Run rspec in parallel
      - ruby/rspec-test
      - ruby/rubocop-check

workflows:
  version: 2
  build_and_test:
    jobs:
      - build
      - test:
          requires:
            - build

BUNDLER_VERSION: 1.17.2に変更して再Push

Qiita用画像CircleCI3.png

これでできた!!!と思ってshow all checkをクリックして確認すると、
途中でbuildが止まっています...!

qiita circleCI1.png

よくよく見たらこんな記載が。

Qiita用画像CircleCI.png

これは有料枠にしろということですね!?
無料枠ではコンテナ1つでないと動かないのですが、このコードは2つのDockerを起動させようとしています。
なので途中で止まっていたんだなあということがわかりました...

というわけで修正を行います

うまくいったパターン

今度はこちらの記事を参考に、config.ymlを作成します。
エラーがいくつか出たので最終的には細かく修正を加えています。

参考:CicleCIでRspecとRubocop通すまでにつまずいたところとその解決法
https://qiita.com/naota7118/items/056770bcf53136e94788

.circleci/config.yml
# CircleCIのバージョンのことで、「2」、「2.0」、「2.1」のうちのどれかを指定します。
version: 2.1

orbs:
  ruby: circleci/ruby@1.1.0

# jobsは、実行処理の単位です。
jobs:
  build:
    # DockerでRubyのイメージを作成します。Dockerfileもしくはローカル環境とVersionは合わせること。
    docker:
      - image: circleci/ruby:2.6.6-node-browsers
        environment:
          # Dockerで作った、circleCI上で'bundle install'します
          BUNDLER_VERSION: 1.17.2
    steps:
      - checkout
      - ruby/install-deps

test:
    parallelism: 3
    docker:
      - image: circleci/ruby:2.5.1-node-browsers
        environment:
          DB_HOST: 127.0.0.1
          RAILS_ENV: test
          BUNDLER_VERSION: 2.1.4
      - image: circleci/mysql:5.7
        command: --default-authentication-plugin=mysql_native_password
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
          MYSQL_ROOT_HOST: '%'
    steps:
      - checkout
      - ruby/install-deps
      - run: mv config/database.yml.ci config/database.yml
      - run:
          name: Wait for DB
          command: dockerize -wait tcp://localhost:3306 -timeout 1m
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load
      # Run rspec in parallel
      - ruby/rspec-test
      - ruby/rubocop-check

workflows:
  version: 2
  build_and_test:
    jobs:
      - build
      - test:
          requires:
            - build

引用:CicleCIでRspecとRubocop通すまでにつまずいたところとその解決法
https://qiita.com/naota7118/items/056770bcf53136e94788

公式:CircleCI-Public/circleci-demo-ruby-rails
https://github.com/CircleCI-Public/circleci-demo-ruby-rails/blob/master/config/database.yml

参考:【CircleCI】Railsアプリに導入(設定ファイルについて)
https://qiita.com/Daiki-Abe/items/d90599d904c2b1370d10

というわけで修正

①bundle exec rake:db migrateを追加

マイグレーションされていないというエラーが出ているので、上記のファイルの中に
bundle exec rake:db migrateを追加します

②細かすぎるgemまで見ているのでRubocopを編集

migrateが無事されましたが、今度はRubocopでエラーがでました。
エラー文は以下。

For more information: https://docs.rubocop.org/rubocop/versioning.html 
vendor/bundle/ruby/2.6.0/gems/msgpack-1.3.3/.rubocop.yml: Lint/Eval has the wrong namespace - should be Security 
Error: RuboCop found unsupported Ruby version 2.3 in `TargetRubyVersion` parameter (in vendor/bundle/ruby/2.6.0/gems/msgpack-1.3.3/.rubocop.yml). 2.3-compatible analysis was dropped after version 0.81. 
Supported versions: 2.4, 2.5, 2.6, 2.7, 3.0

Rubocop.ymlでrubyバージョンの指定をすることで解決しました

AllCops:
  # Rubyバージョン指定
  TargetRubyVersion: 2.6

参考:原因と対応「Error: RuboCop found unsupported Ruby version 2.1」
https://blog.mothule.com/ruby/rubocop/ruby-rubocop-found-unsupported-ruby-version

buildには失敗してるけどRubocopは通っています!

次はRSpecですがここでもエラーが出ています。

③RSpecのmysql2::Error::ConnectionError:を解消する

②のエラーを直したあとにrspecを実行すると、エラーがでるようになりました
rspecの実行結果は以下です。

PS C:\Users\> rspec
An error occurred while loading ./spec/features/toppage_spec.rb.
Failure/Error: ActiveRecord::Migration.maintain_test_schema!

Mysql2::Error::ConnectionError:
  Unknown MySQL server host 'db' (0)
# ./spec/rails_helper.rb:28:in `<top (required)>'
# ./spec/features/toppage_spec.rb:1:in `<top (required)>'

Top 0 slowest examples (0 seconds, 0.0% of total time):

Finished in 0.00009 seconds (files took 3 minutes 27 seconds to load)

0 examples, 0 failures, 1 error occurred outside of examples

databese.ymlに以下を加えることで解決しました。

databese.yml
test:
  <<: *default
  database: Music-record_test
  host: <%= ENV['MYSQL_ROOT_HOST'] || '127.0.0.1' %>

参考:ローカル環境のCircleCIでMysql2::Error::ConnectionError: Unknown MySQL server host 'db' (-2)が出る
https://qiita.com/mitsu1208g/questions/c9681d1204a1b878f987

④RSpecのエラー LoadError: cannot load such file -- rspec_junit_formatterの解消

次はCircleCIでのRSpecでエラーがでました。

Error reading historical timing data: file does not exist
Requested weighting by historical based timing, but they are not present. Falling back to weighting by name.
No examples found.
bundler: failed to load command: rspec (/home/circleci/repo/vendor/bundle/ruby/2.6.0/bin/rspec)
LoadError: cannot load such file -- rspec_junit_formatter
  /home/circleci/repo/vendor/bundle/ruby/2.6.0/gems/rspec-core-3.9.3/lib/rspec/core/formatters.rb:235:in `require'
  /home/circleci/repo/vendor/bundle/ruby/2.6.0/gems/rspec-core-
・
・
・

  /home/circleci/repo/vendor/bundle/ruby/2.6.0/gems/rspec-core-3.9.3/lib/rspec/core/runner.rb:45:in `invoke'
  /home/circleci/repo/vendor/bundle/ruby/2.6.0/gems/rspec-core-3.9.3/exe/rspec:4:in `<top (required)>'
  /home/circleci/repo/vendor/bundle/ruby/2.6.0/bin/rspec:23:in `load'
  /home/circleci/repo/vendor/bundle/ruby/2.6.0/bin/rspec:23:in `<top (required)>'

Exited with code exit status 1
CircleCI received exit code 1

Uploading test results
0s

Uploading artifacts
0s

LoadError: cannot load such file -- rspec_junit_formatter
この1文がエラーの原因のようです。

rspec_junit_formatterがないということで以下の記事を参考にGemを追加します。

参考:CI/CD環境を作った時に起きたエラー備忘録[Rails + CircleCI +Capistrano +AWS]
https://qiita.com/orangeupup90027/items/6f83850d1842adf3c09a

① rspec_junit_formatterがないと言われる
テストを走らせた時に出たエラー

Error reading historical timing data: file does not exist
Requested weighting by historical based timing, but they are not present. Falling back to >weighting by name.
No examples found.
bundler: failed to load command: rspec (/home/circleci/circleci-demo-ruby->rails/vendor/bundle/ruby/2.5.0/bin/rspec)
LoadError: cannot load such file -- rspec_junit_formatter

結論としてはrspec_junit_formatterをgemfileに追加する。

Gemfileにrspec_junit_formatterを追加します。

Gemfile
group :test do
  # Adds support for Capybara system testing and selenium driver
  gem 'capybara', '>= 2.15'
  gem 'selenium-webdriver'
  # Easy installation and use of chromedriver to run system tests with Chrome
  # gem 'chromedriver-helper'
  gem 'rspec_junit_formatter' #追加
  gem 'webdrivers', '~> 3.0'
end

これでGithubにプッシュするとCircleCIの画面はこのようになりました。

CircleCIde.png

長くなりましたが、これでCircleCIでBuildができました!

最終的なコード

細かな修正を入れた結果が以下です。

※CircleCIはインデントに厳しい&文字列のブロックの別れ方がシビアなので、実際にコードをコピー・記載するときはコメントなしの方が良いです。

circleci\config.yml
# CircleCIのバージョンのことで、「2」、「2.0」、「2.1」のうちのどれかを指定します。
version: 2

jobs:
  build:
    docker:
      - image: circleci/ruby:2.6.6-node-browsers
        environment:
          - RAILS_ENV: 'test'
          - MYSQL_HOST: 127.0.0.1
          - MYSQL_USERNAME: 'root'
          - MYSQL_PASSWORD: ''
          - MYSQL_PORT: 3306
      - image: circleci/mysql:5.7
        environment:
          - MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
          - MYSQL_ROOT_HOST: '%'
    working_directory: ~/repo
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-
      - run:
          name: install dependencies
          command: |
            bundle install --jobs=4 --retry=3 --path vendor/bundle
      - save_cache:
          paths:
          - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
      - run: mv config/database.yml.ci config/database.yml
      - run: bundle exec rake db:create
      - run: bundle exec rake db:migrate
      - run: bundle exec rake db:schema:load
      - run:
          name: RuboCop
          command: bundle exec rubocop
      - run:
          name: RSpec
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
              circleci tests split --split-by=timings)"
            bundle exec rspec \
              --format progress \
              --format RspecJunitFormatter \
              --out /tmp/test-results/rspec.xml \
              --format progress \
              $TEST_FILES

      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results

config\database.yml.ci
test: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: <%= ENV.fetch("MYSQL_USERNAME") %>
  password: <%= ENV.fetch("MYSQL_PASSWORD") %>
  host: <%= ENV.fetch("MYSQL_HOST") %>
  port: <%= ENV.fetch("MYSQL_PORT") %>
  database: ci_test

ここまでお付き合いいただきありがとうございます!

参考

【circleCI】Railsアプリでgithubと連携してrubocopとrspecテストを走らせる
https://qiita.com/AK4747471/items/b2161784065f21cd1645

CicleCIでRspecとRubocop通すまでにつまずいたところとその解決法
https://qiita.com/naota7118/items/056770bcf53136e94788

【CircleCI】Railsアプリに導入(設定ファイルについて)
https://qiita.com/Daiki-Abe/items/d90599d904c2b1370d10#databaseymlci%E3%82%92%E4%BD%9C%E6%88%90

【初心者向け】deviseのユーザー登録をRSpecでテストする
https://qiita.com/HiMinmeg/items/37a59cd266c63330797a#%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B32!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuでデプロイしたときにapplication errorの対処法

初投稿です。
間違いがあるかもしれませんがそのときにはご指摘ください。

概要

Herokuでデプロイしたあとに本番環境でapplication errorが発生。
ログを見るように書いてあるのでログを確認してみると

heroku[router]: at=error code=H10 desc="App crashed" method=GET

と書いてある。調べてみるとどうやらアプリがクラッシュしてしまっているらしい。次に

herock run rails  c

を行うと最後のところに

/app/app/controllers/application_controller.rb:13: syntax error, unexpected
end-of-input, expecting end (SyntaxError)

とある。どうやらapplication_controllerでendの数が違っていた様子。
修正してアップしたら問題なくいけました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby ~配列~】勉強メモ3

Rubyの復習。
ほぼ自分の勉強メモです。
過度な期待はしないでください。

配列

配列とは、複数の情報を順番にまとめて管理するときに利用するもの。
配列は、[値1, 値2, 値3]のように作成する。
配列に入っているそれぞれの値のことを要素と呼ぶ。
また、配列も1つの値なので、変数に代入することが出来る。

# 変数languagesに、配列を代入
languages = ["日本語", "英語", "スペイン語"]

puts languages
出力結果
日本語
英語
スペイン語

 

  • 配列に値を追加

配列への要素の追加には、<<という演算子を使用。

languages = ["日本語", "英語", "スペイン語"]
languages << "フランス語"

puts languages
出力結果
日本語
英語
スペイン語
フランス語

 

  • 配列の要素を取得

配列の要素には、前から順に「0, 1, 2, ...」と数字が割り振られる。
これをインデックス番号といいます。インデックス番号は、0から始まる。

配列の要素の取得の仕方は、配列[インデックス番号]とすることで、要素を取得することが出来る。

languages = ["日本語", "英語", "スペイン語"]

# インデックス番号が1の要素を出力
puts languages[1]

# インデックス番号が0の要素を使って「◯◯を話せます」となるように出力
puts "#{languages[0]}を話せます"
出力結果
英語

日本語を話せます


  • 配列の値を変更
変更の仕方
languages = ["日本語", "英語", "スペイン語"]

# スペイン語をイタリア語に変更
languages[2] = "イタリア語"

puts languages
出力結果
日本語
英語
スペイン語


  • lengthメソッド

配列が持つメソッドで、lengthメソッドは配列の要素数を返す。
つまり、配列の中にいくつ要素があるのか調べることが出来る。

変更の仕方
languages = ["日本語", "英語", "スペイン語"]

puts languages.length
出力結果
3


過去投稿記事

【Ruby ~基本(コマンド/メソッド)~】勉強メモ
【Ruby ~条件分岐~】勉強メモ2

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】if文の基本

基本の基本

@expired_reservations = []および@reservations = []をif文ないに記述し沼にはまりました。改めて考えれば基本の内容ですが、嵌っている間はわからないものです。
戒めのために記事にします。

 # 本日の日付を基準に予約中、終了した予約を選別
  def check_date_time_for_reservation
    @dateTime = DateTime.now
    @expired_reservations = []
    @reservations = []
    if user_signed_in?
      user_reservations = Reservation.where(user_id: current_user.id).joins(:reservations_info).order(reservation_day: "DESC")
      user_reservations.each do |reservation|
        check_reservation_dateTime(reservation) #メソッド
        check_expired_reservation_dateTime(reservation)
      end
    elsif trainer_signed_in?
      traienr_reservations = Reservation.where(trainer_id: current_trainer.id).joins(:reservations_info).order(reservation_day: "DESC")
      traienr_reservations.each do |reservation|
        check_reservation_dateTime(reservation) #メソッド
        check_expired_reservation_dateTime(reservation)
      end
    else
      redirect_to root_path
    end
  end

  def check_expired_reservation_dateTime(reservation)
    if reservation.reservations_info.reservation_day < DateTime.now
      @expired_reservations.push(reservation)
    end
  end

  def check_reservation_dateTime(reservation)
    if reservation.reservations_info.reservation_day >= DateTime.now
      @reservations.push(reservation)
    end
  end
  # //本日の日付を基準に予約中、終了した予約を選別
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

バリデーション不足によるエラー解決 ~Active Storage を用いた画像投稿機能の実装時発生~

はじめに

・投稿者は初学者ですので誤った情報を投稿してしまうことがあります。その時はぜひ、遠慮会釈なしにご指摘いただけると幸いです。

今回のエラーの原因

画像データのバリデーションが欠けていたため、本来であれば画像とセットで投稿しないといけないが、画像がない状態でデータの保存ができてしまっていた。
そのため、画像のないデータを受け取ったビューファイルが混乱し、エラーが起きた。

以下、詳細。

画像投稿の実装の手順

画像投稿機能を実装しようとActiveStorageを導入した。
そして、以下の手順で準備を進めた。
まずは投稿データと画像データが1対1の関係になるように記した。

models/prototype.rb
class Prototype < ApplicationRecord
  belongs_to :user
  has_one_attached :image #1対1の関係になる
  has_many :comments, dependent: :destroy

  validates :title, :catch_copy, :concept, presence: true
end

次に、コントローラーを以下のように書き換えた。

controllers/prototypes_controller.rb
  def create
    @prototype = Prototype.create(prototype_params)

    if @prototype.save
      redirect_to root_path
    else
      render :new
    end
  end

  private 
  def prototype_params
    params.require(:prototype).permit(:title, :catch_copy, :concept, :image).merge(user_id: current_user.id)
  end

prototype_params:imageを追加することによって投稿データに画像も付属されるように設定。

続いてviweファイルの調整を行う。

views/prototypes/index.html.erb
<div class="card__wrapper">
  <%= render partial: 'prototype',  collection: @prototype, locals: { prototype: @prototype } %>
</div>
views/prototypes/_prototype.html.erb
<div class="card">
  <%= link_to image_tag(prototype.image, class: :card__img ), prototype_path(prototype.id) %>
  <div class="card__body">
    <%= link_to prototype.title, prototype_path(prototype.id), class: :card__title %>
    <p class="card__summary">
      <%= prototype.catch_copy %>
    </p>
    <%= link_to "by #{prototype.user.name}", user_path(prototype.user.id), class: :card__user %>
  </div>
</div>

エラー発生

投稿の内容は、「投稿物の名前」、「キャッチコピー」、「コンセプト」と先ほど設定した「画像」の全部で4つ。
ローカル環境で動作確認をしていたところ、画像がない状態で投稿すると、投稿が弾かれることなく以下のようなエラー画面が発生した。

スクリーンショット 2020-12-01 12.11.39.png

よく見ると上から3行目に、
Can't resolve image into URL: to_model delegated to attachment, but attachment is nil
と書かれていることが分かる。
Can't resolve image/attachment is nil と書かれているので、「画像がないからエラーが出ているぞと」言っているのだろうと理解できた。

エラー解決のための仮説

エラー発生直前に行なっていた動作と、エラー画面を合わせて考えると、以下のような仮説にたどり着いた。

1、画像だけがない状態で投稿をしたらエラーが発生した。
2、エラー画面に表示されているコードはビューファイルのもので、尚且つ、画像を表示するコードを赤く示している。

=>あるはずの画像データがないため、ビューファイルのコードが混乱しているのではないか。
=>画像だけがない状態で投稿をしたら、弾かれなかったということは画像データのバリデーションが正常に機能していないのではないか。

この2つの仮説をもとに、モデルのファイルを覗いてみると、案の定、画像データのバリデーションが欠けていた。

models/prototype.rb
class Prototype < ApplicationRecord
  belongs_to :user
  has_one_attached :image #1対1の関係になる
  has_many :comments, dependent: :destroy

  validates :title, :catch_copy, :concept, presence: true
end

models/prototype.rb
class Prototype < ApplicationRecord
  belongs_to :user
  has_one_attached :image #1対1の関係になる
  has_many :comments, dependent: :destroy

  validates :title, :catch_copy, :concept, :image, presence: true
end

validates :image が抜けていたために起きたエラーであった。
これによりバリデーションがかかり、エラーの原因は解消できた。

エラー画面が消えない(余談)

バリデーションを設定し直したことで原因は解消されたが、サーバーやPCを再起動してもエラー画面から抜け出すことができなった。最初は、他に抜けているところがあるに違いないと、様々なファイルを覗いたが間違いはないように思えた。
頭を抱えて休日の半日を過ごしたが、原因は簡単なものだった。

バリデーションの設定前に保存した、画像がない投稿データがあるからエラー画面から抜け出せないだけであった。

Sequel Proを用いて直接、画像のない投稿データをレコードごと削除すると、無事にエラーの解消ができた。
Herokuでデプロイした後であっても、下記のブログを参考にすれば解決できることも判明したので、忘れないようにこれも記述する。

参考文献

herokuでデプロイしたDBにSequelProで接続してみる
【Active Storage】ファイルアップロード時のバリデーション設定
【初心者向け】RailsのActive Recordの解説&メソッドまとめ

とても参考になりました。
ありがとうございました。

感想

「バリデーション抜け」も「画像がない投稿データ」も、些細な原因ではあるが、解決までに十数時間を費やした。

エラー画面を見続けて、苦しさとか悲しさとか色々と感情が込み上げて泣きそうにもなったが、解決できたときは晴れやかな気持ちになって「プログラミング最高!」と1人で盛り上がった。

エラーで挫けそうになることはこれから先、何度もあるはずだが、解決できたら最高な気分になれるという今回の教訓を思い出して乗り越えたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ruruby: RustでつくっているRuby

この記事は言語実装 Advent Calendar 20202日目の記事です。前回はκeenさんの「自作コンパイラをブラウザ上で動かす」、次回はmitsuchiさんの「LLVM の Kaleidoscope を育てながら作る」です。

はじめに

みなさま、ご無沙汰しております。monochromeです。rurubyという名前でRuby処理系のRustによる実装を行っていて、昨年のAdvent CalendarでRustでつくる(つくれるかもしれない)Rubyという記事を書きました。今回はこの1年間の進捗と、今後の予定などを書いてみます。

レポジトリはこちら。実行にはRustツールチェーン(nightly channel)のインストールが必要です。
https://github.com/sisshiki1969/ruruby

毎度の宣伝ですが、「プログラミング言語処理系が好きな人の集まり」というSlackチャンネルがあって、言語処理系の話題が好きなプロ・アマ・学生・大学教員・水道屋・正常者・異常者などがごった煮になって議論や雑談をしています。質問、実装や発表の報告、学術的な話題、ポエム、何でもありです。この記事を読むまでもなく言語処理系が好きな方、この記事を読んで言語処理系に興味を持った方、この記事を読んでも言語処理系に興味が持てない方は是非ご参加ください。

要約

RustでRubyインタプリタを作っている。構文・機能の大半は実装できていて、中規模のプログラムも動くようになってきた。辛いこともあるけど楽しい。
野望としてはもっと多くのプログラムやライブラリを動かしたい、あともっと速く動くようにしたい。

Optcarrotが動いた話

昨年の段階で小さなベンチマークは動くようになったので、次の目標としてOptcarrotというプログラムを動かすことを目指すことにしました。OptcarrotはRubyコアチーム・コミッタのmameさんが作られている、Rubyで書かれたNES(ファミコン)のエミュレータです1。今年のクリスマスにリリースされるRuby3をRuby2の3倍速く動作させる目標(Ruby3x3)の検証に使われています。また、公式(CRuby)以外の各種実装の速度比較もできます。

Optcarrotは自作Ruby処理系のターゲットとして都合がよい、いくつかの性質を備えています。

  • ある程度大きな、「普通の」プログラムである
    Rubyの(一般によく使用される)各種文法・記法をまんべんなく使って書かれています。テストプログラムを自分で書くと未実装の構文・機能を避けて書きがちなので、「普通のプログラムが普通に動く」ことを目指す上では重要です。

  • 外部ライブラリへの依存がない
    実際にゲームで遊ぶにはグラフィックス・入出力ライブラリ(と連携するためのFFIライブラリ)が必要ですが、ベンチマーク専用のモードでは他のライブラリへの依存はありません。自作処理系では機能が乏しいのとライブラリも未整備で、特にC言語で書かれたライブラリは動きませんから、これもありがたいことです。Optcarrotは自作Ruby処理系製作者のために作られたものといって過言ではありません。

  • ベンチマークを取れる
    ツールが付属していて、各処理系・バージョンをDocker内で自動的にビルド・実行して速度を計測し、グラフにできます。

で、色々頑張った結果、自作処理系でOptcarrotを動かすことができました。実行速度はまだまだですが、GitHubの既存処理系のベンチマークリストの片隅にrurubyも入れてもらいました。ありがとうございます。データは5月ごろのものなので、今は倍ぐらい速くなっています。
benchmark-summary.png

以下、その過程で実装に時間がかかった部分を紹介します。

ガーベジコレクタ(GC)

スクリプト言語はヒープ上に大量のオブジェクトを生成するため、定期的に不要になったオブジェクトを掃除する必要があります。掃除をするコンポーネントがガーベジコレクタ(GC)です。たいていのプログラムはGCなしでも動きますが、ベンチマークを取るのにフェアではないのでスクラッチから作りました。初心者なのでマーク&スイープのシンプルなもの(precise, non-moving mark and sweep GC)です。256KBを1ページとし、オブジェクトとマーク用のビットマップをその中に入れています。

現在のところGCが扱うのはRubyオブジェクトの構造体のみです。その他にクロージャを生成する際に関数フレームをヒープにアロケートするので、将来的にはそれもGCの対象とする予定です2

動作としては、

  1. ページの先頭からオブジェクトを割り当てていく
  2. ページの残りが少なくなったところでGC起動
  3. ルートから全てのオブジェクトをたどり、ビットマップにマークをつける。
  4. マークのないオブジェクトを廃棄してフリーリスト(linked list)につないでいく
  5. オブジェクトの生成要求があればフリーリストを外して払い出す
  6. 空になったページは一旦廃棄して後で再利用
  7. ページが一杯になったら新しいページを割り当てる

という感じです。

大量にオブジェクトを使い捨てるapp_aobench.rbで計測したベンチマーク(CRuby v3.0.0-devとの比較)を載せておきます。

gc_bench.png

左がメモリ消費、当たり前ですがGC導入で激減しています。右は実行時間、素朴な実装なのでパフォーマンス的にはまだまだです。将来的には世代別GCなどを導入したいですが、一体いつになることやら。

rurubyのGCについての発表資料

コルーチン(Fiber)

ファミコンではメインのCPUと画面描画用のGPU(PPUと呼んでいる)が同時に動きながらデータのやり取りを行います。Optcarrotでもこれをエミュレートするため、コルーチンを実現するRubyの言語機能であるFiberクラスを使っています。

コルーチンについてはこれまたmameさんがn月刊ラムダノート Vol.1, No.1(2019)に詳しく書かれています。RubyのFiberは(古典的な)コルーチンに忠実な実装と言って良いと思います。
『「コルーチン」とは何だったのか?』著者のブログエントリ

JavaScriptのジェネレータやC#のasync/awaitみたいなコルーチン「っぽい」ものは状態機械に変換することができますが、RubyのFiberは任意のメソッドを呼び出していった先からいきなりyieldして呼び出し元に帰る(そして後で元の場所へ復帰する)ことができるので、rurubyのようにメソッド呼び出しのたびにホスト言語(Rust)の関数を再帰的に呼び出していく動作モデルだと同じことをやるのは不可能です3

The life of Fiber.png

実装にかなりてこずった挙句、結局OSのスレッドを使ってRustのsync channelの仕組みにより複数のVMを同期させつつ並行に動かしています。上の図はrurubyにおけるFiberの生成から実行、廃棄に至るライフサイクルを示しています。青の矢印は子→親へのデータ転送(Fiber.yieldメソッド)、赤は親→子(fiber.resumeメソッド)を示しています。

  • 親FiberがFiber.newで子Fiber(fiberオブジェクト)を生成
  • 親がfiber.resumeを実行すると親は一旦停止して子の実行が開始
  • Fiber.yieldが呼ばれると子は一時停止して親からのfiber.resumeを待機
  • 再び親がfiber.resumeを呼ぶと動作は子に移る
  • 子が実行を終えるとDeadとなり、次のGCのタイミングで回収される

本家RubyのFiberはユーザースレッドを用いて実装されており、rurubyに比べ軽量で高速に動作します。特に新しいFiberの生成は文字通り桁違いに速いです。Rustでポータブルな形で同じことをやるのはなかなか難しそうですが、自分の実装力が上がったらいつかチャレンジしてみたいと思っています。
Fiberを実装した人の講演動画「Fiber in the 10th year」 RubyKaigi2017

正直Fiberのような機能はなくても大半のプログラムは動きますし、実はOptcarrotもFiberの実装を持たない処理系のためにコードを自己書き換えしてFiberを使わないコードに変換する機能を持っています。rurubyはこのために複数スレッドでVMを動かす仕組みを入れました(アロケータ・GCは共有)4。結構面倒くさいので、もし自作言語だったら、もしくはターゲットがOptcarrotでなかったらこういうのは間違いなく実装しなかったと思います。モチベーションの維持には自己満足が大事。
rurubyのFiberの実装についての発表資料

カロリーメイトquineが動いた話

少し前の話になりますが、今年の夏に大塚製薬のカロリーメイト・リキッドのプロモーションサイトが話題になりました。
CalorieMate to Programmer CUI MODE

(ネタバレになりますが)その中にQuineプログラムが隠れています(コードはこちらのブログ参照)。これはRubyで書かれており、自分自身のコードと同一の文字列5を出力した後、アニメーションが始まります。これは面白いと思い、rurubyで動くか試してみました。…が最初は全く動かず、ここでまたつまづきました。

  1. 複素数クラスとリテラル
    缶の中に液体が注がれてしぶきが飛び散るアニメーションがありますが、これは実行時に流体力学的シミュレーションにより計算されています。そのために複素数クラス及びリテラルが使われています。これはただ実装するだけ。

  2. 謎の記法
    全体的にeval$s=%q{Z=?\s;eval"$><<S=Z*4"+(%w{+"n=#{-~n%3};eval$s=%q{#$s}#YE";(以下略)のような謎コードになっていて、今見返してもなぜこれが自分の処理系で動くのか理解できません。かすかに覚えている範囲では、まず文字列をevalして6得られた文字列をevalし、得られた文字列をさらにevalするという構造になっていたかと思います。
    ・#{}(文字列中に式を埋め込むテンプレートリテラル、変数一つのみの場合は#$sのようにも書ける)
    ・%q{}, %w{}のような文字列リテラル(%記法)
    ・$>(標準出力オブジェクト、<<演算子で右辺のオブジェクトを出力できる)
    ・?\s(文字リテラル、\sで空白文字を表す)
    のような多彩な記法7があるので、これらすべてを正しくパースするのが大変でした。

  3. 単一代入、多重代入時の評価順
    s[k]=Z*(k=(k*2-21)** 2/99)+q*79+Z+q*2*(6-k)のようなコードが最初は正しく動かずかなりハマりました。これ、配列のインデックスkを右辺式の中で再代入していて、先に右辺を評価してから左辺の配列を評価するとダメなんですね。確かにそちらの方が自然8かもしれないですが、分かるまではかなり悩みました9
    で、恐ろしいことに単一代入だと 左辺を評価→右辺を評価→代入 ですが、多重代入(例:a,b,c = 1,2)の場合は 右辺を順に評価→左辺を順に評価→順に代入 という順番になる奇妙な仕様です。
    多重代入の謎仕様を何とかしてほしいという市民の訴え

  4. 不思議な代入式
    Rubyでは a+b=3が有効な式10です(a+(b=3)と解釈される)。こういうのを正しくパースするのも(手書きパーサなので)かなり手間取りました。

実行速度について

インタプリタの高速化に関してはここではあまり書きませんが、機会があればまた。各種ベンチマークとOptcarrotのデータを載せておきます。フィボナッチ数列の計算が一番遅いことからわかるようにメソッド呼び出しのオーバーヘッドがまだかなり大きいので頑張りたい。なんとか2倍(遅い)以内に収めたいですねえ。

各種ベンチマーク
benchmark CRuby(v3.0.0dev) ruruby rate
loop_times.rb 0.82 ± 0.03 s 1.15 ± 0.03 s x 1.41
loop_for.rb 0.85 ± 0.01 s 1.50 ± 0.01 s x 1.76
loop_whileloop.rb 0.40 ± 0.01 s 0.63 ± 0.01 s x 1.60
so_concatenate.rb 2.59 ± 0.01 s 3.13 ± 0.04 s x 1.21
string_scan_str.rb 0.15 ± 0.00 s 0.29 ± 0.01 s x 2.00
string_scan_re.rb 0.19 ± 0.00 s 0.30 ± 0.01 s x 1.57
so_mandelbrot.rb 1.90 ± 0.02 s 2.76 ± 0.03 s x 1.46
app_mandelbrot.rb 1.31 ± 0.02 s 1.24 ± 0.02 s x 0.94
app_fibo.rb 0.53 ± 0.00 s 1.56 ± 0.01 s x 2.95
app_aobench.rb 9.72 ± 0.40 s 20.34 ± 0.40 s x 2.09
so_nbody.rb 1.10 ± 0.02 s 2.35 ± 0.04 s x 2.13

rateが大きいほどrurubyが遅いことを示します。
Rubyレポジトリ内のベンチマーク コードはこちら

Optcarrotベンチマーク
benchmark ruby ruruby rate
optcarrot 40.68 ± 0.40 fps 14.19 ± 0.05 fps x 2.87
optcarrot --opt 127.21 ± 1.66 fps 57.36 ± 1.89 fps x 2.22

--optオプションをつけるとコードを自己書き換えして高速化されます。

既存言語の処理系実装について

最後に既存言語の実装について少し書きます。

自作言語にせよ、既存言語にせよ、インタプリタにせよコンパイラにせよ、自分の実装がある程度動くようになって(例:自作Cコンパイラで自分自身をコンパイルできた!)ひとしきり興奮した後、大抵は次に何をするかという問題に直面します11。これはこの界隈でセルホスト後燃え尽き症候群と呼ばれる深刻な病態です12。特に自作言語の場合はユーザーも少なく、一人で地道に文法を拡張して、扱える型やクラスを増やしてライブラリを整備して…というのはモチベーションを維持しづらく、なかなか続かないと思います。

一方、すでに広く使われている言語の場合はきちんと動作することが保証されているライブラリやプログラムがたくさんあります。それらを一つ一つ動かしていくというのはそれなりに楽しい作業です。もちろん巨大なフレームワークなどは依存する全てのライブラリが動かないとダメなので、依存の少ない小さめのものから始めていくことになります。私にとってはOptcarrotが最初の具体的な目標となり、そしてその選択は結果的には成功だったと思っています。

既存言語の処理系実装の良いところ
  • 面倒くさい部分・あまり興味のない機能も実装しないといけないので、実装力がつく
  • 動く(はずの)プログラムがライブラリを含めて豊富にあり、自分の処理系が正しく動いているかを検証できる
    ライブラリが動いたらそれを使っているもっと大きなプログラムを動かして…というわらしべ長者戦略です。いずれrurubyでもRuby on Railsが動く予定です13
既存言語の処理系実装のつらいところ
  • 謎の文法や謎の仕様の実装がつらい
    つらい。

今後の野望

今はruby/specという(rubyレポジトリの中にあるので、たぶん)Ruby公式のスペックスイートを動かせるようにしようと頑張っています。基本的には基本クラスのメソッドがまだ全然足りないのと、冒頭で「構文・機能の大半は実装できていて」とか書いてますが、実は例外処理がまだ動きません。これは今年中に何とかしたいです。
あと、高速化も頑張りたい。
寒さも厳しくなってきましたので、みなさま健康に気をつけて良い進捗を。
それではまた来年。


  1. 作者のプレゼン動画ブログエントリ 

  2. クロージャを作らない通常のメソッド呼び出しでは関数フレームはスタック上に確保する。その他の、例えばArrayクラスのオブジェクトが使う配列の構造体などはRustがよしなに管理してくれる。 

  3. これは嘘で、レジスタ保存領域と戻り先のアドレスとスタック領域を1セット用意してその都度切り替えれば可能。古くはC言語のsetjmp/longjmp、最近だとUNIXのucontextというのがある。普通のポータブルなやり方では困難、という意味。 

  4. ついでに言うとrurubyでは1プロセスの中でアロケータ+複数スレッドの組をさらに複数走らせることができるようになっている。何のためかというとRustのテスト(cargo test)はマルチスレッドで走るため。 

  5. 自分自身と同じものを出力するのが本来のquineだが、これは色などが微妙に異なる3パターンあって、出力するたびに異なるバージョンが出力されるという凝りよう。人間とは役に立たないことにこそ熱中する生き物である。 

  6. %w{}リテラルで囲まれた部分は、中の空白がすべて除去されて結合され、一つの文字列になる。このおかげでロゴの部分の空白は潰された状態でevalされる。 

  7. Perlなど、いろんな言語由来らしい 

  8. 評価の順序としては自然だが、こういうコード自体は全く自然ではない 

  9. そんなのprintfデバッグでもすればすぐ分かるんじゃないのか、と言われるかもしれないが、そもそもこのプログラムが何をやっているのかさっぱり分からないので大変なのである 

  10. 普通は'+'の方が'='より優先順位が高いので(a+b)=3にパースされてエラーになるはず。でないとa=b+3(a=b)+3になってしまう。これも試行錯誤した挙句、アドホックに解決した。 

  11. コード書くのにモチベーションは不要という奇行種は除く 

  12. そのまま開発を続けてgitやらpythonやらがコンパイルできるところまで突き抜けていく方もいるようですので、あくまで一般論です 

  13. 無理です。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】tapについて少し調べてみた。

環境

Ruby 2.6
Rails 5.1.7

背景

機能実装中、似たような処理を探していたら「tap」を見つけてなんだこれ、と思い調べた。これは初めて遭遇したので備忘録として。

tapとは

"foo".tap {|str| puts str.reverse}
# oof
# => "foo"

メソッドの返り値は変わっておらず小文字のままだけれど、tap自体は動いていて文字列が反対になっている。
調べたところによると、tapはメソッドの中で何かしたいけど、メソッド自体の評価は変えたくない時に使うみたい。

実際に見たコード

こんな使い方もある(らしい)。

...
User.new.tap do |item|
  item.name = "Taro"
  item.address = "test@gmail.com"
  --- 中略 ---
  item.tel_num = "09012345678"
end.save!
...

endの後にsave!ってなんやねんと思ったけど大分スッキリ書けますね。
tapを応用していって可読性の高いコーディングしていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kinx での JIT、そして MIR の話 ... Ruby だけでは勿体ないネ

この記事は「言語実装 Advent Calendar 2020」 21日目の記事です。

本記事は、主に JIT コンパイルに関する話題ですが、一般的なものではなく私の身の回りでやってることを中心にご紹介したいと思います。

はじめに

今回は、言語実装 Advent Calendar ということで 自作言語である Kinx における、最新の native 実装状況、そして JIT ライブラリのお話、および JIT 関連の注目ライブラリ MIR に関してご紹介します。本記事は、以下で構成されています。

  1. JIT コンパイラのプラットフォーム依存性について
    • JIT コンパイラの概要と課題。一般的には実行時にどこをコンパイルするかとかトレーシングがどうとかなのかもしれませんが、プラットフォーム依存も一つの問題です。Ruby は独特の方法で回避しましたが...
  2. Kinx Native - sljit による JIT コンパイル
    • native というキーワードに関して元々は ここ(JIT Compile) でいくつか紹介していたのですが、その後、割とアップデートさせたので再度まとめ直してみました。
  3. JIT ライブラリ - 手軽に JIT コンパイルを体験
    • 直接アセンブラを使わないまでも、抽象化されたアセンブリを直接扱うためのライブラリを用意しています。その紹介と今後したいことなど。
  4. MIR 感想
    • 注目の JIT コンパイラ基盤。Cコンパイラが付いている。Ruby の人たちはよく知ってるアレです。非常に楽しみなプロジェクトで、少し触ってみた感想などをご紹介。

しかし本業が忙しすぎて色々なことを進める時間が圧倒的に足りなくなっている今、いつできるのか...。ちょっと前まではもう少し時間が取れたのですが。まぁ個人的なサイドプロジェクトなので、焦らず気長に行きます。もちろん、もし少しでもご興味を持っていただけるのであれば、どなたでもどんな内容でも コントリビュート大歓迎 です!(Kinx の GitHub は こちら

【ご参考】Kinx とは

Kinx の最初のコミットは 2019/11/28 のようです。約 1 年(本記事の公開は 2020/12/21 ですが、書き始めたのは 11/11)経ちました。さて、元々の動機は下記を参照していただくとして、キャッチフレーズがありますので例によってそこからスタートします。Here we go!

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。ご存じない方は下記をご参照ください。

つい昨日(12/20)、「令和時代の基礎文法最速マスター Advent Calendar 2020」 20日目の記事として「Kinx 基礎文法最速マスター」がアップされましたので、そちらも合わせてご覧ください。

1. JIT コンパイラのプラットフォーム依存性について

あらためて、Just In Time、つまり まさにその場で コンパイルして実行する方式。ネイティブコード(機械語、マシン語1)にコンパイルして実行するため実行速度を追い求める人たちの間では 欲しくて欲しくてたまらない機能 の一つです。

ただし、普通は プロセッサーごとに個別に実装しないといけない。x64 だけ、とかならまだいいんだけど、どっかで ARM くらいはサポートすっかなー、とか、PowerPC は、SPARC?、とかになると、そもそもテストするのが大変です。

そして、同じ x64 でも Windows と Linux は違います。Microsoft x64 Calling Convention と System V Calling Convention という、いわゆる 呼出規約 というものが違っています。

Microsoft x64 / System V Calling Convention

そもそも関数呼び出しで使うレジスタが違います。これは読み替えればよいのだけれど。

引数 1 引数 2 引数 3 引数 4 引数 5 引数 6 ... and more
Microsoft rcx rdx r8 r9 (stack) (stack) (stack)
System V rdi rsi rdx rcx r8 r9 (stack)

double 型の受け渡しにはどちらも SSE レジスタを使いますが、Microsoft x64 の場合は xmm0 から xmm3 まで、System V の場合は xmm0 から xmm7 までとなっています。そして、以下のように Microsoft x64 の場合は位置によって使うレジスタが固定であるのに対し、System V では 位置がズレます

引数 1
int
引数 2
double
引数 3
int
引数 4
double
Microsoft ecx xmm1 r8d xmm3
System V edi xmm0 esi xmm1

こういうのを考えてプラットフォームごとに JIT コンパイラを作るわけですが、こういうのを意識しても所詮 x64 の世界の内側だけの話であって ARM にすら対応できていない、という現状なのです。

Ruby 方式

そこで、Ruby は なんと C ソースを出力してコンパイルして実行するという大胆な方式 を採用しました。メリットは上記に書いた多くのプロセッサへの対応を 他人に丸投げ(いい意味で)できること、デメリットはファイル入出力やプロセス起動などのオーバーヘッドが大いに気になることと 別途コンパイラを要求する ところ。実行環境にコンパイラを要求するのは、まぁ Linux なら gcc があるし、とか思うけど Windows が問題 ですよね。普通の人にコンパイラをインストールして、とかなかなか言いづらい。しかも本体とコンパイラを合わせないとイケなかったり。

これをやるなら、私なら C コンパイラも含めてパッケージに入れて提供したい、と思う。LLVM とか使えるのあるしね。デカいけど。ちなみに、以前 Clang で JIT するやり方を以下に公開してみました。Clang/LLVM 8.0.0 の頃なので多少古いが、今でも役に立つかな?

x64 だけに絞っても、全部スタティックリンクすると約 35MB の実行ファイルが出来上がります。デカいぜ。

そして(多少)時は流れ...

...と、ここまでは以前も書いた内容とほぼ同じ。で、やはり誰かが計画するわけですね。C コンパイラの同梱。そのためのプロジェクトが MIR です。がっつり Ruby の JIT に組み込むことを計画してます。軽量な最適化機能付き(関数のインライン展開付き) JIT コンパイラです。さらに、十分抽象化された中間表現 を持ち、それによって 様々なプラットフォームへの対応を可能 にしています。そして、C コンパイラが付いているのが超絶に便利 です。Ruby JIT に使うので当然でしょう。ところが、実はそれは単純に C スクリプト環境としても最適ということを意味します。あぁ、これ欲しかったですよ。ただし、まだバグも色々ありますが。

ある意味、C 言語にトランスパイルする言語はこれで全て実行可能にすることもできる でしょう。新たに C 言語にトランスパイルする言語を実装しようとしたとき、その言語実装自体は 超簡単 になりそうです2。...いや、いろいろ難しいかもしれませんが。イメージで話しました。すみません。

元々 Kinx でも同様に一気にアセンブルするやり方を試したりできないかなー、と kcs という自作 C JIT コンパイラを使ってみようと思ってました。ただ、x64 だけなのと最適化はほぼ無いので、MIR のほうが断然良いですね。もちろん kcs は JIT 基盤にしようとした訳ではなく、単に C スクリプトを実現しようとしたものなので、そもそもの目的が違ってます。しかし MIR みたいなのが出てくるとなると彼ももうお払い箱ですね…。残念ですが。一応 kcs の紹介だけ(更新止まってます)。

MIR は Ruby JIT への組み込みが計画されていますが、それだけでは勿体ないですね。これほどフレキシブルでライトウェイトでありながら、かつ十分抽象化された IR レイヤを持つ MIT ライセンスの(C コンパイラを含む)JIT コンパイラは他に見つけられなかったので、Kinx でも活用できないか、他に何か良い使い方が無いか、夢が膨らみます。MIR については後述します。つーか、Clang/LLVM はとにかくデカいんです(くどいですが)。

2. Kinx Native - sljit による JIT コンパイル

さて、本題に戻りましょう。まずは Kinx の native からです。

Kinx では、native というキーワードを使うことでネイティブ・コードにコンパイルして実行するという機能を持っています。いわゆるトレーシングとかは無しに、いきなりネイティブ・コードにコンパイルして実行させます。これは数値計算を主とする各種ベンチマークではそれなりに結構いい感じの結果を出してます。例えば「この記事」参照。

...が、実用上、通常のプログラムの中ではイマイチ使いこなせていません(私自身が。アイタタタ)。まぁでも、他の言語にはあまりない Kinx の特長でもあるので、大切に育てていきたい機能ではあります。

さて、Kinx の native による JIT コンパイルでは、上記のプラットフォームに依存してしまうという問題に対し、sljit を採用することで回避しています。sljit はアセンブラを抽象化しており、その抽象化アセンブラで書くことでコード出力時に x64 や ARM などに合わせて出力ができるようになっています。その代わり、レジスタ数とかどのプロセッサーでも共通に使えるようにミニマムに合わせておかなければなりません。

ちなみに、sljit のサポートアーキは以下の通りとのこと。

  • Intel-x86 32
  • AMD-x86 64
  • ARM 32 (ARM-v5, ARM-v7 and Thumb2 instruction sets)
  • ARM 64
  • PowerPC 32
  • PowerPC 64
  • MIPS 32 (III, R1)
  • MIPS 64 (III, R1)
  • SPARC 32

ただし、x64 以外ではテストしていない & 64ビットしか想定していないので悪しからず。

具体的なソースコードとして、ネイティブ・コード用 IR 出力ルーチンは src/ast_native.c にあり、sljit を使ったネイティブ・コード用コンパイラは src/nir_compile.c にあります。

また、sljit 自体の使い方(と事例)は、先日「Kinx 実現技術 - JIT: sljit の使い方」に書いてみましたので、興味ある方はこちらも参照してみてください。

Native - 個別の実装状況

では、本題となるアップデート状況です。ここ(JIT Compile) での紹介の後、ちょっとずつ進化してます。

IR (Intermediate Representation) :tada:

native ではネイティブ・コードに変換する上で扱いやすいように、スクリプト VM 用の IR と全く異なるコードを出力します。なぜなら例外の扱いとかコンパイル時点における型チェックの厳密さとかが全く異なるためで、それに合わせて違う体系にしています。

今回、IR 出力時のレジスタ割り当て方式を変更したところ、必要なレジスタ数が減ってだいぶスッキリさせられました。また、余計なコードもだいぶ出さないようにすることもしました。

前回の例でいうと、

native nfib(n) {
    if (n < 3) return n;
    return nfib(n-2) + nfib(n-1);
}

これは以下のようになります。

nfib(1, 3):
  .L0
  .L1
       0:   load                    r1, $(0,0)
       1:   lt                      r2 = r1, 3
       2:   jz                      .L4
  .L2
       3:   load                    r1, $(0,0)
       4:   ret                     r1
  .L3
  .L4
       5:   load                    r1, $(0,0)
       6:   sub                     r2 = r1, 2
       7:   arg                     r2
       8:   call                    r1 = [rec-call]()
       9:   excpt                   r2 = check
       a:   jz                      .L6
  .L5
       b:   ret                     0
  .L6
       c:   load                    r2, $(0,0)
       d:   sub                     r3 = r2, 1
       e:   arg                     r3
       f:   call                    r2 = [rec-call]()
      10:   excpt                   r3 = check
      11:   jz                      .L8
  .L7
      12:   ret                     0
  .L8
      13:   add                     r3 = r1, r2
      14:   ret                     r3
  .L9
      15:   ret                     0

以前に比べて使用するレジスタ数が 10 → 3 に削減。ライン数も 0x20 → 0x15 に削減。

例外処理

例外機能は以前の記事から変わっていません。なので、現時点でも以下の制限があります。

  • スタックトレースは取得できない。
  • オブジェクトがまだ十分使えないので、キャッチする例外オブジェクトは実質あまり使えない。例外のフローはきちんと動作する。

いやぁ、もう少し機が熟す必要がありますね。

型チェック

これも以前から変わっていません。関数の入口で引数の型をチェック。レキシカル変数をアクセスする場合もチェック。関数から返ってきたときにもチェック。型が期待するものと違ってたらすぐさま例外を上げます。

Switch-Case :tada:

Switch-Case は、以前の記事では未サポートでしたが、頑張ってサポートしました。最新版では以下の形で実装しています。

  • Case の数に応じて 2 分探索と線形探索を切り替える。
  • Case 条件は Integer のみ。
  • ジャンプテーブル化は現在未サポート。

たぶんジャンプテーブルも実現可能な感じには sljit をハックしたのですが、まずは 2 分探索で十分だろうと判断して上記で実現。少なくともこれで実用的な Switch-Case を実現することはできるので。ジャンプテーブル化は時間があれば実施しますが、優先度は低いです。

VM 関数呼び出し

これは現時点でも未サポートです。子 VM を起動してジャンプすれば良いとは思いますが、入れ子になった VM 内で例外が発生した場合の処理が大変そうなので、まだ手がつけられていません。おそらく以下のような感じでできるのでは、と思っています。

  • フレームに native からのコールであるフラグを立てる。
  • return 時、native からであれば復帰値をセットして VM から return する。
  • 例外発生時、スタックを巻き戻している最中に native コールされたフラグを見つけた場合、そこでスタックトレース情報の作成を中断し、native 向けの例外情報を設定して native にリターン。native 側に帰ってきたら、native での例外伝搬の仕組みで伝搬させていく。

例外のスタックトレースを native 関数内でも保存するようにしないといけません。やってみる時間があれば挑戦してみる、という予定ですが、今は難しい。ただ、たぶん面倒くさいだけで実現可能だとは思います。

オブジェクトへのアクセス :tada:

これは一部実装しました。具体的には IntegerDouble への配列をサポートしました。以下のように Quick Sort くらいはできます。

native quicksort_n(a:int[], first, last) {
    var i = first;
    var j = last;
    var x = a[(first + last) / 2];
    while (true) {
        while (a[i] < x) i++;
        while (x < a[j]) j--;
        if (i >= j) break;

        [a[i], a[j]] = [a[j], a[i]];
        ++i; --j;
    }
    if (first  < i - 1)
        quicksort_n(a, first, i - 1);
    if (j + 1 < last)
        quicksort_n(a, j + 1, last);
}

const N = 20;
var a = N.times { => Integer.parseInt(Math.random() * 100) };
System.print("Before:");
a.each { System.print(" %2d" % _1); };
System.print("\n");

quicksort_n(a, 0, N - 1);

System.print("After: ");
a.each { System.print(" %2d" % _1); };
System.print("\n");

結果。

Before: 97  6 27  1 21 25 96 52  6  0 11 98 97 66 76 66 69 99 68 80
After:   0  1  6  6 11 21 25 27 52 66 66 68 69 76 80 96 97 97 98 99

整数配列は a:int[] と記述し、実数の場合は a:dbl[] という記述をします。

レジスタ割り付け :tada:

実際には変数へのレジスタ割り付け自体はやっていません。

...が、IR 出力時のレジスタ割り当て方式を 大幅に 変更したので、それに伴って演算結果で保存可能なものをレジスタに割り当てることにした結果、劇的にメモリアクセスが減りました。例えば、上記 fib の例でいうと以下のようになります。

以前のコードでは fib(n-2) の部分は以下のように出力されていました(無駄なコードがいっぱいです)。

      d7:   48 8b 14 24                                 mov rdx, [rsp]
      db:   48 89 94 24 30 02 00 00                     mov [rsp+0x230], rdx
      e3:   48 8b 94 24 30 02 00 00                     mov rdx, [rsp+0x230] 
      eb:   48 83 ea 02                                 sub rdx, 0x02
      ef:   48 89 94 24 38 02 00 00                     mov [rsp+0x238], rdx
      f7:   48 8b 94 24 38 02 00 00                     mov rdx, [rsp+0x238]
      ff:   48 89 54 24 18                              mov [rsp+0x18], rdx     
     104:   48 c7 84 24 18 01 00 00 01 00 00 00         mov [rsp+0x118], 0x01
     112:   48 8b 4b 18                                 mov rcx, [rbx+0x18]
     116:   48 89 d8                                    mov rax, rbx
     119:   49 8b 57 08                                 mov rdx, [r15+0x8]
     11d:   48 89 54 24 10                              mov [rsp+0x10], rdx
     122:   48 8d 74 24 08                              lea rsi, [rsp+0x8]
     127:   48 89 c7                                    mov rdi, rax
     12a:   ff d1                                       call rcx
     12c:   48 89 84 24 40 02 00 00                     mov [rsp+0x240], rax

これがこうなりました。

      93:   48 8b 2c 24                                 mov rbp, [rsp]
      97:   48 8d 45 fe                                 lea rax, [rbp-0x2]
      9b:   48 89 44 24 18                              mov [rsp+0x18], rax
      a0:   48 c7 84 24 18 01 00 00 01 00 00 00         mov qword [rsp+0x118], 0x1
      ac:   48 8b 4b 18                                 mov rcx, [rbx+0x18]
      b0:   48 89 d8                                    mov rax, rbx
      b3:   49 8b 57 08                                 mov rdx, [r15+0x8]
      b7:   48 89 54 24 10                              mov [rsp+0x10], rdx
      bc:   48 8d 74 24 08                              lea rsi, [rsp+0x8]
      c1:   48 89 c7                                    mov rdi, rax
      c4:   ff d1                                       call rcx
      c6:   48 89 c5                                    mov rbp, rax

分かりづらいとは思いますが全部載せるとこんな感じで、メモリアクセスがかなり減りました。

nfib: (native-base:0x7fbd9c030010)
       0:   53                                          push rbx
       1:   41 57                                       push r15
       3:   41 56                                       push r14
       5:   41 55                                       push r13
       7:   55                                          push rbp
       8:   41 54                                       push r12
       a:   48 8b df                                    mov rbx, rdi
       d:   4c 8b fe                                    mov r15, rsi
      10:   4c 8b f2                                    mov r14, rdx
      13:   48 81 ec 38 02 00 00                        sub rsp, 0x238
      1a:   49 8b 47 08                                 mov rax, [r15+0x8]
      1e:   48 83 c0 01                                 add rax, 0x1
      22:   49 89 47 08                                 mov [r15+0x8], rax
      26:   48 3d 00 04 00 00                           cmp rax, 0x400
      2c:   72 29                                       jb 0x57
      2e:   48 c7 43 20 01 00 00 00                     mov qword [rbx+0x20], 0x1
      36:   48 c7 43 28 06 00 00 00                     mov qword [rbx+0x28], 0x6
      3e:   48 c7 c0 00 00 00 00                        mov rax, 0x0
      45:   48 81 c4 38 02 00 00                        add rsp, 0x238
      4c:   41 5c                                       pop r12
      4e:   5d                                          pop rbp
      4f:   41 5d                                       pop r13
      51:   41 5e                                       pop r14
      53:   41 5f                                       pop r15
      55:   5b                                          pop rbx
      56:   c3                                          ret
      57:   49 83 bf 10 01 00 00 01                     cmp qword [r15+0x110], 0x1
      5f:   0f 85 11 01 00 00                           jnz 0x176
      65:   49 8b 57 10                                 mov rdx, [r15+0x10]
      69:   48 89 14 24                                 mov [rsp], rdx
      6d:   48 8b 2c 24                                 mov rbp, [rsp]
      71:   48 89 e8                                    mov rax, rbp
      74:   48 83 f8 03                                 cmp rax, 0x3
      78:   7d 19                                       jge 0x93
      7a:   48 8b 2c 24                                 mov rbp, [rsp]
      7e:   48 89 e8                                    mov rax, rbp
      81:   48 81 c4 38 02 00 00                        add rsp, 0x238
      88:   41 5c                                       pop r12
      8a:   5d                                          pop rbp
      8b:   41 5d                                       pop r13
      8d:   41 5e                                       pop r14
      8f:   41 5f                                       pop r15
      91:   5b                                          pop rbx
      92:   c3                                          ret
      93:   48 8b 2c 24                                 mov rbp, [rsp]
      97:   48 8d 45 fe                                 lea rax, [rbp-0x2]
      9b:   48 89 44 24 18                              mov [rsp+0x18], rax
      a0:   48 c7 84 24 18 01 00 00 01 00 00 00         mov qword [rsp+0x118], 0x1
      ac:   48 8b 4b 18                                 mov rcx, [rbx+0x18]
      b0:   48 89 d8                                    mov rax, rbx
      b3:   49 8b 57 08                                 mov rdx, [r15+0x8]
      b7:   48 89 54 24 10                              mov [rsp+0x10], rdx
      bc:   48 8d 74 24 08                              lea rsi, [rsp+0x8]
      c1:   48 89 c7                                    mov rdi, rax
      c4:   ff d1                                       call rcx
      c6:   48 89 c5                                    mov rbp, rax
      c9:   48 8b 43 20                                 mov rax, [rbx+0x20]
      cd:   48 83 f8 00                                 cmp rax, 0x0
      d1:   74 19                                       jz 0xec
      d3:   48 c7 c0 00 00 00 00                        mov rax, 0x0
      da:   48 81 c4 38 02 00 00                        add rsp, 0x238
      e1:   41 5c                                       pop r12
      e3:   5d                                          pop rbp
      e4:   41 5d                                       pop r13
      e6:   41 5e                                       pop r14
      e8:   41 5f                                       pop r15
      ea:   5b                                          pop rbx
      eb:   c3                                          ret
      ec:   4c 8b 24 24                                 mov r12, [rsp]
      f0:   49 8d 44 24 ff                              lea rax, [r12-0x1]
      f5:   48 89 44 24 18                              mov [rsp+0x18], rax
      fa:   48 c7 84 24 18 01 00 00 01 00 00 00         mov qword [rsp+0x118], 0x1
     106:   48 8b 4b 18                                 mov rcx, [rbx+0x18]
     10a:   48 89 d8                                    mov rax, rbx
     10d:   49 8b 57 08                                 mov rdx, [r15+0x8]
     111:   48 89 54 24 10                              mov [rsp+0x10], rdx
     116:   48 8d 74 24 08                              lea rsi, [rsp+0x8]
     11b:   48 89 c7                                    mov rdi, rax
     11e:   ff d1                                       call rcx
     120:   49 89 c4                                    mov r12, rax
     123:   48 8b 43 20                                 mov rax, [rbx+0x20]
     127:   48 83 f8 00                                 cmp rax, 0x0
     12b:   74 19                                       jz 0x146
     12d:   48 c7 c0 00 00 00 00                        mov rax, 0x0
     134:   48 81 c4 38 02 00 00                        add rsp, 0x238
     13b:   41 5c                                       pop r12
     13d:   5d                                          pop rbp
     13e:   41 5d                                       pop r13
     140:   41 5e                                       pop r14
     142:   41 5f                                       pop r15
     144:   5b                                          pop rbx
     145:   c3                                          ret
     146:   4a 8d 44 25 00                              lea rax, [rbp+r12]
     14b:   48 81 c4 38 02 00 00                        add rsp, 0x238
     152:   41 5c                                       pop r12
     154:   5d                                          pop rbp
     155:   41 5d                                       pop r13
     157:   41 5e                                       pop r14
     159:   41 5f                                       pop r15
     15b:   5b                                          pop rbx
     15c:   c3                                          ret
     15d:   48 c7 c0 00 00 00 00                        mov rax, 0x0
     164:   48 81 c4 38 02 00 00                        add rsp, 0x238
     16b:   41 5c                                       pop r12
     16d:   5d                                          pop rbp
     16e:   41 5d                                       pop r13
     170:   41 5e                                       pop r14
     172:   41 5f                                       pop r15
     174:   5b                                          pop rbx
     175:   c3                                          ret
     176:   48 c7 43 20 01 00 00 00                     mov qword [rbx+0x20], 0x1
     17e:   48 c7 43 28 07 00 00 00                     mov qword [rbx+0x28], 0x7
     186:   48 c7 c0 00 00 00 00                        mov rax, 0x0
     18d:   48 81 c4 38 02 00 00                        add rsp, 0x238
     194:   41 5c                                       pop r12
     196:   5d                                          pop rbp
     197:   41 5d                                       pop r13
     199:   41 5e                                       pop r14
     19b:   41 5f                                       pop r15
     19d:   5b                                          pop rbx
     19e:   c3                                          ret

最適化 :tada:

定数伝搬と定数の畳み込み を実装しました。他はやっていませんが、enum とか const とかしてシンボル化した値を伝搬させて畳み込むのは結構効果があります。

Kinx Native - 今後の予定

以下はやりたいですが、全然取り掛かれる気配がないのが問題ですね。やはり焦らず気長に。。。

  • オブジェクトの配列
  • スクリプト関数の呼び出し
  • オブジェクト・プロパティアクセス
  • BigInteger のオブジェクト利用効率化
  • 例外オブジェクトの送出

3. JIT ライブラリ - 手軽に JIT コンパイルを体験

さて、次は JIT ライブラリです。

Kinx では、native 以外にも直接 JIT を楽しめる JIT ライブラリがあります。これはまさしく sljit をラップしたライブラリです(sljit の使い方の例等は「こちら」)。必要なレジスタ数などの自動計算などもあり、直接使うより便利にはしています。

JIT ライブラリに関しては以下に記事を載せています。私個人的にはこれを拡張して色々できると楽しいなー、と思っていますが、なかなか手が付けられませんね。うーん。

以前と同じことを書いても仕方がないので、サンプルを再掲して今後したいことなど。細かい使い方に関しては上記の「JIT ライブラリ」をご参照ください。

JIT ライブラリ・サンプル

こんな感じで、抽象化アセンブラを Kinx 上から手軽に使えます。

using Jit;

var c = new Jit.Compiler();
var entry1 = c.enter();
    var jump0 = c.ge(Jit.S0, Jit.IMM(3));
    c.ret(Jit.S0);
    var l1 = c.label();
    c.sub(Jit.R0, Jit.S0, Jit.IMM(2));
    c.call(entry1);
    c.mov(Jit.S1, Jit.R0);
    c.sub(Jit.R0, Jit.S0, Jit.IMM(1));
    c.call(entry1);
    c.add(Jit.R0, Jit.R0, Jit.S1);
    c.ret(Jit.R0);

jump0.setLabel(l1);
var code = c.generate();

for (var i = 1; i <= 42; ++i) {
    var tmr = new SystemTimer();
    var r = code.run(i);
    System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r);
}

Jit.Compiler オブジェクトを作って、enter で関数エントリを作り、色々レジスタをいじくりまわして ret するコードを書きます。で、実行するときは generate() して run()、となります。generate() して dump() とすると、アセンブル・リストを見ることもできます。

上記は、よく見ないと分からないかもしれませんが(ご想像の通り)フィボナッチ数列を求めるプログラムです。アセンブル・リストを出してみましょう。実行せずに code.dump() とすると以下が表示されます。

       0:   53                                          push rbx
       1:   41 57                                       push r15
       3:   41 56                                       push r14
       5:   48 8b df                                    mov rbx, rdi
       8:   4c 8b fe                                    mov r15, rsi
       b:   4c 8b f2                                    mov r14, rdx
       e:   48 83 ec 10                                 sub rsp, 0x10
      12:   48 83 fb 03                                 cmp rbx, 0x3
      16:   73 0d                                       jae 0x25
      18:   48 89 d8                                    mov rax, rbx
      1b:   48 83 c4 10                                 add rsp, 0x10
      1f:   41 5e                                       pop r14
      21:   41 5f                                       pop r15
      23:   5b                                          pop rbx
      24:   c3                                          ret
      25:   48 8d 43 fe                                 lea rax, [rbx-0x2]
      29:   48 89 fa                                    mov rdx, rdi
      2c:   48 89 c7                                    mov rdi, rax
      2f:   e8 cc ff ff ff                              call 0x0
      34:   49 89 c7                                    mov r15, rax
      37:   48 8d 43 ff                                 lea rax, [rbx-0x1]
      3b:   48 89 fa                                    mov rdx, rdi
      3e:   48 89 c7                                    mov rdi, rax
      41:   e8 ba ff ff ff                              call 0x0
      46:   49 03 c7                                    add rax, r15
      49:   48 83 c4 10                                 add rsp, 0x10
      4d:   41 5e                                       pop r14
      4f:   41 5f                                       pop r15
      51:   5b                                          pop rbx
      52:   c3                                          ret

ここでは Linux ですが、Windows だと使われるレジスタが変わります。そう、例の呼出規約の話です。型チェックなどがバッサリ無い分、そして書いた通りのコードが出ているので(大体のところ)非常に綺麗ですね。スタックの操作とかレジスタの退避とかは必要な分だけ自動的に行われるので、その辺りもラクチンです。

JIT ライブラリ・ベンチマーク

では、ベンチマークを載せてみましょう。fib(42) を再帰で計算させた結果です。

言語 版数 User 時間
Kinx(Jit-Lib) 0.10.0 0.828
HHVM 3.21.0 2.227
Kinx(native) 0.10.0 2.250
PyPy 5.10.0 3.313
PHP 7.2.24 11.422
Ruby 2.5.1p57 14.877
Kinx 0.10.0 27.478
Python 2.7.15+ 41.125

JIT ライブラリは結構使い方次第で役に立ちそうだとは思ってはいるんですけどね。何気に native もいい結果なので、目下、自己満足中です。しかし Ruby 速いなー。どんどん速くなるなー。いいナー3。それ以上に PHP が速いけど。

JIT ライブラリ・今後の予定

そんな JIT ライブラリとしては、以下を計画したいと思っています。

  • 文字列操作機能(Kinx オブジェクトも使用可能かつある程度抽象化した形で)。
  • C 関数のお手軽呼び出し機能。

C 関数をお手軽に呼べるようにできれば、かなり拡張できるようになります。具体的には dll からエントリ・ポイントを取得してコールするようにすれば良さそうなので、やればできそうです。

あー、やりたいことは沢山あります。焦らず、気長に。

4. MIR 感想

そしてお待ちかねの MIR です。Windows でも Linux でもそれなりには行けました。ここでは Linux での内容をお伝えします。オリジナルのリポジトリは以下です。

Build してみよう

では早速 MIR にトライしてみましょう。clone します。

$ git clone https://github.com/vnmakarov/mir.git
Cloning into 'mir'...
remote: Enumerating objects: 435, done.
remote: Counting objects: 100% (435/435), done.
remote: Compressing objects: 100% (231/231), done.
remote: Total 10386 (delta 280), reused 336 (delta 204), pack-reused 9951
Receiving objects: 100% (10386/10386), 3.59 MiB | 3.08 MiB/s, done.
Resolving deltas: 100% (6372/6372), done.
Checking out files: 100% (1370/1370), done.

$ cd mir

$ ls
CMakeLists.txt      c2mir              mir-gen-ppc64.c   mir-interp.c  mir.h
HOW-TO-PORT-MIR.md  check-threads.sh   mir-gen-s390x.c   mir-ppc64.c   mir2c
LICENSE             include            mir-gen-stub.c    mir-reduce.h  mir3.svg
MIR.md              llvm2mir           mir-gen-x86_64.c  mir-s390x.c   mirall.svg
Makefile            mir-aarch64.c      mir-gen.c         mir-tests     real-time.h
README.md           mir-bin-driver.c   mir-gen.h         mir-utils     sieve.c
adt-tests           mir-bitmap.h       mir-gen.svg       mir-varr.h
c-benchmarks        mir-dlist.h        mir-hash.h        mir-x86_64.c
c-tests             mir-gen-aarch64.c  mir-htab.h        mir.c

cmake がありますが、Makefile がありますので Linux(実際は WSL)では直接 make します。

$ make
cc -std=gnu11 -Wno-abi -fsigned-char -fno-tree-sra -fno-ipa-cp-clone -DMIR_PARALLEL_GEN -c -O3 -g -DNDEBUG -o mir.o mir.c
cc -std=gnu11 -Wno-abi -fsigned-char -fno-tree-sra -fno-ipa-cp-clone -DMIR_PARALLEL_GEN -c -O3 -g -DNDEBUG -o mir-gen.o mir-gen.c
cc -std=gnu11 -Wno-abi -fsigned-char -fno-tree-sra -fno-ipa-cp-clone -DMIR_PARALLEL_GEN -O3 -g -DNDEBUG -I. mir-gen.o c2mir/c2mir.c c2mir/c2mir-driver.c mir.o -lm -ldl -lpthread -o c2m
cc -std=gnu11 -Wno-abi -fsigned-char -fno-tree-sra -fno-ipa-cp-clone -DMIR_PARALLEL_GEN -I. -O3 -g -DNDEBUG -o m2b mir.o mir-utils/m2b.c
cc -std=gnu11 -Wno-abi -fsigned-char -fno-tree-sra -fno-ipa-cp-clone -DMIR_PARALLEL_GEN -I. -O3 -g -DNDEBUG -o b2m mir.o mir-utils/b2m.c
cc -std=gnu11 -Wno-abi -fsigned-char -fno-tree-sra -fno-ipa-cp-clone -DMIR_PARALLEL_GEN -I. -O3 -g -DNDEBUG -o b2ctab mir.o mir-utils/b2ctab.c

あっさりと、うまくいきました。

ベンチマークしてみよう

では恒例のフィボナッチ、行ってみましょう。JIT ライブラリのときと同じ条件で fib(42) で実行し、結果をマージしてみます。

int printf(const char *fmt, ...);
#define N 42

int fib(int n)
{
    if (n < 3) return n;
    return fib(n-2) + fib(n-1);
}

int main(void)
{
    printf("fib %d = %d\n", N, fib(N));
    return 0;
}

gcc と比較してみるため、gcc でもコンパイル。

$ gcc -O0 -o fib_gcc_o0 fib.c
$ gcc -O2 -o fib_gcc_o2 fib.c

さて、結果です。

言語 版数 User 時間 備考
GCC -O2 7.5.0 0.531 コンパイル時間を除く
Kinx(Jit-Lib) 0.10.0 0.828
GCC -O0 7.5.0 1.109 コンパイル時間を除く
c2m -eg - 1.500 全ての関数を最初にコンパイルする方式
c2m -el - 1.750 関数が呼び出される直前にコンパイル、必要な関数のみコンパイル
HHVM 3.21.0 2.227
Kinx(native) 0.10.0 2.250
PyPy 5.10.0 3.313
PHP 7.2.24 11.422
Ruby 2.5.1p57 14.877
Kinx 0.10.0 27.478
Python 2.7.15+ 41.125
c2m -ei - 71.453 全て MIR インタプリタで実行

さすがに GCC -O2 が速いです。といってもコンパイルが無視されていますがこの程度だと 15ms くらいだったのでまぁ良いでしょう。今のところ、GCC の最適化無しでも c2m より速いようですが、コンパイル時間を除くと若干差は縮まるとは思われます。尚、c2m は -O2 がデフォルトのようです。-O0 を付けると遅くなります。元々、gcc とか clang とかはスタートアップとコンパイルフェーズが長いのでそれを回避したい、ということなので、ランタイムは多少遅くても良いでしょう。そういう意味では gcc とは比較したい場所が違いますね。

この MIR、まだプロジェクトは初期のステージとのことなのでこれからと言ったところでしょう。現時点での結果としては、全然悪くないです。

IR 自体まだ仕様が安定してなさそうなのですが、非常に楽しみです。

(その他)MIR での標準ライブラリについて

printf を自分でプロトタイプ宣言してますが、MIR では標準ライブラリを 動的にロードして 実行していました。おお、画期的。こんなやり方できたんですね。目からうろこでした。実際本当にいいのかはよくわかりませんが、このやり方でうまく動いています。

具体的には以下のソースコードです(リンク先が変わってしまったらごめんなさい)。

この技、今度使おう。

今後

いくつか妄想だけでも...

  • Ruby 方式の簡易版 JIT コンパイルを Kinx で実現してみたり。
  • C 拡張スクリプトとして色んな言語の拡張を C の動的コンパイルで実現できるようにしてみたり。
  • 簡単なサンプル自作言語として C へのトランスパイラを作成してみたり。

時間がいくらあっても足りない。

と、いいつつ、MIR を使いたい一心で何か始めます。というか始めてます。ちゃんと記事ができるようになったら紹介できるかもしれません。

おわりに

ここまで読んでくださってありがとうございます。

今後、以下のようなことは考えています。

  • Kinxnative 機能をゆっくりでも育てていく4
  • Kinx の JIT ライブラリを機能拡張する。
  • MIR でチャレンジ精神を発揮する。

言語実装 Advent Calendar ということで、自作言語 Kinx を紹介させていただくと共に、それに関する JIT 関連と最近の興味を記事にしてみました。全部と言わずとも少しでも皆さんの興味にヒットして、何かしらのお役に立てれば幸いです。

また、もし宜しければ Kinx は GitHub にソースコードを公開していますので、Star とか貰えるとやる気出ます(やる気があっても時間がないのはいかんともしがたいが...)。泣き言言ってますが、焦らず、気長に見守っててください。もちろん、最初にも書きましたが、どなたでもどんな内容でも コントリビュート大歓迎 ですので、何かありましたらぜひぜひ宜しくお願い致します。

ではこの辺で、またどこかでお会いしましょう。

改めて、最後までお読みいただき、ありがとうございました。


  1. 全く関係ない話ですが、昔、何かの本で「マシン語は一部で『魔神語』と呼ばれ恐れられている」(←うろ覚え、適当)みたいなことが書いてあって、子供ながらに面白いと思った記憶が多少残ってます(なんだっけなー)。 

  2. 当時(Kinx を作り始めたのは約 1 年前)、これの完成版があったら、もしかしたら最初から全て C 言語にトランスコンパイルする形で設計したかもしれません。というより、Kinx 以前に同じシンタックスで作っていた前身言語は、Kinx 同様 VM 実行させるように実装した後、Clang ベースで別バージョンを作った過去があります。当然のように起動がモッサリしました。MIR はこういうのを避けられそうで Good です。 

  3. ちなみに Kinx は Ruby 2.4.0 と大体同じくらいの実行速度なレベルです。VM 性能は、割とがっつりチューニングしているのでそんなに変わらん気がするのだが。GC の差かな。あまり頑張って調べる気はないのですけど。Kinx の GC は当初から変わらず Stop The World での Mark & Sweep を堅持しているので。しかし、Ruby は1.8.7 の頃とは全く比較になりませんね。 

  4. Kinx 自体はデバッガ作りたいとか、Language Server をサポートしたい(主に VSCode)とか、パッケージマネージャーを用意したいとか、マニュアルをコンプリートさせたいとか、GUI ライブラリ取り揃えたいとか、やりたいことはテンコ盛りなのですが…。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby 基礎知識 #1

プログラミング言語のRubyを学んできたので、アウトプットしていきたいと思います。

Rubyとは何か?

Rubyとは、Matzことまつもと ゆきひろ氏が作成した1995年に一般公開されたオブジェクト指向スクリプト言語。
スクリプト言語とは、可読性が高く比較的簡単に書きやすい言語の総称(正式な定義は存在しないらしい)。

Rubyを使用してできること

Rubyは汎用性の高いプログラミング言語となるため、様々なサービスを作ることができる。
「機械学習」と「スマートフォンアプリ制作」は唯一、Rubyが向いていない分野。
使用しているサービスの具体例 : クックパッド・Hulu・Twitter

Rubyを使用するには

  • 一般的に、自分のPCで開発をする環境のことをローカル環境、開発する環境を用意することを環境構築と呼ぶ。
    ローカル環境を構築することで、自分のPCさえあればいつでもどこでもRubyのコードを書き、自分の好きなプログラムを開発することが可能。

  • 開発に必要なツールは、「テキストエディタ」「ターミナル」の2つを用意する。
    テキストエディタは、外部からインストール(私は"VS code")を行う。
    ターミナルは、Macの場合標準でインストールされている。

  • テキストエディタをインストール後、.rbと末尾につけたファイルを作成。

  • ターミナル上で作成したディレクトリ(ファイル)へと移動し、ruby [実行したいRubyファイルのパス]を入力。

+ ターミナル上で実行されれば、Rubyファイルの実行が完了となる。

最後に

Rubyの部分でも深い知識が必要となるので、定期的にアウトプットし知識を定着させていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Windows・macOS完全対応GitLab開発環境最終版 (2020年12月)

わずか3ヶ月前にmacOSにGitLab開発環境セットアップ (GDK) (2020年9月)という記事を書きました。が、1四半期が経過し状況も変化したので新たに書くことになりました。

tl; dr: GitLab本体やGitaly(バックエンド)をmacOSやWindows、Linuxの各種環境で開発する環境を用意する。

GitLab Development Kit

GitLab Development Kitと呼ばれるGitLab本体を手元のコンピュータで開発できるツールがGitLab開発開始直後より存在していました。
一方、AWSが3年前の今日2017年12月1日(日本時間)に発表したAWS Cloud 9、マイクロソフト傘下GitHubのGitHub Codespaces (現在Early access program中)(旧マイクロソフトVisual Studio Codespaces)(移行ニュースリリース)、そして2020年10月、Google Cloudから発表された新Cloud Shell Editorに代表されるトレンドがありました。
この四半期の間にGDKがクラウドIDEプラットフォームに対応しました。対応しているクラウドIDEはGitpodというサービスです。

Gitpod

https://www.gitpod.io/

GitpodはGitHub(.com)、GitLab(.com)およびBitbucket(.org)に対応するクラウドIDEサービスです。Eclipse Foundation参加のオープンソースプロジェクトEclipse Theiaをベースにしています。2020年8月にはGitpodそのものもオープンソース化されました。Gitpodクラウド版のユーザとしては、無料から始めることができ、無料プランではパブリックリポジトリであれば月50時間までワークスペースを利用できます。

USおよびEUのregionにあるGCP上のKubernetes (GKEなのではないかと推測) に構築されており、ユーザーは16vCPU、59GBメモリの利用ができるようです。なお、以前にあった(そしてドキュメントに記載されている)Asiaのクラスターは既に削除されていて利用できないとのこと。

手順

ここを読むほとんどの人は以下の作業が必要になる。例外はGitLab team memberおよび過去にGitLabへコード貢献があった人(過去のGitLab Hackathon参加者も該当する可能性が高い)。

  1. https://gitlab.com にログインする。アカウントがない場合はSign upから作成する。
  2. https://gitpod.io にログインする。アカウントがない場合はGitLabアカウントでアカウント作成&ログインする。
  3. https://gitlab.com/gitlab-org/gitlab を開いて、Forkボタンを押したのち、自分のユーザ名の右にあるSelectボタンを押す。そして、しばらく待つ(2-10分くらい?)。
  4. 完了したら、表示されたページのWeb IDEの右にある▽を押し、プルダウンメニューからGitpodを選ぶ。そして、もう一度Gitpodを押す。
  5. 7-8分待つ。

Gitpodに関する注意

GitLabログインを通じて、Gitpod側にはクレデンシャルが設定済みなので、git fetch/push/pullの操作が可能になります。gpコマンドとして利用可能です。
GDKターミナルでは /workspace/gitlab-development-kit がカレントディレクトリとして指定されており、gdk コマンドが利用できます。
GitLabターミナルでは /workspace/gitlab がカレントディレクトリとして指定されており、(通常のGDKセットアップと異なり) gdk コマンドが利用できませんので、注意してください。

その他は https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/master/doc/howto/gitpod.md を見てください。

まとめ

すぐにGitLabの開発に参加できるGitLab Development Kit powered by Gitpodについて説明しました。一人でも多くのコードコントリビュータが増えることを願っています。

すぐに試したい?

GitLab Tokyoコミュニティは12月2〜3日Hackathonイベントを日本語・オンラインで実施します。参加するには、こちらの記事に書かれたセットアップが必要となります。開催時間中にセットアップすることも可能です。ぜひご参加ください。

Qiitaの先人たちによる同種の記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Active Storageで画像のサイズを指定したら表示速度が大幅改善したよ

はじめに

問題

  • ページに表示したい画像の量が多くてページの読込に時間がかかる
  • ユーザーの投稿画像を表示するので事前にサイズの調整ができない

環境

  • Active Storage
  • mini_magick
  • image_processing
  • EC2 & S3 利用

考えたこと①

アップロード時にリサイズしたい
=> やり方わからず断念

考えたこと②

読込時にサーバー側でリサイズしたら速くならないかな
=> できた!

参考

こちらの記事を参考にしました。
Active StorageのVariantの指定方法いろいろ

また日本語のリファレンスにはメソッドの細かい説明がなかったので、こちらの英語で書かれたリファレンスを参考にしました。
ActiveStorage::Variant

やはり公式に一度目を通すのは大事ですね。image_processingがないと変換できないようです。なんとなくですが概要を理解できた気がします。

Google翻訳様様です。

手順

具体的な手順は参考のURLからご確認いただければと思います。

やったこと

example.rb
image_tag @hoge.image.variant(combine_options:{gravity: :center, resize:"AxB^", crop:"AxB+0+0"}).processed, class: 'hoge'

variantメソッドを使って

  • リサイズ
  • センターフィット
  • カット

を行いました。サイズはCSSで指定していたのでそれに合わせました。(こうなるとCSSのサイズ表記はいらない、しない方がいいのかな?)

また、processedオプションをつけることで2度目以降の読込はリサイズしたURLを返すことができるのでより速くなるそうです(ちょっとロジックがよくわかなくて自信ないですが…この辺はもっとしっかり勉強しないとダメですね)

結論、このようにCSSにすべて調整させるのではなくサーバーサイドで画像を整えてからフロントに渡すことが大事とのことです。

改善前

サイトトップページがこちら

スクリーンショット 2020-12-01 14.53.21.png

  • 投稿一覧にアバターを載せておりページあたり数十件単位で画像がロードされる
  • 投稿にも任意で画像添付でき、一覧に表示させている

読込完了まで、長い時で10秒以上かかっている時もありました。

サイトパフォーマンスがこちら

スクリーンショット 2020-12-01 13.32.22.png
スクリーンショット 2020-12-01 13.32.36.png

スクショに含め忘れたのですが、読込完了まで4.3秒(たしか)というスコアでした。
あきらかに画像が足を引っ張っているのがわかります。

計測について

今回はPageSpeed Insightsで読込速度を計測しました。

PageSpeed Insightsは「WEB ページの読み込み時間を短くしよう」こちらの記事で教わりました。
サイトパフォーマンスについては本当にこれが正しいのか、という指摘もあるようですが、とりあえず現時点で広く使われているサービスですし、入口としては使って問題ないのかなと思っています。

改善後

スクリーンショット 2020-12-01 13.39.27.png
スクリーンショット 2020-12-01 13.39.41.png

読込完了まで4.0秒になりました。
点数と読込完了時間に関しては期待した程の結果は出ませんでしたが、改善項目から画像が一気に姿を消しました。
これはなかなか素晴らしい改善ではないでしょうか。大満足です。

(ちなみに、PageSpeed InsightsではモバイルとPCで結果が変わるのですが、ここに載せたのはモバイルの結果です。PCでは94点(0.9秒)を記録しています。となると、レスポンシブ関係のフロント記述あるいはフロント&サーバー間の処理が悪いのかもしれません)

おわりに

本当はアップデート時にリサイズして保存したかったのですが、いったん諦めてvariantを使って表示処理を改善してみました。

これはこれで新しい発見があったのでよかったです。

あとはNginxと絡めて処理速度を上げたりできるっぽいので(アップデートの話かな?)落ち着いたら他の方法もいろいろ試していきたいと思います。

✔︎

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Active Storage:画像のサイズを指定したら表示速度が大幅改善したよ

はじめに

問題

  • ページに表示したい画像の量が多くてページの読込に時間がかかる
  • ユーザーの投稿画像を表示するので事前にサイズの調整ができない

環境

  • Active Storage
  • mini_magick
  • image_processing
  • EC2 & S3 利用

考えたこと①

アップロード時にリサイズしたい
=> やり方わからず断念

考えたこと②

読込時にサーバー側でリサイズしたら速くならないかな
=> できた!

参考

こちらの記事を参考にしました。
Active StorageのVariantの指定方法いろいろ

【2020.12.1追記】

改めてログを確認すると"Active Storage's ImageProcessing transformer doesn't support :combine_options, as it always generates a single ImageMagick command. Passing :combine_options will not be supported in Rails 6.1."と警告が…

こちらの書き方はRails 6.1 からは使えなくなるっぽいです
そして解消方法を探したところ次の記事が参考になりましたので掲載致します

Rails 6.0の "Active Storage's ImageProcessing transformer doesn't support :combine_options" という警告に対処する方法

また日本語のリファレンスにはメソッドの細かい説明がなかったので、こちらの英語で書かれたリファレンスを参考にしました。
ActiveStorage::Variant

やはり公式に一度目を通すのは大事ですね。image_processingがないと変換できないようです。なんとなくですが概要を理解できた気がします。

Google翻訳様様です。

手順

具体的な手順は参考のURLからご確認いただければと思います。

やったこと

example.rb
image_tag @hoge.image.variant(combine_options:{gravity: :center, resize:"AxB^", crop:"AxB+0+0"}).processed, class: 'hoge'

variantメソッドを使って

  • リサイズ
  • センターフィット
  • カット

を行いました。サイズはCSSで指定していたのでそれに合わせました。(こうなるとCSSのサイズ表記はいらない、しない方がいいのかな?)

また、processedオプションをつけることで2度目以降の読込はリサイズしたURLを返すことができるのでより速くなるそうです(ちょっとロジックがよくわかなくて自信ないですが…この辺はもっとしっかり勉強しないとダメですね)

結論、このようにCSSにすべて調整させるのではなくサーバーサイドで画像を整えてからフロントに渡すことが大事とのことです。

【2020.12.1追記】

こちらの記述だと先述のとおり Rails 6.1 からはサポートされなくなります。

combine_options がNGのようですね。

よって下記のように書き換えました。

example.rb
image_tag @hoge.image.variant(resize_to_fill: [128, 128]), class: 'hoge'

やっていることは同じです。

processedオプションについてはちょっとわからなかったので、追々調べたいと思います。あまりパフォーマンスに影響していない気もしていて、とりあえず記述消してしまいました。

改善前

サイトトップページがこちら

スクリーンショット 2020-12-01 14.53.21.png

  • 投稿一覧にアバターを載せておりページあたり数十件単位で画像がロードされる
  • 投稿にも任意で画像添付でき、一覧に表示させている

読込完了まで、長い時で10秒以上かかっている時もありました。

サイトパフォーマンスがこちら

スクリーンショット 2020-12-01 13.32.22.png
スクリーンショット 2020-12-01 13.32.36.png

スクショに含め忘れたのですが、読込完了まで4.3秒(たしか)というスコアでした。
あきらかに画像が足を引っ張っているのがわかります。

計測について

今回はPageSpeed Insightsで読込速度を計測しました。

PageSpeed Insightsは「WEB ページの読み込み時間を短くしよう」こちらの記事で教わりました。
サイトパフォーマンスについては本当にこれが正しいのか、という指摘もあるようですが、とりあえず現時点で広く使われているサービスですし、入口としては使って問題ないのかなと思っています。

改善後

スクリーンショット 2020-12-01 13.39.27.png
スクリーンショット 2020-12-01 13.39.41.png

読込完了まで4.0秒になりました。
点数と読込完了時間に関しては期待した程の結果は出ませんでしたが、改善項目から画像が一気に姿を消しました。
これはなかなか素晴らしい改善ではないでしょうか。大満足です。

(ちなみに、PageSpeed InsightsではモバイルとPCで結果が変わるのですが、ここに載せたのはモバイルの結果です。PCでは94点(0.9秒)を記録しています。となると、レスポンシブ関係のフロント記述あるいはフロント&サーバー間の処理が悪いのかもしれません)

おわりに

本当はアップデート時にリサイズして保存したかったのですが、いったん諦めてvariantを使って表示処理を改善してみました。

これはこれで新しい発見があったのでよかったです。

あとはNginxと絡めて処理速度を上げたりできるっぽいので(アップデートの話かな?)落ち着いたら他の方法もいろいろ試していきたいと思います。

✔︎

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ターミナル で文字化けしたら Git Bash を使おう

はじめに

初心者ですが、Rails Girls Japan Advent Calendar 2020 の初日を書きます。

記事内容とは直接関係ないのですが、今年は Ruby技術者認定試験 Silver 合格Ruby技術者認定試験 Gold 合格 とRuby漬けの年でした。

しかしながら、こんなショボい失敗を続けています。自責の念
良い子の皆さんは真似をしないように。

VSCode で文字化け

cmd.exe
C:\Users\user\Documents\GitHub\baukis2>bundle exec rails db:create
FATAL:  ���[�U"superrino"�̃p�X���[�h�F�؂Ɏ��s���܂���
Couldn't create 'baukis2_development' database. Please check your configuration.

rails db:createしようとしただけなのにエラー、しかも文字化け。

文字コード確認

cmd.exe
C:\Users\user\Documents\GitHub\baukis2>chcp
現在のコード ページ: 932

chcpで文字コードの確認及び変更ができます。

文字コード番号 文字コード
932 Shift_JIS
65001 UTF-8

文字化け治らず

cmd.exe
C:\Users\user\Documents\GitHub\baukis2>chcp 65001
Active code page: 65001

C:\Users\user\Documents\GitHub\baukis2>bundle exec rails db:create
FATAL:  ���[�U"superrino"�̃p�X���[�h�F�؂Ɏ��s���܂���

あれれ、Rubyって内部コードがUTF-8じゃなかったっけ。

Git Bash で解決

rails をやっている方はGit 入っていると思います。

Git.Bash
user@DESKTOP-JJHPP4A MINGW64 ~/Documents/GitHub/baukis2 (master)
$ bundle exec rails db:create
FATAL:  ユーザ"superrino"のパスワード認証に失敗しました

ちゃんと表示されました。

まとめ

皆さん、よいクリスマス、よいお年をお過ごしください

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kinx 基礎文法最速マスター

この記事は「令和時代の基礎文法最速マスター Advent Calendar 2020」 20日目の記事です。

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。ご存じない方は下記をご参照ください。

これまでの記事から基礎文法的なものを抜粋し、最新の情報にしてまとめました。元々かつての「基礎文法最速マスター」の存在は知っており、当初よりそれに倣って記事を書いてみていた、ということで「それをまとめてみたら結構行けんじゃね?」的な感覚で軽い気持ちで急遽エントリーしてみました1

プログラム基礎

hello, world

プログラムはトップレベルから記述可能。

hello.kx
System.println("hello, world.");

hello.kx という名前で保存し、以下のように実行.

$ ./kinx hello.kx
hello, world.

コメント

コメントは C/C++ 形式と Perl のような # 形式と両方利用可能。

/* Comment */
// Comment
# Comment

変数宣言

変数宣言は var で宣言する。

var a = 10;

初期化子を使って初期化するが、初期化子を書かなかった場合は null となる。ちなみに Kinx においては nullundefined は同じ意味(賛否あると思うが)。どちらも a.isUndefined が true となる。

データ型一覧

Kinx は動的型付け言語だが、内部に型を持っている。

Type CheckProperty Example Meaning
Undefined isUndefined null 初期化されていない値。
Integer isInteger, isBigInteger 100, 0x02 整数。演算では自動的に Big Integer と相互変換される。
Double isDouble 1.5 実数。
String isString "aaa", 'bbb' 文字列。
Binary isBinary <1,2,3> バイナリ値。バイトの配列。要素は全て 0x00-0xFF に丸められる。
Array isArray, isObject [1,a,["aaa"]] 配列。扱える型は全て保持可能。
Object isObject { a: 1, b: x } JSON のようなキーバリュー構造。
Function isFunction function(){},
&() => expr
関数。

Undefined 以外であれば isDefined で true が返る。

尚、真偽値としての truefalse は整数の 1、0 のエイリアスでしかないのに注意。
Boolean 型というのは特別に定義されていないが、オブジェクトとして真偽を表す TrueFalse という定数が定義されているので、整数値とどうしても区別したい場合はそちらを使用する。

System.println(True ? 1 : 0);   // 1
System.println(False ? 1 : 0);  // 0

ブロックとスコープ

ブロックはスコープを持ち、内側で宣言された変数は外側のブロックからは参照不可。同じ名前で宣言した場合、外側ブロックの同名変数は隠蔽される。

var a = 10;
{
    var a = 100;
    System.println(a);
}
System.println(a);

この辺が「見た目は JavaScript」であっても中身は JavaScript ではない感じの部分。というか、JavaScript のスコープは変態過ぎて使いづらい(今時は TypeScript なのでこういうのも今は昔の話ですね)。

名前空間

名前空間として、namespace を使用可能。名前空間は オブジェクト であり、名前空間内で宣言されたクラス、モジュールは名前空間オブジェクトに設定されます。ただし、定数等は名前空間オブジェクトには自動的に設定されないため、自分で設定する必要があります。

namespace N {
    class A {
        ...
    }

    const X = 10;
    N.X = 100;

    var a = new A(); // OK
    ...
}

// var a = new A(); // エラー
var a = new N.A(); // OK

// System.println(X); // エラー
System.println(N.X); // OK

名前空間はネストできます。

namespace A {
namespace B {

    class X { ... }

} // namespace B

    var x = new B.X(); // OK

} // namespace A

var x = new A.B.X(); // OK

式(エクスプレッション)は、以下の優先順位で四則演算、関数呼び出し、オブジェクト操作等が可能。

# 要素 演算子例 評価方向
1 要素 変数, 数値, 文字列, ... -
2 後置演算子 ++, --, [], ., () 左から右
3 前置演算子 !, +, -, ++, -- 左から右
4 パターンマッチ =~, !~ 左から右
5 べき乗 ** 右から左
6 乗除 *, /, % 左から右
7 加減 +, - 左から右
8 ビットシフト <<, >> 左から右
9 大小比較 <, >, >=, <= 左から右
10 等値比較 ==, != 左から右
11 ビットAND & 左から右
12 ビットXOR ^ 左から右
13 ビットOR | 左から右
14 論理AND && 左から右
15 論理OR || 左から右
16 三項演算子 ? :, function(){} 右から左
17 代入演算子 =, +=, -=, *=. /=. %=, &=, |=, ^=, &&=, ||= 右から左

いくつか特徴を以下に示す。

演算

演算結果によって型が自動的に結果に適応していく。3/21.5 になる。

num = 3 + 2;    // 5
num = 3 - 2;    // 1
num = 3 * 2;    // 6
num = 3 / 2;    // 1.5
num = 3 % 2;    // 1

インクリメント・デクリメント

前置形式・後置形式があり、C と同様。

var a = 10;
System.println(a++);    // 10
System.println(++a);    // 12
System.println(a--);    // 12
System.println(--a);    // 10

データ型

数値

整数、実数

整数、実数は以下の形式。整数では可読性向上のため任意の場所に _ を挿入可能。_ は単に無視される。

var i = 2;
var j = 100_000_000;
var num = 1.234;

文字列

基本

ダブルクォートとシングルクォートの両方が使えるが、エスケープしなければならないクォート文字が異なるだけでどちらも同じ意味になる。

var a = "\"aaa\", 'bbb'";
var b = '"aaa", \'bbb\'';
System.println(a == b ? "same" : "different");  // same

内部式

%{...} の形式で内部に式を持つことができる。

for (var i = 0; i < 10; ++i) {
    System.println("i = %{i}, i * 2 = %{i * 2}");
}
// i = 0, i * 2 = 0
// i = 1, i * 2 = 2
// i = 2, i * 2 = 4
// i = 3, i * 2 = 6
// i = 4, i * 2 = 8
// i = 5, i * 2 = 10
// i = 6, i * 2 = 12
// i = 7, i * 2 = 14
// i = 8, i * 2 = 16
// i = 9, i * 2 = 18

フォーマッタ

文字列に対する % 演算子はフォーマッタ・オブジェクトを作成する。

var fmt = "This is %1%, I can do %2%.";
System.println(fmt % "Tom" % "cooking");

%1%1 はプレースホルダ番号を示し、% 演算子で適用した順に合わせて整形する。適用場所が順序通りであれば、C の printf と同様の指定の仕方も可能。さらに、C の printf と同じ指定子を使いながら同時にプレースホルダも指定したい場合は、$ の前に位置指定子を書き、$ で区切って記述する。例えば、16進数で表示したい場合は以下のようにする。

var fmt = "This is %2%, I am 0x%1$02x years old in hex.";
System.println(fmt % 27 % "John");
// This is John, I am 0x1b years old in hex.

フォーマッタ・オブジェクトに後から値を適用していく場合は、%= 演算子で適用していく。

var fmt = "This is %1%, I can do %2%.";
fmt %= "Tom";
fmt %= "cooking";
System.println(fmt);

実際のフォーマット処理は、System.println 等で表示するとき、文字列との加算等が行われるとき、に実際に実行される。

Raw 文字列

文字列内部ではなく、%{...} で文字列を記載することで Raw 文字列を作成することが可能。%-{...} を使うと、先頭と末尾の改行文字をトリミングする。ヒアドキュメントのようにも使えるので、ヒアドキュメントはサポートしていない。また、%<...>%(...)%[...] を使うこともできる。

var a = 100;
var b = 10;
var str = %{
This is a string without escaping control characters.
New line is available in this area.
{ and } can be nested here.
};
System.println(str);
var str = %-{
This is a string without escaping control characters.
New line is available in this area.
But newlines at the beginning and the end are removed when starting with '%-'.
};
System.println(str);

\ でエスケープする必要があるのは、内部式を使う場合 %{% に対するものと、ネストした形にならないケースでのクォート文字である {} に対するものだけとなる。

また、カッコは対応する閉じカッコでクォートするが、以下の文字を使ったクォートも可能である。その場合は、開始文字と終了文字は同じ文字となる。例えば、%|...| のような形で使用する。

  • |, !, ^, ~, _, ., ,, +, *, @, &, $, :, ;, ?, ', ".

正規表現リテラル

正規表現リテラルは /.../ の形式で使う。リテラル内の /\ でエスケープする必要がある。以下が例。

var a = "111/aaa/bbb/ccc/ddd";
while (group = (a =~ /\w+\//)) {
    for (var i = 0, len = group.length(); i < len; ++i) {
        System.println("found[%2d,%2d) = %s"
            % group[i].begin
            % group[i].end
            % group[i].string);
    }
}

/ を多用するような正規表現の場合、%m プレフィックスを付け、別のクォート文字を使うことで回避できる。例えば %m(...) といった記述が可能。これを使って上記を書き直すと、以下のようになる。

var a = "111/aaa/bbb/ccc/ddd";
while (group = (a =~ %m(\w+/))) {
    for (var i = 0, len = group.length(); i < len; ++i) {
        System.println("found[%2d,%2d) = %s"
            % group[i].begin
            % group[i].end
            % group[i].string);
    }
}

尚、正規表現リテラルを while 等の条件式に入れることができるが注意点があるので補足しておく。例えば以下のように記述した場合、str の文字列に対してマッチしなくなるまでループを回すことができる(group にはキャプチャ一覧が入る)。その際、最後のマッチまで実行せずに途中で break 等でループを抜けると正規表現リテラルの対象文字列が次回のループで正しくリセットされない、という状況が発生する。

while (group = (str =~ /ab+/)) {
    /* block */
}

正規表現リテラルがリセットされるタイミングは以下の 2 パターン。

  • 初回(前回のマッチが失敗して再度式が評価された場合を含む)。
  • str の内容が変化した場合。

将来改善を検討するかもしれないが、現在は上記の制約があることに注意。

配列

配列は任意の要素を保持するリスト。インデックスでアクセスできる。またインデックスに負の数を与えることで末尾からアクセスすることもできる。

var a = [1,2,3];
var b = [a, 1, 2];
System.println(b[0][2]);    // 3
System.println(a[-1]);      // 3

配列構造は左辺値で使用すると右辺値の配列を個々の変数に取り込むことが可能。これを使用して値のスワップも可能。

[a, b] = [b, a];    // Swap

スプレッド(レスト)演算子を使っての分割も可能。

[a, ...b] = [1, 2, 3, 4, 5];
// a = 1
// b = [2, 3, 4, 5]

尚、宣言と同時に以下の書き方もできる。

var a = 3, b = [4], x = 3, y = [4];
{
    var [a, ...b] = [1, 2, 3, 4, 5];
    // a = 1
    // b = [2, 3, 4, 5]
    [x, ...y] = [1, 2, 3, 4, 5];
    // x = 1
    // y = [2, 3, 4, 5]
    [z] = [1, 2, 3, 4, 5];
    // okay z = 1, but scoped out...
}
System.println("a = ", a);      // 3
System.println("b = ", b[0]);   // 4
System.println("x = ", x);      // 1
System.println("y = ", y[0]);   // 2

バイナリ

バイナリはバイト配列であり、<...> の形式で記述する。全ての要素は 0x00 ~ 0xFF の範囲にアジャストされ、配列のようにアクセス可能。

バイナリと配列は相互にスプレッド演算子で分割、結合することが可能。

var bin = <0x01, 0x02, 0x03, 0x04>;
var ary = [...bin];
    // ary := [1, 2, 3, 4]

var ary = [10, 11, 12, 257];
var bin = <...ary>;
    // bin := <0x0a, 0x0b, 0x0c, 0x01>

ただし、バイナリになった瞬間に 0x00-0xFF に丸められるので注意。

オブジェクト

いわゆる JSON。ただし、ソースコード上のキー文字列に対してクォートする必要は無い(しても良い)。

var a = { a: 100 };
a.b = 1_000;
a["c"] = 10_000;
System.println(a.a);        // 100
System.println(a.b);        // 1000
System.println(a.c);        // 10000

内部的に実はオブジェクトと配列は同じであり、両方の値を同時に保持できる。

var a = { a: 100 };
a.b = 1_000;
a["c"] = 10_000;
a[1] = 10;
System.println(a[1]);       // 10
System.println(a.a);        // 100
System.println(a.b);        // 1000
System.println(a.c);        // 10000

宣言と同時にオブジェクトの特定の要素を同じ名前の変数に代入が可能。

var obj = { x: 100, y: 200, z: 300 }; 
var { x, y } = obj; // x = 100, y = 200

文・制御構文

文(ステートメント)として、宣言、代入、continuebreakreturnthrowyield、および制御構文として if-elsewhiledo-whileforfor-inswitch-casetry-catch-finally が、使用可能。if-else の接続はぶら下がり構文で使用する。

宣言文

var で宣言する。初期化子で初期化も可能、またカンマで区切って複数同時に宣言することも可能。

var a;
var a = 0, b = 100;

型を指定することもできるが、現時点で native でしか使用されない。スクリプト上では単に無視される。

native 関数に関しては後述する。

将来的には型が指定されていた場合は型チェックして実行前にエラーさせるようにするのが良い使い方だと思う(動的型付け言語の型アノテーションによる事前チェック、といった意味合い)。逆に native では指定しなければ全て int と見なされる。

型を指定する場合は変数名の後に記述する。

var a:dbl;
var a:int = 0, b:dbl = 100.0;
native<dbl> test(a:dbl, b:dbl) {
    /* function body */
}

宣言文では、配列形式とオブジェクト形式で任意の要素の値を取得が可能。

var ary = [1, 2];
var obj = { x: 100, y: 200 }; 

var [a, b] = ary;   // a = 1, b = 2
var { x, y } = obj; // x = 100, y = 200

代入文

代入文は普通の式文。代入は右辺から評価される。

a = b = 10;

上記では b = 10 が先に評価され、その結果が a に代入される。

配列を左辺値に指定して各要素の取得が可能。

[a, b] = [1, 2];   // a = 1, b = 2
[b, a] = [a, b];   // スワップ

continue

ループ先頭に戻る。正確にはループ条件式の直前に戻る。ただし、for 文の場合は第三フィールド(カウンタ更新の部分、何て言うんだ?)の直前に戻る。

continue はラベル指定が可能。continue LABELLABEL の示すブロックの先頭(ブロックがループの場合は上記の場所)に制御が戻る。また、continue は if 修飾が可能。continue if (expression) の形で条件を指定することができる。

continue;
continue LABEL;

break

ループを抜ける。正確にはループ・ブロックの直後に進む。

break はラベル指定が可能。break LABELLABEL の示すブロックの末尾に制御が進む。また、break は if 修飾が可能。break if (expression) の形で条件を指定することができる。

break;
break LABEL;

return

関数を抜ける。正確にはスタックをクリアし、復帰値を設定して呼び出し元の次の命令に進む。

return のみを指定した場合は暗黙的に null が返る。また、return は if 修飾が可能。return expression if (expression) の形で条件を指定することができる。

また、関数が Fiber として定義されていたた場合、一旦リターンすると次の呼び出しで FiberException 例外が発生する。実は catch してもう一回呼ぶと再度最初から実行できるが、この仕様で良いのかはわからない。

return; // same as `return null;`
return expression;

throw

例外を送出する。例外システムは貧弱だが実用できないわけではない。

例外オブジェクトは type()what() というメソッドを持ち、型とメッセージを取得できる。がしかし型によって捕捉する例外を区別したりできない。キャッチしてから type() で確認する感じ。今のところ、SystemExceptionFiberExceptionRuntimeException というのがあるが、ユーザーが一般に投げられる例外は RuntimeException

型によって区別できた方が良いのかな。個人的には例外はあくまで「例外」であって、エラー処理が適切にできれば良いのだが、ご意見ご要望をお待ちしております。

また、throw も if 修飾が可能。throw expression if (expression) の形で条件を指定することができる。

ちなみに catch 節の中では throw 単独での利用が可能。この場合、catch した例外オブジェクトをそのまま再送出する。

throw;
throw expression;

yield

Fiber で一旦ホスト側に処理を戻すために使用。値を返すことも可能。ホスト側から再度 resume(args) 返ってきた値 args を受け取ることも可能。その際、引数は配列の形でまとまってくるので、個別に受信したい場合はスプレッド(レスト)演算子を使って以下のように受け取る。

[a, ...b] = yield;

上記の例では最初の引数を a で受け取り、残りの引数を配列として b が受け取る。また、yield も if 修飾が可能。yield expression if (expression) の形で条件を指定することができる。

通常は以下の形式。

yield;
yield expression;

Fiber#resume(args) の復帰値を受け取る場合は以下の形式。

var fiber = new Fiber(&{
    a = yield;       // a = [10, 20, 30]
    [a1] = yield;    // a1 = 10
})
fiber.resume(); // first call.
fiber.resume(10, 20, 30);
fiber.resume(10, 20, 30);

尚、今後触れるつもりだが、&{...} はブロックを渡しているように見えて実際は &() => {...} と同じ意味。具体的には引数無しの無名関数オブジェクトを簡潔に表現できるようにしたもの。ブロックを渡しているように見えていいなと勝手に思ってこうしてみた。

if-else

if (expression) block else block の形で使用。複数条件を連続させる場合は以下のようにぶら下がり構文を使用する。

if (expression) {
    /* block */
} else if (expression) {
    /* block */
} else {
    /* block */
}

while

while は条件判断をループの最初で行うループ構造を示す。以下が例だが詳細は難しくないため省略。

while (expression) {
    /* block */
}

do-while

do-while は条件判断をループの最後で行うループ構造を示す。従って、必ず 1 度はループ・ブロックが処理される。詳細は省略。

do {
    /* block */
} while (expression);

for

for は「初期化」「条件式」「更新部」(それぞれ何て言うんだ?)の 3 つのフィールドを持つ制御構造。初期化部では var を指定して for ブロックのスコープ内だけで有効な変数の宣言が可能。詳細は省略。

for (initialize; condition; update) {
    /* block */
}

for-in

for in もサポート。

for (var e in collection) {
    ...
}

変数 evar は付けなくても良いが、外側のスコープに同じ名前の変数があった場合はそちらにバインドされる。変数 e のスコープを限定したい場合は var をつける。collection には以下のオブジェクトを指定できる。

  • Range オブジェクト
  • 配列(Array)
  • オブジェクト(連想配列)

それぞれ以下のような動作をする。

Range オブジェクト

for (var e in 2..10) {
    System.println(e);
}

結果。

2
3
4
5
6
7
8
9
10

ちなみに以下のようにすると終端がないため無限ループする。

for (var e in 2..) {
    System.println(e);
}

結果は以下の通り。開始は指定した 2 から。

2
3
4
...
1021
1022
1023
...

配列(Array)

こんな感じ。まぁ普通。

for (var e in [2,3,4,5,6,7,8,9,10]) {
    System.println(e);
}

結果。

2
3
4
5
6
7
8
9
10

配列で受け取る。

Kinx
for ([i, j] in [[1,2], [3,4], [5,6]]) {
    System.println("[%{i}, %{j}]");
}

結果。

[1, 2]
[3, 4]
[5, 6]

もう一つ、Ruby と同じ動き。

Kinx
for ([i, j] in [1, 2, 3]) {
    System.println([i, j].toJsonString());
}

結果。[1, 2][3, null] を期待するかもしれないがそうはならない。

[1,null]
[2,null]
[3,null]

オブジェクト(連想配列)

オブジェクトの場合、キーとバリューが配列の形で取り出される。

Kinx
var obj = { a: 10, b: 100 };
for ([key, value] in obj) {
    System.println("key: %{key} => value: %{value}");
}

結果はこうなる。

key: a => value: 10
key: b => value: 100

switch-case

switch-case は悪名高いフォールスルー。だが、C プログラマとしてはフォールスルーじゃないと逆に変な感じでムズムズする。想像してみよう。break が無いと逆に 「次に行く感」 を感じてしまうところに根本原因があると思う。ここは馴染んだ道具に合わせてフォールスルーだ。ちゃんと break 書こうぜ。

おそらく解としては case-when のような別の構文も用意しておく、または break も書くが fallthrough も書かせる、でしょうか。要望が多ければ対応しようかなー、とは思います。

ちなみに C 言語同様、default は最後に置かなくてもいいんだ。さらに数値以外も case に書ける。こんな感じ。

var array = [1,2,3,4,5,6,7,8,9,10];
function switchTest(n) {
    switch (n) {
    case 1:     System.println(n); break;
    case 2:     System.println(n); break;
    case 3:     System.println(n); break;
    case 4:     System.println(n); break;
    case 5:     System.println(n); break;
    case 6:     System.println(n); break;
    case 7:     System.print(n, ", "); /* fall through */
    case 8:     System.println(n); break;
    case 100:   System.println(n); break;

    default:
        System.println("default");
        break;
    case array.length():
        System.println("array-length:%{n}");
        break;
    case "aaa":
        System.println(n);
        break;
    case "bbb":
        System.println(n);
        break;
    }
}
0.upto(100, function(i) {
    System.print("%{i} => ");
    switchTest(i);
});

尚、native でも switch-case はサポートしているが、ラベルは整数(と整数式)のみ。

try-catch-finally

try-catch-finally は例外を扱うための構文。以下のように使用する。だいたい分かってもらえそうなので、詳細は省略。尚、catch (e)(e) は省略できない。最近の JavaScript では省略できるっぽいので、できるようにするか検討中。

try {
    /* block */
} catch (e) {
    /* block */
} finally {
    /* block */
}

native でもサポートしているが、実際の例外オブジェクトを投げることができない制約がある(Type Mismatch とか Divide By Zero とかがスローされる可能性があるのでソレ用)のと、スタックトレースが保持されないという制約がある。これらは何とかなりそうな気もするので、今後の検討課題。

関数とクロージャ

見た目は JavaScript の代表。ただ、最近の JavaScript はこんな感じじゃなくなってきているな。変わりすぎだよ。

関数

通常の関数

構造化プログラミングの時代から、共通の処理はひとつにまとめることは行われてきた。オブジェクト指向全盛の今でもそれは変わらない。さらに、現代の関数は単に処理が共通化できるだけではないのがミソだ。関数は静的スコープ(レキシカル・スコープ)を持ち、クロージャが実現可能。クロージャに関しては後述する。

function add(a, b) {
    return a + b;
}
System.println(add(1, 2));  // 3

引数を受け取り、色々とホゲホゲして値を返すことができる。

引数は、今どきの JavaScript で超絶便利になったスプレッド演算子によって分解、統合が可能。まるで JavaScript の話をしているみたいだが、Kinx の話だ。この機能によって可変引数なんかも実現できる。

function toArray(...a) {
    var b = [...a]; // copy
    return b + [3];
}
System.println(toArray(1, 2).join(', '));  // "1, 2, 3"

ちなみにこのサンプルでやっていることに意味は無いので...、機能説明用。

また、関数の引数に配列、オブジェクトの形式での値取得も可能。

function sample([a, b], {x, y}) {
    System.println("a = ", a);  // a = 1
    System.println("b = ", b);  // b = 2
    System.println("x = ", x);  // x = 300
    System.println("y = ", y);  // y = 400
}
sample([1,2,3,4,5], {a:100, b:200, x:300, y:400, z:500});

これは実は何気に便利だったりする。

再帰呼び出しも可能。ただし、末尾再帰の最適化はやってない。やるのは簡単だと思う。尚、実際やるとしても実行時にしか判断できないのと、スタックトレースが正しく取得できない問題がある(そういうものだと思えばいいのかもしれない)。

function fib(n) {
    if (n < 3) return n;
    return fib(n-2) + fib(n-1);
}

System.println("fib(34) = ", fib(34));  // fib(34) = 9227465

native 関数

Kinx には native という必殺技がある。先ほどのフィボナッチ数列のソースコードで functionnative に変えてみよう。

native fib(n) {
    if (n < 3) return n;
    return fib(n-2) + fib(n-1);
}

System.println(fib($$[1].toInt()));

たったこれだけのこの軽微な修正がどんな影響を与えるか、ベンチマークの結果を示そう(2020/11/11 現在)。5 回実行して一番速かった(user)ものを掲載している。

言語 fib(34) fib(38) fib(39)
Kinx 0.15.2(native) 0.047 0.313 0.500
Ruby 2.5.1p57 0.344 2.234 3.672
Kinx 0.15.2 0.594 4.344 6.875
Python 2.7.15+ 0.828 5.797 10.348
9227465 63245986 102334155

他はあまり変わらないにも関わらず Python だけ前回より遅くなってる気がするが、事実を記載しておこう(ごめんよ、Python 君)。

native キーワードが付いた関数はその名の通りマシン語コードにネイティブ・コンパイルし、JIT 実行させている。色々制約はあるが、数値計算だけならかなり速度的にいける。制約に関しては ここ(英語だが) 参照。

無名関数

無名関数として式中に直接関数定義を書ける。関数を別の関数の引数にもできる。

var calc = function(func, a, b) {
    return func(a, b);
};
System.println(calc(function(a, b) { return a + b; }, 2, 3));   // 5

ラムダ式

上記の無名関数をより簡潔に書けるようにしたのがラムダ式。function の代わりに & を、ブロックの代わりに => expression の形をとる。上記サンプルを書き換えるとこうなる。

var calc = &(func, a, b) => func(a, b);
System.println(calc(&(a, b) => a + b, 2, 3));   // 5

実は、expression の部分はブロックも取れるので、何か複数の処理を記載したい場合は &(args) => { ... } の形で記載することができる。逆にこの機能のため、expression に直接オブジェクトを書くことができない点に注意。オブジェクトを直接返したい場合は、({ a: 100 }) とかっこで括ることで回避する。

ブロック・オブジェクト

派生して、ブロック・オブジェクトを定義できるようにした。以下のように &{...} の形でブロックを引数として渡すことができる。

var a = 10;
var doit = &(block) => block();
System.println(doit(&{
    return a + 100;
}));

中身は引数無しの無名関数でしかないのだが、ブロックを渡してる感じが出ていいかなと思って。単純に &() => { ... } をさらに簡潔に記述できるようにしただけ。

コールバック・ブロック

関数の引数の最後にコールバックを置くケースはたくさんある。そういったケースで、ブロック的に関数呼び出し引数の外側に表記することができる。見た目だけの話だが、書きやすさで結構いい感じ。ただし、後から仕様追加したので、上記のブロック・オブジェクト等々とイマイチ一貫性が無い感じにはなっている。今は許容中。

尚、引数が コールバックだけ のときはカッコも省略できるという特典もついている。また、引数は以下のようにも書ける。

  • 引数リストを省略して、_1_2 といった名前でアクセスすることも可能。
  • _ は出現順に自動的に _1_2 と割り当てられていく。

これらを組合せて割と多種多様にかけるので、ここではサンプルを掲載することでエッセンスをつかんでもらいたい(手抜き...)。

var c = 100;
function f(...args) {
    var callback = args.pop();
    return callback(...args);
}

# This is a normal style.
var r = f(2, 10) { &(a, b) => a + b } + c;
System.println(r);

# Do not have to put the parameter list.
var r = f(2, 10) { => _1 * _2 } + c;
System.println(r);

# Can put a statement after a parameter list.
var r = f(3, 10) { &(a, b) return a + b; };
System.println(r);

# ':' is available for distinguishing parameters and a statement list.
var r = f(4, 10) { &(a, b): return a + b; };
System.println(r);

# Can put a statement list directly without a parameter list.
var r = f(5, 10) { return _1 * _2; };
System.println(r);

# No sense, but it is okay without statements.
var r = f(6, 10) { &(a, b) };
System.println(r.isUndefined);

# This means nothing to do, it can be used for ignoring callback, etc.
var r = f(7, 10) {};
System.println(r.isUndefined);

# Arguments can be accessed via special placeholders from _1 to _9.
var r = f(8, 10) { return _1 + _2; };
System.println(r);

# `_` is assigned to a placeholder according to the order of appearance.
var r = f(9, 10) { return _ + _; }; # same as `_1 + _2`
System.println(r);

# This is an example of multiple statements and assignment to the argument placeholder.
var r = f(9, 10, 11) {
    _4 = 10;    # A value can be assigned.
    return _1 + _2 + _3 + _4;
};
System.println(r);

# This is an example of map().
var r = [1, 2, 3].map() { => _1 * 2 };
System.println(r);

# The parenthesis can be omitted when the arguments is a callback function only and it is given by a block.
var r = [4, 5, 6].map { => _1 * 2 };
System.println(r);

# This is another example of omitting a parenthesis.
var r = (1..10).sort { => _2 <=> _1 };
System.println(r);

クロージャ

クロージャとは静的スコープを持つ関数オブジェクトのこと。よくあるサンプルは以下。

function newCounter() {
    var i = 0;          // a lexical variable.

    return function() { // an anonymous function.
        ++i;            // a reference to a lexical variable.
        return i;
    };
}

var c1 = newCounter();
System.println(c1()); // 1
System.println(c1()); // 2
System.println(c1()); // 3
System.println(c1()); // 4
System.println(c1()); // 5

静的スコープを持つことによって、関数呼び出しに対して状態を持つことができる所がポイント。ん?クラス・インスタンスみたいだって?正解。実際、ある意味 JavaScript のクラスの概念はここから派生している。そして、Kinx のクラスはまさにその 延長線上 にある。

クラス、モジュール、ファイバー

クラスはオブジェクト指向の中で最も重要な概念。一言で言えば、「データと操作のパッケージ」だ。オブジェクトの形を定義する設計図になる。その設計図に基づいて作られた個々の実体が「インスタンス」。一般的に「オブジェクト」と言えばインスタンスのことを指すが、オブジェクトと呼んだ場合はもう少し幅広い概念を指す。

オブジェクト指向基礎講座おしまい。

クラス

クラスはオブジェクトの形を定義する設計図。サンプルは以下。もちろん定義だけあっても何も動かない。工場に図面登録しても実際にオーダーが入って生産ラインでモノを作らなければ何も出来上がらないのと一緒。

class ClassName(a) {

    var privateVar_;

    private initialize() {
        privateVar_ = a;
        this.publicVar = 0;
    }

    /* private method */
    private method1() { /* ... */ }
    private method2() { /* ... */ }

    /* public method */
    public method3() { /* ... */ }
    public method4() { /* ... */ }

}

そこでインスタンス化だ。new 演算子でインスタンス化する。もちろんコンストラクタに引数を渡すこともできる。サンプルは以下の通り。

var obj = new ClassName(100);

ちなみに、インスタンス化する際に initialize() メソッドがあれば自動的に呼び出されるが、コンストラクタの引数は initialize() メソッドの引数ではないことに注意。また、コンストラクタに渡す引数が無ければ、(a) ごと削除できる。class ClassName {...} といった感じで。

Protected は現在未サポート。どうやって実現しようか検討中。

今時オブジェクト指向は常識みたいな扱いかとは思うが、あえて書くなら小難しい話よりもビャーネ・ストラウストラップの言ってた「オブジェクト指向の本質はカプセル化とマルチ・インスタンス」というのを理解するのが早いと思う。C では static 関数でカプセル化はできるがマルチ・インスタンスはできない。構造体を使えばマルチ・インスタンスはできるがカプセル化はできない。だから C with classes を作った、って。

マルチ・インスタンスが分かればオブジェクト指向も分かったも同然だ。たぶん。

継承

継承は : を使う。基底クラスのコンストラクタに引数を渡すこともできる。基底クラスのメソッドは super 経由で呼び出せる。継承は俗に言う is-A 関係というやつだ。

class BaseClass(a0) {
    public method1() { /* ... */ }
}
class ClassName(a0, a1) : BaseClass(a1) {
    public method1() { /* ... */ }
    public method2() {
        method1();          // This is calling ClassName class's method.
        this.method1();     // This is also calling ClassName class's method.
        @method1();         // Same as `this.method1();`
        super.method1();    // This is calling BaseClass class's method.
    }
}

ちなみに Kinx では this. (this plus dot) を @ で代用できる。

instanceOf

instanceOf() メソッドを使ってどのクラスのインスタンスかを確認できる。尚、基底クラス名を与えても true を返す。サンプルは以下。

class BaseClass {}
class ClassName : BaseClass {}

var x = new BaseClass();
var y = new ClassName();

System.println(x.instanceOf(BaseClass));    // 1
System.println(x.instanceOf(ClassName));    // 0
System.println(y.instanceOf(BaseClass));    // 1
System.println(y.instanceOf(ClassName));    // 1

モジュール

モジュールは機能の追加を行う。いわゆる多重継承による菱形継承の問題の解決に役立つ。概念的には継承とは別の観点で機能を分離し、あとから追加できるようにしたもの。

犬も服を着るのが当たり前の時代、服を着るというのは人間だけの専売特許ではないのです(人間という抽象概念に含まれている機能ではない)。そういうものは後から色んなオブジェクトに mixin。もちろん、服を着るための基本機能が備わってないとダメ。

module Printable {
    public print() {
        System.print(@value);
    }
    public println() {
        System.println(@value);
    }
}

class Value(v) {
    mixin Printable;
    private initialize() {
        @value = v;
    }
}

var v = new Value(100);
v.println();    // 100

この場合、Printable モジュールは、メンバーとして @value でアクセスできる要素があるクラスであれば mixin して print および println メンバー関数をアタッチできる。モジュール Printable 内の @、すなわち this は、ホストとなるクラスのインスタンスを表すことに注意。上記の場合、mixin された Value クラスのインスタンスが Printable の中で参照される。尚、複数のクラスに mixin されたとしてもちゃんと区別される。

ファイバー(Fiber)

軽量スレッドと呼ばれるファイバーをサポートしている。ファイバーはクラス(class Fiber)なので多くのクラスのうちの一つに過ぎないが、yield を使えるのはファイバーの中だけなので説明しておく。以下はファイバーでフィボナッチ数列を求める例。

var fib = new Fiber(&{
    var a = 0, b = 1;
    while (true) {
        yield b;
        [a, b] = [b, a + b];
    }
});

var r = 35.times().map(&(i) => fib.resume());
r.each(&(v, i) => System.println("fibonacci[%2d] = %7d" % i % v));

結果は以下の通り。

fibonacci[ 0] =       1
fibonacci[ 1] =       1
fibonacci[ 2] =       2
fibonacci[ 3] =       3
fibonacci[ 4] =       5
fibonacci[ 5] =       8
fibonacci[ 6] =      13
fibonacci[ 7] =      21
fibonacci[ 8] =      34
fibonacci[ 9] =      55
fibonacci[10] =      89
fibonacci[11] =     144
fibonacci[12] =     233
fibonacci[13] =     377
fibonacci[14] =     610
fibonacci[15] =     987
fibonacci[16] =    1597
fibonacci[17] =    2584
fibonacci[18] =    4181
fibonacci[19] =    6765
fibonacci[20] =   10946
fibonacci[21] =   17711
fibonacci[22] =   28657
fibonacci[23] =   46368
fibonacci[24] =   75025
fibonacci[25] =  121393
fibonacci[26] =  196418
fibonacci[27] =  317811
fibonacci[28] =  514229
fibonacci[29] =  832040
fibonacci[30] = 1346269
fibonacci[31] = 2178309
fibonacci[32] = 3524578
fibonacci[33] = 5702887
fibonacci[34] = 9227465

演算子オーバーライド

演算子オーバーライドとは

オブジェクトに対する演算子の挙動を上書きすること。演算子がクラスに属しているメソッドと考えれば「オーバーライド」となり、クラスに属さないと考えると「オーバーロード」となるイメージだが、ここでは Ruby っぽく演算子はクラス・オブジェクトへのメッセージでありクラスに属しているイメージで、そのクラス・メソッドを上書きする形を表現して「オーバーライド」で統一しておく。

尚、C++ の演算子オーバーロードは演算子の多重定義である。クラス・メソッドではなく、同じ名前の関数(や演算子)でも、その引数の違いによって呼び出される関数が区別される機能のこと。

基本形

オーバーライド可能な演算子の種類は以下の通り。

  • ==, !=, >, >=, <, <=, <=>, <<, >>, +, -, *, /, %, [], ().

例として、+ 演算子をオーバーライドしてみましょう。関数名を演算子名の + とするだけ。他の演算子でも同じ。

class Sample(value_) {
    @isSample = true;
    @value = value_;
    public +(rhs) {
        if (rhs.isSample) {
            return new Sample(value_ + rhs.value);
        }
        return new Sample(value_ + rhs);
    }
}

rhs として渡されるものは、適宜想定するコンテキストに合わせて場合分けして実装する必要がある。上記のように実装すると、以下のように使える。

var s1 = new Sample(10);
var s2 = s1 + 100;
s1 += 1100;
System.println(s1.value);  // => 1110
System.println(s2.value);  // => 110

a += b も内部的には a = a + b に展開されるので正しく動作する。

尚、オブジェクトに対するメソッド呼び出しなので、以下のようにも書ける。

var s1 = new Sample(10);
var s2 = s1.+(100);
System.println(s2.value);  // => 110

基本的に、[] 演算子と () 演算子以外の右辺値を取る演算子は、同様の動作をする。

[] 演算子

[] はインデックス要素的なアクセスを許可する。ただし、インデックスには整数(Integer)かオブジェクト、配列しか使えない。実数(Double)は動作するが引数には整数(Integer)で渡ってくる。文字列は使えない(プロパティ・アクセスと同じであり、無限ループする可能性があるため)。

実際に、例えば Range には実装されており、以下のようなアクセスが可能。

System.println((2..10)[1]);     // => 3
System.println(('b'..'z')[1]);  // => 'c'

ただし内部で toArray() されるので、イテレーションは最後まで行われた後に応答される。具体的には以下のように実装されている。

Range(多少異なるがこんな感じ)
class Range(start_, end_, excludeEnd_) {
    ...

    public [](rhs) {
        if (!@array) {
            @array = @toArray();
        }
        return @array[rhs];
    }
}

[] 演算子もメソッド呼び出し風に書くと以下のようになる。

System.println((2..10).[](1));     // => 3
System.println(('b'..'z').[](1));  // => 'c'

() 演算子

() 演算子はオブジェクトに直接作用する。C++ のファンクタ(operator() を定義したクラス)みたいなもの。例えば以下のようにクラス・インスタンスを関数のように見立てて直接 () 演算子を適用できる。

class Functor {
    public ()(...a) {
        return System.println(a);
    }
}

var f = new Functor();
f(1, 2, 3, 4, 5, 6, 7);  // => [1, 2, 3, 4, 5, 6, 7]

メソッド呼び出し風に書くと以下と同じ。

var f = new Functor();
f.()(1, 2, 3, 4, 5, 6, 7);  // => [1, 2, 3, 4, 5, 6, 7]

サンプル

スタック

スタック操作を << で行えるクラス Stack を作ってみよう。<< で Push する。>> でポップさせたいが、引数に左辺値を渡せないので、無理矢理だが () 演算子で行く。ちょっと中途半端だが仕方ない。配列を Push すると末尾に全部追加するようにしておく。

class Stack {
    var stack_ = [];
    public <<(rhs) {
        if (rhs.isArray) {
            stack_ += rhs;
        } else {
            stack_.push(rhs);
        }
    }
    public ()() {
        return stack_.pop();
    }
    public toString() {
        return stack_.toString();
    }
}
var s = new Stack();
s << 1;
s << 2;
s << 3;
s << 4;
s << [5, 6, 7, 8, 9, 10];
System.println(s);
var r = s();
System.println(s);
System.println(r);

実行してみましょう。

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
10

期待通り。

有理数クラス

別のサンプルとして四則演算のみをサポートした有理数クラスを作ってみる。符号処理は今回は省略。基本形は以下の通り。

class Rational(n, d) {
    @isRational = true;
}

まずは初期化。Rational オブジェクトのコピーも作れるようにしておく。また、有理数の演算では最大公約数を求める機会も多いのでそのための private メソッドを用意する。また、確認しやすいように toString() メソッドも用意しておく。

class Rational(n, d) {
    @isRational = true;
    private gcd(a, b) {
        if (a < b) {
            [a, b] = [b, a];
        }
        var r;
        while ((r = a % b) != 0) {
            [a, b] = [b, r];
        }
        return b;
    }
    private initialize() {
        if (d.isUndefined && n.isRatioal) {
            d = n.denominator;
            n = n.numerator;
        }
        var g = gcd(n, d);
        @numerator = Integer.parseInt(n / g);
        @denominator = Integer.parseInt(d / g);
    }
    public toString() {
        return "%{@numerator}/%{@denominator}";
    }
}

var r = new Rational(5, 10);
System.println("r = ", r);  // => r = 1/2

では、早速四則演算を定義していく。

ここではまず + 演算子の定義をする。ただし、r1 + r2r1 が破壊されるのは直感的ではないので、新しいオブジェクトを返すようにする。また、直接破壊的に操作する別のメソッドを用意しておく。ついでにオブジェクトのクローンをつくる clone() メソッドを作って活用する。

class Rational(n, d) {
    @isRational = true;
    private gcd(a, b) {
        if (a < b) {
            [a, b] = [b, a];
        }
        var r;
        while ((r = a % b) != 0) {
            [a, b] = [b, r];
        }
        return b;
    }
    private initialize() {
        if (d.isUndefined && n.isRational) {
            d = n.denominator;
            n = n.numerator;
        }
        var g = gcd(n, d);
        @numerator = Integer.parseInt(n / g);
        @denominator = Integer.parseInt(d / g);
    }
    public toString() {
        return "%{@numerator}/%{@denominator}";
    }
    public clone() {
        return new Rational(this);
    }
    public add(rhs) {
        if (rhs.isInteger) {
            return this + new Rational(rhs, 1);
        } else if (rhs.isRational) {
            var n = @numerator * rhs.denominator + @denominator * rhs.numerator;
            var d = @denominator * rhs.denominator;
            var g = gcd(n, d);
            @numerator = Integer.parseInt(n / g);
            @denominator = Integer.parseInt(d / g);
        } else {
            throw RuntimeException("Unsupported type for rational calculation");
        }
        return this;
    }
    public +(rhs) {
        return @clone().add(rhs);
    }
}

var r1 = new Rational(5, 10);
var r2 = new Rational(2, 6);
var r3 = r1 + r2;
var r4 = r1 + 2;
System.println("r1 = ", r1);
System.println("r2 = ", r2);
System.println("r1 + r2 = ", r3);
System.println("r1 + 2  = ", r4);

rhs が Integer の場合、こんなこと(this + new Rational(rhs, 1) のことね)する必要はないのだが、こんなこともできる、という意味での単なる例。新たに Rational オブジェクトを作って再度 .+() 演算子が呼ばれて正しく計算されるというイメージ。

結果は以下のように表示される。

r1 = 1/2
r2 = 1/3
r1 + r2 = 5/6
r1 + 2  = 5/2

では、四則演算全て定義してみよう。先ほどの無駄っぽいところ(this + new Rational(rhs, 1) のことね)も今回は変えておく。

class Rational(n, d) {
    @isRational = true;
    private gcd(a, b) {
        if (a < b) {
            [a, b] = [b, a];
        }
        var r;
        while ((r = a % b) != 0) {
            [a, b] = [b, r];
        }
        return b;
    }
    private makeValue(n, d) {
        var g = gcd(n, d);
        @numerator = Integer.parseInt(n / g);
        @denominator = Integer.parseInt(d / g);
        return this;
    }
    private initialize() {
        if (d.isUndefined && n.isRational) {
            d = n.denominator;
            n = n.numerator;
        }
        makeValue(n, d);
    }
    public toString() {
        return "%{@numerator}/%{@denominator}";
    }
    public clone() {
        return new Rational(this);
    }
    public add(rhs) {
        if (rhs.isInteger) {
            return makeValue(@numerator + @denominator * rhs, @denominator);
        } else if (rhs.isRational) {
            return makeValue(@numerator * rhs.denominator + @denominator * rhs.numerator,
                             @denominator * rhs.denominator);
        } else {
            throw RuntimeException("Unsupported type for rational calculation");
        }
    }
    public sub(rhs) {
        if (rhs.isInteger) {
            return makeValue(@numerator - @denominator * rhs, @denominator);
        } else if (rhs.isRational) {
            return makeValue(@numerator * rhs.denominator - @denominator * rhs.numerator,
                             @denominator * rhs.denominator);
        } else {
            throw RuntimeException("Unsupported type for rational calculation");
        }
    }
    public mul(rhs) {
        if (rhs.isInteger) {
            return makeValue(@numerator * rhs, @denominator);
        } else if (rhs.isRational) {
            return makeValue(@numerator * rhs.numerator,
                             @denominator * rhs.denominator);
        } else {
            throw RuntimeException("Unsupported type for rational calculation");
        }
    }
    public div(rhs) {
        if (rhs.isInteger) {
            return makeValue(@numerator, @denominator * rhs);
        } else if (rhs.isRational) {
            return makeValue(@numerator * rhs.denominator,
                             @denominator * rhs.numerator);
        } else {
            throw RuntimeException("Unsupported type for rational calculation");
        }
    }
    public +(rhs) {
        return @clone().add(rhs);
    }
    public -(rhs) {
        return @clone().sub(rhs);
    }
    public *(rhs) {
        return @clone().mul(rhs);
    }
    public /(rhs) {
        return @clone().div(rhs);
    }
}

var r1 = new Rational(5, 10);
var r2 = new Rational(2, 6);
var r3 = r1 + r2;
var r4 = r1 - r2;
var r5 = r1 * r2;
var r6 = r1 / r2;
System.println("r1 = ", r1);
System.println("r2 = ", r2);
System.println("r1 + r2 = ", r3);
System.println("r1 - r2 = ", r4);
System.println("r1 * r2 = ", r5);
System.println("r1 / r2 = ", r6);

結果。

r1 = 1/2
r2 = 1/3
r1 + r2 = 5/6
r1 - r2 = 1/6
r1 * r2 = 1/6
r1 / r2 = 3/2

clone() についての補足

clone() は通常、上記のように new 自分自身のクラス(this) で定義することが多いが、以下のようにすると新たに作ったオブジェクトが過去のオブジェクトへの参照を持ち続けてしまうので、新たに作成したオブジェクトが死なない限りその元オブジェクトも GC で解放されないといったことになり、リークする可能性がある。

class A(arg_) {
    @isA = true;
    var a_;
    private initialize() {
        a_ = arg_.isA ? arg_.get() : 0;
        // arg_ = null が無いと参照を持ち続けてしまう
    }
    public get() {
        return a_;
    }
    public clone() {
        return new A(this);
    }
    /* ... */
}

上記コメントのように初期化後に arg_ = null とすれば OK だが、それ以外にも、arg_a_ を共用させる方法もある(上記 Rational クラスはそれに近い方法)。例えば以下のような感じ。

class A(a_) {
    @isA = true;
    private initialize() {
        a_ = a_.isA ? a_.get() : 0;
    }
    public get() {
        return a_;
    }
    public clone() {
        return new A(this);
    }
    /* ... */
}

こうすることで、新たなオブジェクトから過去のオブジェクトへの参照が切れるので、しかるべき時にきちんと GC が働くようになる。

おわりに

かなり長い記事になりましたが、最後まで読んでいただきありがとうございます。

ここに記載した「Kinx 基礎文法最速マスター」は、これまで記事にした以下のものをまとめて、可能な限り(見落としてなければ...)最新の情報にアップデートしたものです。

これらも含め、以下のメイン記事からはこれまで書かせていただいた様々な記事へのリンクを集約しています(書いた時期によって多少古い記事である可能性はありますが...)。

もしご興味があるようでしたらぜひぜひ覗いてみてください。また、ソースコード一式は以下の GitHub 上にあります。

こちらももしご興味ありましたらぜひご覧いただけると、また軽く Star をプッシュしていただけたりすると非常に励みになります。

では改めて、最後まで読んでいただきありがとうございました。
またどこかでお会いしましょう。

P.S.
というか、明日(12/21)の「言語実装 Advent Calendar 2020」にも記事がアップされる予定です。そちらもどうぞよろしく。


  1. ...が、思ったより疲れました。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

qiita post のpublicとprivateの確認テスト

Mac OS X-10.13.3 ruby-2.7.0p0

qiitaの限定記事をwebで公開記事にしたあと,qiita postがエラーになる点を,idからpublicかを確かめるためのテスト.


  • source ~/Github/grad_research_20s/yamamoto/qiita_org_tests/hoge.org
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マルチスケールシミュレーション特論:第 11 回をまとめてみた

ruby-2.5.5p157

はじめに

次回更新予定


  • source ~/Downloads/git/grad_members_20f/members/taiseiyo/memos/class11.org
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Example title

test

aaaaaatesttest

  • 1
  • 2
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Example title

Example

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

セッションとCookie

セッションとCookie

今回はRailsでログイン機能を作るときに学んだセッションとCookieについて書いていこうと思います。

セッション

WebアプリケーションではブラウザからサーバへHTTPリクエストを送り、HTTPレスポンスを受け取って表示する。
このHTTPリクエストを繰り返し行うことで順番に操作を行っていく。
しかし、HTTPはステートレス(*1)なプロトコルなので、
そのままでは、同じユーザーから送られた1つ目のリクエストから2つ目のリクエストに情報を引き継ぐことができない。
(例えば、ユーザー登録情報画面に個人情報を記載して登録ボタンを押したが必須のところの書き忘れがあると
初めから記入し直さなくてはいけないなど。)
そのため、Webアプリケーションではサーバー側にセッションと言う仕組みを用意して、
1つのブラウザから連続して送られている一連のリクエストの間で、「状態」を共有できるようにしている。
Railsでは、コントローラからsessionと言うメソッドを呼び出すことで、セッションにアクセスできる。
sessionはハッシュのように扱うことができる。
セッションをデータを入れるには、任意のキーを指定して値を格納します。

session[:user_id] = @user.id

値を取り出すには次のように参照する。

@user_id = session[:user_id]

Cookie

セッションに似た仕組みとしてCookieがある。
セッションはアプリケーションサーバー側で独自に実現される仕組みであるのに対して
CookieはブラウザとWebサーバの間でやり取りされる、より汎用的な仕組み。
Cookie情報は、キーと値のペアの集合。

*1 ステートレスとは?
https://qiita.com/wind-up-bird/items/b210e294ecb147d67e2b

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ログイン機能の実装【Rails】

はじめに

現在、転職活動用のポートフォリオ作成のため、補助金などに関する記事の閲覧・検索アプリケーションを作成しております。その際に実装したユーザー管理機能(deviseの導入)をアウトプットのため記載していきます。記事初投稿になります。

環境:Rails 6.0

1.deviseの導入

Gemfile
gem 'devise'

を記述後、ターミナルにて

ターミナル
% bundle install
% rails g devise:install

のコマンドを実行し、ライブラリをインストールします。

2.モデルの作成

ターミナル
% rails g devise user

のコマンドにて、モデルとマイグレーションの生成やルーティングの設定を行います。

3.テーブルの作成

先ほど生成したマイグレーションファイルを使用してテーブルを作成します。

...._devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string  :first_name,         null: false
      t.string  :last_name,          null: false
      t.string  :first_name_kana,    null: false
      t.string  :last_name_kana,     null: false 
      t.string  :email,              null: false, default: "", unique: true
      t.string  :encrypted_password, null: false, default: ""
      t.integer :state_id,           null: false
      t.string  :phone_number,       null: false
      t.date    :birth_day,          null: false
      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

デフォルトでemailとpasswordのカラムは作成されているので、そのほか必要に応じて記載します(first_nameやbirth_dayなど)。

ターミナル
% rails db:migrate

を実行し、usersテーブルが作成できる。

4.モデルの編集

app/models/user.rb
class User < ApplicationRecord
  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to :state
  has_many :sns_credentials, dependent: :destroy

  devise :database_authenticatable, :registerable,
  :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:facebook, :google_oauth2]

  validates :first_name, :last_name, :first_name_kana, :last_name_kana, :state_id, :phone_number, :birth_day, presence: true
  PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i.freeze
  validates_format_of :password, with: PASSWORD_REGEX, message: 'には英字と数字の両方を含めて設定してください'
  validates :first_name, format: { with: /\A[ぁ-んァ-ン一-龥]/, message: 'は全角で入力してください' }
  validates :last_name, format: { with: /\A[ぁ-んァ-ン一-龥]/, message: 'は全角で入力してください' }
  validates :first_name_kana, format: { with: /\A[ァ-ヶー-]+\z/, message: 'は全角カナで入力してください' }
  validates :last_name_kana,  presence: true, format: { with: /\A[ァ-ヶー-]+\z/, message: 'は全角カナで入力してください' }
  validates :phone_number, format: { with: /\A\d{11}\z/, message: 'は数字のみで入力してください' }

#中略

こちらにバリデーションやアソシエーションを追記していきます。

5.ビューファイルの編集

index.html.erb
<header>

#中略
<%# ユーザーがログインしているとき %>
    <% elsif user_signed_in? %>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item login-user">ようこそ、<%= current_user.last_name %>さん!</li>
      </ul>
      <ul class="navbar-nav ml-auto">
        <li class="nav-item"><%= link_to '記事一覧', root_path %></li>
        <li class="nav-item"><%= link_to 'マイページ', "#" %></li>
        <li class="nav-item"><%= link_to 'ログアウト', destroy_user_session_path, method: :delete %></li>
      </ul>
    </div>
<%# ログインしていないとき %>
    <% else %>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-auto">
        <li class="nav-item"><%= link_to '記事一覧', root_path %></li>
        <li class="nav-item"><%= link_to '新規登録', new_user_registration_path %></li>
        <li class="nav-item"><%= link_to 'ログイン', new_user_session_path %></li>
      </ul>
    </div>
    <% end %>
  </nav>
  </div>
</header>

今回はログインしている場合としていない場合でビューファイルを分けています。

6.ストロングパラメーターの設定

先ほどマイグレーションファイルでも記述しましたが、ユーザー情報のカラムはemailとpasswordがデフォルトで入っており、その他の任意の情報(ユーザー名など)は追記する必要があります。その際に新たな情報を渡すためにストロングパラメーターも編集しなければならないですが、deviseに関しては、処理を行うコントローラーがGemの中に記述されており、編集することができません。そのためdeviseのコントローラーにストロングパラメーターを反映する方法として、devise_parameter_sanitizerメソッドを使用しました。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  private

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :first_name_kana, :last_name_kana, :state_id, :phone_number, :birth_day])
  end
end

全てのコントローラーが継承しているファイルである、application_controller.rbファイルに記述しています。deviseの処理を行うコントローラーはGemの中に記述されており編集できないため、本ファイルに定義しています。

7.最後に

一通りの実装方法を記述してきましたが、不明な点や間違っている点がありましたら、ご指摘いただけると幸いです。今後はdevise実装から管理者機能の追加、SNS認証の導入も行っていますので、そのアウトプットもできたらと思っております。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

devise_parameter_sanitizerメソッドについて

devise_parameter_sanitizerとは?

 devise(Gem)のUserモデルに関わる「ログイン」「新規登録」などのリクエストからパラメーターを取得できるようになるメソッド。
 また、新しく追加したカラムをdeviseに定義されているストロングパラメーターに含めたい場合は、permitメソッドを用いることで含めることができる。
 さらに、deviseに関するストロングパラメーターを記述する際は、application_controller.rbに記述する。(deviseの処理を行うコントローラーはGem内に記述されており編集できないため。)

使用方法

① privateにdeviseに関するストロングパラメーターの記述する。

application_controller.rb
private

def configure_permitted_parameters
  devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー])
end

 第一引数の処理名には、それぞれサインイン時やサインアップ時、アカウント情報更新時の処理を記述する。

deviseの処理名 役割
:sign_in ログインの処理を行う時
:sign_up 新規登録の処理を行う時
:account_update アカウント情報更新の処理を行う時

例)新規登録時に名前(name)を許可する記述

application_controller.rb
private

def configure_permitted_parameters
  devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
end

② before_actionを用いてメソッドを呼び出す。

application_controller.rb
before_action :configure_permitted_parameters, if: :devise_controller?

private

def configure_permitted_parameters
  devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー])
end

「if:」
 before_actionのオプションの一つで、値にメソッド名を指定することで、その戻り値がtrueであるときのみ処理を実行するように設定できる。
 今回の場合は、「:devise_controller?」というdeviseのヘルパーメソッド名を指定して、deviseに関するコントローラーの処理であればその時だけ実行されるように設定している。
 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】Ruby on Rails チュートリアル「第5章」のテストを RSpec で書いてみた

はじめに

こちらは前回の続きとなります。

【RSpec】Ruby on Rails チュートリアル「第4章」のテストを RSpec で書いてみた
https://qiita.com/t-nagaoka/items/4565a453596dfa0b0e3e

対象者

  • Ruby on Rails チュートリアルのテストを Rspec で実施予定、または実施してみたい人
  • Ruby on Rails チュートリアル「第5章」のテストを Rspec で書きたいけど、書き方が分からない人

テストコード

実際にテストコードを書き換えた結果が下記になります。

Minitest

test/controllers/static_pages_controller_test.rb
class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get root_path
    assert_response :success
    assert_select "title", "Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get help_path
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get about_path
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end

  test "should get contact" do
    get contact_path
    assert_response :success
    assert_select "title", "Contact | Ruby on Rails Tutorial Sample App"
  end
end
test/integration/site_layout_test.rb
require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest

  test "layout links" do
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    get contact_path
    assert_select "title", full_title("Contact")
    get signup_path
    assert_select "title", full_title("Sign up")
  end
end
test/helpers/application_helper_test.rb
require 'test_helper'

class ApplicationHelperTest < ActionView::TestCase
  test "full title helper" do
    assert_equal full_title,         "Ruby on Rails Tutorial Sample App"
    assert_equal full_title("Help"), "Help | Ruby on Rails Tutorial Sample App"
  end
end
test/controllers/users_controller_test.rb
class UsersControllerTest < ActionDispatch::IntegrationTest

  test "should get new" do
    get signup_path
    assert_response :success
  end
end

RSpec

spec/requests/static_pages_spec.rb
require "rails_helper"

RSpec.describe "StaticPages", type: :request do
  describe "Home page" do
    it "should get root" do
      visit root_path
      expect(page.status_code).to eq(200)
      expect(page).to have_title full_title
      expect(page).not_to have_title "Home |"
    end
  end

  describe "Help page" do
    it "should get help" do
      visit help_path
      expect(page.status_code).to eq(200)
      expect(page).to have_title full_title("Help")
    end
  end

  describe "About page" do
    it "should get about" do
      visit about_path
      expect(page.status_code).to eq(200)
      expect(page).to have_title full_title("About")
    end
  end

  describe "Contact page" do
    it "should get contact" do
      visit contact_path
      expect(page.status_code).to eq(200)
      expect(page).to have_title full_title("Contact")
    end
  end
end
spec/features/site_layout_spec.rb
require 'rails_helper'

RSpec.describe "SiteLayoutTest", type: :feature do
  before { visit root_path }
  subject { page }
  scenario "layout links" do
    is_expected.to have_link nil, href: root_path, count: 2
    is_expected.to have_link nil, href: help_path
    is_expected.to have_link nil, href: about_path
    is_expected.to have_link nil, href: contact_path
    visit contact_path
    expect(page).to have_title full_title("Contact")
    visit signup_path
    expect(page).to have_title full_title("Sign up")
  end
end
spec/helpers/application_helper_spec.rb
require "rails_helper"

RSpec.describe ApplicationHelper, type: :helper do
  describe "full_title" do
    context 'no argument' do
      it 'return 「Ruby on Rails Tutorial Sample App」' do
        expect(full_title).to eq("Ruby on Rails Tutorial Sample App")
      end
    end
    context 'argument is "Help"' do
      it 'return 「Help | Ruby on Rails Tutorial Sample App」' do
        expect(full_title("Help")).to eq("Help | Ruby on Rails Tutorial Sample App")
      end
    end
  end
end
spec/requests/users_controller_spec.rb
require 'rails_helper'

RSpec.describe UsersController, type: :request do
  describe "GET #new" do
    it "returns http success" do
      get signup_path
      expect(response).to have_http_status(:success)
    end
  end
end

ポイント

full_title ヘルパーを使用するためには、以下のように rails_helper.rb で ApplicationHelper を読み込む必要があります。

spec/rails_helper.rb
RSpec.configure do |config|
  include ApplicationHelper

次回

「第6章」のテストコードを RSpec に書き換える予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ポートフォリオ(TOKYOシェア不動産)解説

アプリ概要

都内のシェアハウス物件を簡単に、シェアハウスに対するリアルな評価を参考にしながら検索できるアプリです。

  • 都内にあるシェアハウスを検索できる
  • 内覧に行ったことがある物件や以前住んでいた物件の口コミを点数付きで投稿する機能を実装
  • 気になる物件をマイページで管理できる機能を実装

下記ページで公開中です!
https://share-house-app.herokuapp.com/

このアプリを作った背景

自分が困った経験と、「こういうものがあればもっとシェアハウス入居者が増えるのでは」という思いつきから。
具体的には下記の通りです。

1. シェアハウスの良さを共有したい
2. シェアハウスならではのリアルな情報を得られるサイトがない

1. シェアハウスの良さを共有したい

私は22歳のときに関西から上京し、女性限定の8名住みのシェアハウスに1年半住んでいます。上京した際にシェアハウスを選んだ理由は、「初期費用も含めて安い家賃で便利な立地に住むことができるから」です。値段と立地の選択肢だけでシェアハウスを選んだのですが、体調不良や災害時にお互いに助け合うことができる、友人ができる、不在時に郵便物を受け取ってもらえる、風呂トイレなどの掃除不要など、実際に住むと思ってた以上にメリットを感じました。
一方で、シェアハウスに住んでいると話すと、驚かれることも多いこと、シェアハウス経験者が少ないことを感じました。

そこから、「もっとシェアハウスの良さを知ってもらいたい、一人暮らし物件を探す時の選択肢にシェアハウスを考えてもらいたい!」
と考えるようになり、「スーモのような、シェアハウス版物件検索サイトを私が作ろう!」と考えました。

2. シェアハウスならではのリアルな情報を得られるサイトがない。

私は初めてシェアハウスを探した時に、どのサイトから探したらいいのか、シェアハウスってどういう雰囲気なのかはサイトからでは全くわからず、とても苦労しました。私の友人も含めて、シェアハウスは共同生活だからこそ、人間関係、共同部分の衛生環境、騒音、匂いなど内見だけじゃわからない不安点を持って入居を始めます。
そこで、「物件ごとの入居者・内見者の口コミ情報を集約できるサイトを作れば便利なのでは?」と思い、「それを実現できるサービスを作ってみたい」と今回の作成に至りました。

機能

・検索機能
・口コミ投稿、評価機能
・マイページ機能
・物件管理機能
・管理者権限

検索機能

  • エリア別で検索ができる
  • 保存数順、家賃順、駅からの距離順で検索ができる

口コミ投稿・評価機能

  • 内覧時、入居時のパターンを分けて口コミを投稿できる

マイページ機能

  • 口コミ投稿した物件を一覧で閲覧できる
  • 保存した物件を一覧で確認できる

物件管理機能

  • 物件の所在地を地図で確認できる

管理者権限

  • 物件情報の登録・編集

追加実装予定

  • 検索機能の複雑化
  • 部屋の家具情報ページの追加
  • 物件画像を複数投稿、複数表示
  • 物件投稿者へのお問い合わせ機能
  • rspecを使ったテストコード
  • レスポンシブ対応
  • 管理者画面で口コミ集計結果を表示
  • AWSにデプロイ

TrelloのURLに詳しく記載しております。https://trello.com/b/Cdp4Es2l/%E3%82%B7%E3%82%A7%E3%82%A2%E3%83%8F%E3%82%A6%E3%82%B9%E6%A4%9C%E7%B4%A2%E3%82%B5%E3%82%A4%E3%83%88

ポイント

・実際にシェアハウスに住んでいる友人とシェアハウスに興味がある友人に物件探しで重視することや心配事をヒアリングし、アプリの仕様を検討
・評価を★で表すために、jQueryのプラグイン(Raty)を使用
・Google Maps APIを使用して、店舗地図を表示
・管理者権限を持ったアカウントのみ、物件情報を扱える仕様に
・GithubでプルリクとTreroでタスク管理を丁寧に行いながら開発
・並び替え検索によって、目的に応じた検索性を向上
・物件ごとの共有設備をアイコンを使って表示
・物件ごとに部屋情報を表示

使用技術等

  • 言語:Ruby
  • フレームワーク:Ruby on Rails
  • フロント:html、Sass、JavaScript(jQuery)
  • インフラ:heroku
  • ソースコード管理:GitHub
  • その他:Trelloでタスク管理

環境

  • Ruby 2.6.6p146
  • Rails 6.0.3.4
  • PostgreSQL 12.3
  • Heroku 

使用した主なGem

  • ransack :詳細な検索機能
  • devise :ユーザーログイン機能
  • kaminari :ページネーション機能
  • geocoder :住所から経度緯度を算出
  • binding.pry :デバックツール
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでDBパスワード等を環境変数(.env)で管理する

DBのパスワードやAPIのアクセスキー等の重要な情報は、
Gitにあげたくなかったり開発環境と本番環境で異なっていたりします。
そういった情報の管理によく利用されるのが環境変数というものらしい。

Railsで環境変数を利用する場合、大まかに下記2パターンある様子。
①.bash_profileに記述する
②dotenv-railsというgemを利用し.envに記述する

今回は、②のenvファイルを利用する方法を自分用に残しておきます。
間違い等あれば、ご指摘いただければ幸いです。

環境

MacOS Catalina
Rails 6.0.3.2
Ruby 2.6.3
mysql 8.0.22

gemを導入する

Gemfileに下記を追記します。

Gemfile
gem 'dotenv-rails'

忘れずに、bundle installします。

bundle install

.envファイルを作成する

Railsアプリのルートディレクトリに移動し、下記を実行。

touch .env

.envファイルを修正し、管理したいキーを追加します。

.env
DB_NAME=RailsApp_production
DB_USERNAME=root
DB_PASSWORD=12345678
DB_HOSTNAME=hogehoge

○○=hogehoge の○○の部分がキーになるので、使用する際に分かりやすいキー名に設定します。

設定した環境変数を使用する

ENV['キー名']で呼び出すことができます。

ENV['DB_NAME']
ENV['DB_USERNAME']

環境変数をgitにあげないようにする

せっかく環境変数を利用しているのに、gitにあげちゃったら元も子もありません。
.gitignoreに下記の行を追記し、.envがgitにコミットされないようにします。
(既に設定していれば修正不要)

.gitignore
.env

終わり

思った以上に簡単に利用することができますね。
環境変数は多くのRailsアプリで利用することになると思うので、できればこの記事を見なくても使えるようにしたい所。
.bash_profileを利用する場合はgemを導入する必要がないのでラクそうだけど、利用手順はちょっと忘れそうな感じがします…。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む