Polished Ruby Programming を読んでいる

ずっと興味のあった洋書の「Polished Ruby Programming」を Kindle で買った。英語の勉強も兼ねて少しずつ読んでいる。

Polished Ruby Programming: Build better software with more intuitive, maintainable, scalable, and high-performance Ruby code (English Edition)

自分の英語の読書スピードだと全てを読む前に飽きてしまいそうなので、目次を見て面白そうな順番で読むことにした。最初は「Chapter 4: Methods and Their Arguments」から。

以下面白そうなトピックの抜粋。

メソッドの引数は可能なら 0 にした方がよい

引数があるとパラメータの設定ミスが起こるので引数が不要なメソッドが一番分かりやすい。次点が1個で2つ以上になるととても難しくなるのでできるだけ引数の数は減らせるようにしよう。

実際に Ruby の Object も大半が 0 になっているのをコードを書いて確かめているのがよい。

h = Hash.new(0)
o = Object.new
o.methods.each do |m|
 h[o.method(m).arity] += 1
end
h
# => {0=>20, 1=>18, -1=>18, 2=>1}

Array, Hash, String も調べてみる。 -1 は可変長引数を受け付ける場合らしい。やはり 0 OR 1 がほとんどになっている。

def method_argument_count(t)
  h = Hash.new(0)
  o = t.new
  o.methods.each do |m|
   h[o.method(m).arity] += 1
  end
  h
end

method_argument_count(Array)
# => {0=>70, 1=>43, -1=>76, -2=>1, 2=>1}
method_argument_count(Hash)
# => {-1=>58, 0=>71, 1=>44, 2=>3}
method_argument_count(String)
# => {0=>67, -1=>71, 1=>41, -2=>2, 2=>8}

Ruby 引数の種類はたくさんある

Ruby の引数種類の詳しい説明は始めて見た。デフォルト引数前に置けるのね。

著者の Ruby を熟知した感じと好きな感じが端々から溢れていてとても楽しい。

callメソッドの糖衣構文

https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html#call_method

Proc#call が実装済みなら以下で呼べるらしい。あんまり使われていないのは C++ みたいに.が省略できないからなんだろうなぁ。(筆者はそれもあって #[]をい使うことが多いとも書いてあった)

foo.(100)      # foo.call(100)と全く同じ意味
foo::(100)     # foo.call(100)と全く同じ意味

追記

ところで今なら日本語版の「研鑽Rubyプログラミング」が発売したばかりなのでそっちを買うのもおすすめです。

※ 実は自分もβ版買ってありました(すっかり忘れてた・・)

blogsync の使い方メモ

こんなルーティーンでやっている。セットアップは過去記事を参考に。Windows でも動く。

1. 下書きを書く

下書きはモバイルアプリやブラウザで作っておく。ちゃんと書くときは PC で書いた方が早いのでタイトルとざっくり内容だけ。

2. 下書きを手元に持ってくる。

$ blogsync pull
    200 <--- https://blog.hatena.ne.jp/tuto0621/ongaeshi.hatenablog.com/atom/entry?page=1203221981
    fresh remote=2023-04-10 23:52:22 +0900 JST > local=0001-01-01 00:00:00 +0000 UTC
  store C:\blog\ongaeshi.hatenablog.com\entry\2023\04\10\235222.md

3. 記事を編集して投稿

$ blogsync push ongaeshi.hatenablog.com\entry\2023\04\10\235222.md

4. git にも同期

投稿したら git レポジトリにもバックアップしておく。

$ git commit

メモ: アプリやブラウザから下書きを更新するとファイル名が別になってしまう

画像を張り付けるなど、ブラウザから下書きを更新してから blogsync pull すると別のファイルとしてファイルが降ってきてしまう。後で検索するときに複数の記事がひっかかってしまうと嫌なので古い下書きは削除している。一度投稿した記事だとそのようなことは起きない。

微妙に分かりにくいけど分かると捗るSpotifyのホーム画面

  • ホーム🏠画面には重要な情報が詰まっている
  • が、微妙にUIが分かりにくいため知らない人が多い
  • 分かると音楽生活が捗るので紹介する

  • 基本ルール: 上から下にスクロールしながら「気になるカテゴリがあったら横にスクロールしてさらに探す」←これが分かりにくい
    • 上下スクロールでざっと見られるようにして欲しい...
    • 例、こんにちは...フレッシュサウンド...夏のサウンドトラック!
    • 気になるので横にスクロールしてみよう
    • ... Summer Hits of the 80sを聞くぞ
    • こんな感じで縦スクロールでカテゴリ見て気になるやつは横スクロールしていけばOK
  • 右上のアイコン2つも重要
  • ベルみたい🔔なやつは最新情報
    • 自分がフォローしているアーティストの曲
    • ベルは存在はよく忘れるけどフォローしてる曲だけピックアップだけ再生できるので結構有用
  • 時計みたい⏰なやつは履歴
    • 自分が過去に聞いたやつが探せる
    • 後からお気に入りしたいときに便利

Ruby で Processing がブラウザ上からできる p5.rb を作りました

ruby.wasm + p5.js の組み合わせです。ほとんどの API は移植したので大体同じことができると思います。

https://p5rb.ongaeshi.me/

使い方

p5.rb を HTML に読みこめばすぐに使えます。

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@next/dist/browser.script.iife.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.js"></script>
    <script type="text/ruby" src="p5.rb"></script>
    <script type="text/ruby">
def setup
  createCanvas(720, 400)
end

def draw
  background(127)
  noStroke()
  0.step(height, 20) do |i|
    fill(129, 206, 15)
    rect(0, i, width, 10)
    fill(255)
    rect(i, 0, 10, height)
  end
end        

P5::init()
    </script>
  </head>
  <body>
    <main>
    </main>
  </body>
</html>

オンラインエディタ

https://p5rb.ongaeshi.me/editor からオンラインエディタが使えます。

作ったものはURLで共有できます。モバイルからも使えるのでまずはこちらを触ってみるのがおすすめです。

できたものは #p5rb タグを付けて共有していただけるとよろこびます。

実装メモ

ruby.wasm の JS::Object 経由で p5.js のメソッドやプロパティをブリッジしています。Ruby の method_missing を使うことで 100 行程度で p5.js の全ての関数やプロパティを呼び出しています。method_missiong すごい。

p5.rb#L217-L332

# JS::Object can call property via function style
class JS::Object
  def method_missing(sym, *args, &block)
    ret = self[sym]

    case ret.typeof
    when "undefined"
      str = sym.to_s
      if str[-1] == "="
        self[str.chop.to_sym] = args.first
        return args.first
      end

      super
    when "function"
      self.call(sym, *args, &block).to_r
    else
      ret.to_r
    end
  end

  def respond_to_missing?(sym, include_private)
    return true if super
    self[sym].typeof != "undefined"
  end

  def to_r
    case self.typeof
    when "number"
      self.to_f
    when "string"
      self.to_s
    else
      self
    end
  end
end

# Call p5.js global functions
$p5 = nil

def method_missing(sym, *args, &block)
  return super unless $p5.respond_to?(:[])
  ret = $p5[sym]

  case ret.typeof
  when "undefined"
    # str = sym.to_s
    # if str[-1] == "="
    #   $p5[str.chop.to_sym] = args.first
    #   return args.first
    # end
    super
  when "function"
    $p5.call(sym, *args, &block).to_r
  else
    ret.to_r
  end
end

# Add new p5() to window.constructors.p5()
JS.eval("window.constructors = { p5: (...args) => new p5(...args) };")

module P5
  Vector = JS.global[:p5][:Vector]

  module_function

  def init(query = "main", obj = self)
    unless query.is_a?(String)
      query, obj = "main", query
    end

    $p5&.remove()
    $p5 = nil

    sketch = ->(p5) {
      $p5 = p5
      init_method(obj, :preload)
      init_method(obj, :setup)
      init_method(obj, :draw)
      init_event_method(obj, :mouseMoved)
      init_event_method(obj, :mouseDragged)
      init_event_method(obj, :mousePressed)
      init_event_method(obj, :mouseReleased)
      init_event_method(obj, :mouseClicked)
      init_event_method(obj, :doubleClicked)
      init_event_method(obj, :mouseWheel)
      init_event_method(obj, :keyPressed)
      init_event_method(obj, :keyReleased)
      init_event_method(obj, :keyTyped)
    }
    
    container = JS.global.document.querySelector(query)
    container.innerHTML = ""
    JS.global.window.constructors.p5(sketch, container)
  end

  def init_method(obj, sym)
    if obj.respond_to?(sym, true)
      m = obj.method(sym)
      $p5[sym] = ->() { m.call() }
    end
  end

  def init_event_method(obj, sym)
    if obj.respond_to?(sym, true)
      m = obj.method(sym)
      if m.parameters.count >= 1
        $p5[sym] = ->(e) { m.call(e) }
      else
        $p5[sym] = ->(e) { m.call() }
      end
    end
  end
end

おわりに

作ったものがあったら #p5rb タグを付けて放流してもらえたら嬉しいです。

全ての戻り値に to_r するのはやりすぎかもしれないけど、プロパティを Ruby の括弧無しメソッド呼び出しで呼べるのは本家でできてもいいのかもと思っています(後で PR 出してみよう)。

はてなブログ用CLIクライアントの blogsync を使ってみる

はてなブログの執筆環境として blogsync を試してみた。

https://github.com/x-motemen/blogsync

GitHub からバイナリをダウンロード。Windows版を使う。 とにかく blogsync pull で全記事の原稿が簡単で取得できるのがすごく便利。 過去記事の検索や参照も VSCode からできてバックアップ環境としても優秀。

ひっかかりポイント config.yml -> config.yaml

さっそく原稿を書こうとして標準入力に入れようとしたらエラー。 どうもWindows版だと記事作成に失敗するらしい。 https://blog.tricrow.com/entry/general/git-hatena

Error CreateFile /dev/stdin: The system cannot find the path specified.

じゃあ WSL 版試そうと思って実行すると今度は別のエラー。

blogsync: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found (required by blogsync)

https://ebc-2in2crc.hatenablog.jp/entry/2020/10/14/030150 多分 go get して自分でビルドすれば大丈夫なんだけどどうしようかなぁ。

記事の作成は手動で行う

発想を変えてドラフト記事の作成やモバイルアプリやブラウザから行うことにした。 すぐに blogsync pull すると記事が落ちてくるので

PS> blogsync pull
     fresh remote=2023-03-09 01:29:33 +0900 JST > local=0001-01-01 00:00:00 +0000 UTC
     store C:\Users\ongaeshi\Documents\blog\ongaeshi.hatenablog.com\entry\2023\03\09\012931.md

記事書いて投稿。

PS> blogsync push ongaeshi.hatenablog.com\entry\2023\03\09\012931.md

URL名は変更できない

エントリURLを変更できるか試してみたがこれは無理そうだった

URL: エントリの URL。これは自動的に与えられ、書き換えても効果はありません。

とはいえファイルに保存されるならデフォルトの 2023/03/09 みたいなパス構造は悪くないのでこれでやってみる。

※ 記事の更新が今までに比べて数倍楽...!

ruby.wasm の JS::Object のプロパティ呼び出しをさらに便利にする

ruby.wasm でいろいろ遊んでいます。実験場も作りました。

rubywasm-sample.ongaeshi.me

JS::Object が楽しくて、JSのオブジェクトをRubyから透過的に扱えます。メソッド呼び出しもできるしブロックで関数を渡せたしりします。

require 'js'
JS.eval("return 1 + 2") # => 3
JS.global[:document].write("Hello, world!")
div = JS.global[:document].createElement("div")
div[:innerText] = "click me"
JS.global[:document][:body].appendChild(div)
div.addEventListener("click") do |event|
   puts event          # => # [object MouseEvent]
   puts event[:detail] # => 1
   div[:innerText] = "clicked!"
end

後はプロパティへの読み書きが JS.global[:document]を経由しないといけないのがちょっと難しいのですが、これも method_missing を頑張ればなんとかならないかと色々試していました。

JS::Object でプロパティに対しても関数スタイルで呼べるようにする

本家の method_missing にもう1パッチ当てました。

ハッシュにアクセスしたときに "function", "undefined" 以外が見つかったときはそのまま JS::Object として返します。また :foo= が来たときはまず self[:foo] で JS::Obejct を見つけたのち [:foo] = args.first で代入します。

require "js"

class JS::Object
  def method_missing(sym, *args, &block)
    ret = self[sym]

    case ret.typeof
    when "undefined"
      str = sym.to_s
      if str[-1] == "="
        self[str.chop.to_sym] = args.first
        return args.first
      end

      super
    when "function"
      self.call(sym, *args, &block).to_r
    else
      ret.to_r
    end
  end

  def respond_to_missing?(sym, include_private)
    return true if super
    self[sym].typeof != "undefined"
  end
end

JS::Object が数値や文字列のときは Ruby オブジェクトに変換する

上で大体いいのですが、返された JS::Object をさらにRubyオブジェクトと計算しようとするとエラーになります。

p "foo".length + 3   # Error: JS::Object + Integer は未定義

そこで JS::Object#to_r というメソッドを追加して数値や文字列のときはRubyの数値や文字列オブジェクトに変換してしまいます(よく見ると↑のサンプルでも関数コールやプロパティgetterの後ろで to_r を呼んでいます)

※ 本当は整数と少数に分けて変換したいところだけど JavaScript は全て number で来るのでひとまず to_f に統一・・

class JS::Object
  def to_r
    case self.typeof
    when "number"
      self.to_f
    when "string"
      self.to_s
    else
      self
    end
  end
end

おわり

プロパティもJSと同じ感じでアクセスできるようになり、かなりよくなったのではないでしょうか?

https://rubywasm-sample.ongaeshi.me/access_properties_from_jsobject_in_function_style/

require 'js'
JS.eval("return 1 + 2") # => 3
JS.global.document.write("Hello, world!")
div = JS.global.document.createElement("div")
div.innerText = "click me"
JS.global.document.body.appendChild(div)
div.addEventListener("click") do |event|
   puts event          # => # [object MouseEvent]
   puts event.detail # => 1
   div.innerText = "clicked!"
end

実践Rust入門を読んでいる(2) - bitonic-sorterの実装

実践Rust入門を読んでいる(1) からの続きを読み進める。

Pythonのサンプルスクリプト読んで対話シェルで実行確認。 いよいよ次からRustで書き始める。

Rust Style Guide https://github.com/rust-lang/fmt-rfcs/blob/master/guide/guide.md

first版

graph

sort --> sort
sort --> sub_sort
sub_sort --> sub_sort
sub_sort --> compare_and_swap

テスト失敗時のメッセージ分かりやすい。配列は行に並べるべき。

---- first::tests::sort_u32_ascending stdout ----
thread 'first::tests::sort_u32_ascending' panicked at 'assertion failed: `(left == right)`
  left: `[4, 10, 11, 20, 21, 30, 110, 330]`,
 right: `[4, 10, 11, 20, 20, 21, 30, 110, 331]`', src/first.rs:36:9

テスト込みで書けた。

second版(ジェネリスク対応)

fn sort<T: Ord>(x: &mut [T], up: bool)に書き直せば基本OK。

# ドキュメントをブラウザから開く
$ rustup doc --std

# エラー時にバックトレースを表示
$ RUST_BACKTRACE=full cargo test

# さらに詳細なバックトレースを表示
$ RUST_BACKTRACE=full cargo test

# エラーメッセージを表示
$ rustc --explain 308

引数を列挙型に変更した。列挙型は大分高機能でC++との大きな違い。

second版(列挙対応済み)。

graph

sort --> do_sort
do_sort --> do_sort
do_sort --> sub_sort
sub_sort --> sub_sort
sub_sort --> compare_and_swap

エラーを返す

戻り値を返すところはセミコロンを省略する、returnがなくてもここが終端だ分かりやすい。

クロージャ対応

フィールド変数が同じ名前のときは省略形で書けるの面白いね。間違えたときはちゃんとエラーにしてくれる。

$ cargo test
   Compiling bitonic-sorter v0.1.0 (/home/ongaeshi/WslCode/bitonic-sorter)
error[E0560]: struct `Student` has no field named `ages`
  --> src/third.rs:59:17
   |
59 |                 ages, // フィールド変数が同じ名前のときは省略形で書ける
   |                 ^^^^ help: a field with a similar name exists: `age`

For more information about this error, try `rustc --explain E0560`.
error: could not compile `bitonic-sorter` due to previous error