petitviolet_blog

@petitviolet blog

リッチなデータ構造を扱うためのGemを作ってみた

以前、StructをRubyで実装するというのをやった。

じゃあ次は、ということでEnumとAlgebraic Data Type(ADT)に相当するようなものが欲しいな〜と思ってGemとして作ってみた。

EnumRuby言語標準としては用意されていないが、RailsにはActiveRecord::Enumというものがあり、カラムをenum的に管理しつつ便利メソッドを山程生やしてくれるものがあるが、そういうのじゃなくてデータ構造としてenum欲しいんだよねという気持ち。 そうなるといわゆるADTみたいなやつ、Scalaでいうところのsealed trait + case class(object)のあれがわりと好きなので欲しくなる。

といっても静的にパターンマッチの網羅性チェックとか出来ているわけでもないしどれくらい嬉しいかは不明。 EnumEnumとして、ADTをADTとして明示できるようにしたかっただけかも知れない。

使い方

READMEに書いてあるがここにも書く。

まずrequireしておく。

require 'rstructural'

Struct

まずStruct。名前をずらすためにRstructとしてある。 これは前回の記事のやつ。

module StructSample
  Value = Rstruct.new(:value)

  puts Value.name #=> 'StructSample::Value'
  puts value = Value.new(100) #=> 'StructSample::Value(value: 100)
  puts value == Value.new(100) #=> true
  puts value.value == 100 #=> true

  Point = Rstruct.new(:x, :y) do
    def message
      "Point: [#{x}, #{y}]"
    end
  end
  p = Point.new(1, 2)
  puts p #=> StructSample::Point(x: 1, y: 2)
  puts p.message #=> Point: [1, 2]

  case Point.new(1, 2)
    in Point[x, 2]# pattern match (Ruby 2.7~)
    puts "here! x: #{x}" #=> here! x: 1
  else
    raise
  end
end 

0個以上の属性を持つデータ構造を定義できる。 0個の属性を許す、という点でRuby標準のStructとは異なる、はず。

EOF = Rstruct.new
puts EOF #=> EOF
puts EOF.new #=> EOF
puts EOF.new == EOF.new #=> true

to_s(inspect)した結果が同じなので分かりづらいがnewするとインスタンスにはなっている。

Enum

例としてHTTPステータスコードenumとして定義してみる。 extend Enumでこれはenumですよという表明になり、enumメソッドが生える。

module EnumSample
  module Status
    extend Enum

    OK = enum 200
    NotFound = enum 404
    InternalServerError = enum 500 do
      def message
        "Something wrong"
      end
    end
  end

  puts Status::OK #=> EnumSample::Status::OK(value: 200)
  puts Status::OK.value #=> 200
  puts Status::InternalServerError.message  #=> Something wrong

  case Status.of(404)
  in Status::NotFound
    puts "NotFound!!!" #=> NotFound!!!
  else
    raise
  end
end

enum <Value>という雰囲気で列挙するとクラス変数としてenumを保持しているのでStatus.of(value)で取れる、というところが少し便利。 見つからなかったらnil

一応、値の重複排除をやっていて

module Status
  extend Enum
  OK = enum 1
  NG = enum 2
  KO = enum 1
end

みたいに同じ値でenumを定義しようとすると、

Traceback (most recent call last):
        3: from sample.rb:36:in `<main>'
        2: from sample.rb:59:in `<module:EnumSample>'
        1: from sample.rb:63:in `<module:Status>'
/path/to/enum.rb:12:in `enum': Enum '1' already defined in EnumSample::Status::OK (ArgumentError)

という感じのエラーがraiseされて安心!

ADT

続いては代数的データ構造。 extend ADTでこれはADTですよという表明になり、constdataメソッドが生える。 constは属性を持たない単一オブジェクトで、dataは1つ以上のフィールドを持つデータ型。 Scalaでいうところのobjectcase classの使い分け。

いい例が思えないが点と円と長方形を表現してみる。

module AdtSample
  module Shape
    extend ADT

    Point = const
    Circle = data :radius do
      def scale(i)
        Circle.new(radius * i)
      end
      def area
        3.14 * radius * radius
      end
    end
    Rectangle = data :width, :height do |mod|
      def area
        width * height
      end
    end

  end

  puts Shape::Point #=> AdtSample::Shape::Point

  puts Shape::Rectangle.new(3, 4) #=> AdtSample::Shape::Rectangle(width: 3, height: 4)
  puts Shape::Rectangle.new(3, 4).area #=> 12
  puts Shape::Circle.new(5).scale(2).area #=> 314.0

  case Shape::Rectangle.new(1, 2)
  in Shape::Rectangle[1, Integer => j] if j % 2 == 0
    puts "here! rectangle 1, #{j}" #=> here! rectangle 1, 2
  else
    raise
  end
end

なんかそれっぽい感じで定義出来ていそう。 ただ、ADTはADTとして共通のインタフェース(Enum#ofのような)ものが特に無いためStructでいいじゃん、というのはその通り。

じゃあ共通のインタフェースを持てるようにしよう、ということでやってみた。

module Option
  extend ADT
  None = const
  Some = data :value

  interface do
    def map(&f)
      case self
      in None
        None
      in Some[value]
        Some.new(f.call(value))
      end
    end
  end
end

puts Option::None.map { |i| i * 2 } #=> None
puts Option::Some.new(10).map { |i| i * 2 } #=> Option::Some(value: 20)

interface(&block)で共通のメソッドを定義出来るようにした。
とはいえ実装ではパターンマッチするなりが必要になるので別々に実装するでも十分な気もするが、定義が分散しないというメリットもあるので使えなくはなさそう。 ここまで来るとようやく単なるStructを使うだけではないメリットが...あるかもしれない。

実装について

パターンマッチを使いたかったのでRuby2.7.0で実装している。 Gemのrequirementもそう。

前の記事で実装したRstructural::StructEnumADT両方で使いまわしているので新規でなにかテクニックを用いたかというとそうでもない。

まず、EnumやADTの各クラスにおいて独自のメソッドを生やしたいというのは容易に想像がつくのでblockを渡してメソッド定義できるようにした。 そのためにもまたblockが与えられていたらRstructが生成したClassオブジェクトに対してclass_evalするということをしている。 共通インタフェースをinterface(&block)で定義できるようにしてあるやつも結局class_evalしているだけではある。

またenumdataメソッド経由でRstruct.newを実行しているためcallerでクラス名を決定するところが壊れるので__callerキーワードで雑にバイパス出来るようにしている。

ADTについて実装をはりつけてみるとこんな感じ。

module Rstructural::ADT
  def self.extended(klass)
    klass.class_variable_set(:@@adt_types, [])
  end

  def const(value = nil, &block)
    if value
      Rstructural::Struct.new(:value, __caller: caller, &block).new(value)
    else
      Rstructural::Struct.new(__caller: caller, &block).new
    end.tap do |k|
      self.class_variable_get(:@@adt_types) << k
      def k.name
        self.class.name
      end
    end
  end

  def data(*fields, &block)
    Rstructural::Struct.new(*fields, __caller: caller, &block).tap do |k| 
      self.class_variable_get(:@@adt_types) << k 
    end
  end

  def interface(&block)
    self.class_variable_get(:@@adt_types).each do |t|
      case t
      in Class
        t.class_eval(&block)
      else
        t.class.class_eval(&block)
      end
    end
  end
end

constは値を持たせることも出来るし、必須ではない。内部ではそれぞれごとにClassオブジェクトを生成しているので==で別のconst同士を比較するとちゃんとfalseになるので実質singleton object的に扱える。

dataは任意の個数のフィールドを持つタプルを生成する。こちらは同じ型でかつ全フィールドが==trueなら、==trueになるという実装をRstructural::Structでしてある。

感想

Rubyには静的型チェックがなくデータコンテナとしてのClassを定義するのも面倒だしHashがインタフェースになりがちだけど少しでもインタフェースに対して制約をつけるためにはこういうのがあると便利かなとは改めて思った。

RubyのStructをRubyで実装する

RubyのStructはこういうやつ。

$ irb
irb(main):001:0> Point = Struct.new(:x, :y)
irb(main):002:0> Point.new(1, 2)
=> #<struct Point x=1, y=2>
irb(main):003:0> Point.class
=> Class 

Structという名前だけあり、データを持たせておくのに便利なクラスを作ることができる。 実装はここ。 ruby/struct.c

ぼくCよめないのでRubyで実装してみよう!ということでやってみた。

やってみた実装はこんな感じ。

module Rstruct
  def self.new(*attributes)
    begin
      names = caller.map do |stack|
        # ".../hoge.rb:7:in `<module:Hoge>'"
        if (m = stack.match(/\A.+in `<(module|class):(.+)>.+/))
          m[2]
        end
      end.reject(&:nil?)
      file_name, line_num = caller[0].split(':')
      line_executed = File.readlines(file_name)[line_num.to_i - 1]
      names << line_executed.match(/\A\s*(\S+)\s*=/)[1] # "  Point = Rstruct.new(:x, :y)\n"
      class_name = names.join('::')
    rescue
      class_name = 'Rstruct'
    end
    Class.new.tap do |k|
      k.class_eval <<~RUBY
        def initialize(#{attributes.join(", ")})
          #{attributes.map { |attr| "@#{attr} = #{attr}" }.join("\n")}
        end

        #{attributes.map { |attr| "attr_reader(:#{attr})" }.join("\n")}

        def inspect
          if #{attributes.empty?}
            "#{class_name}"
          else
            __attrs = Array[#{attributes.map { |attr| "'#{attr}: ' + @#{attr}.to_s" }.join(", ")}].join(", ")
            "#{class_name}(" + __attrs + ")"
          end
        end

        alias :to_s :inspect

        def deconstruct
          [#{attributes.map { |attr| "@#{attr}" }.join(", ")}]
        end
      RUBY
    end
  end
end

なかなか辛いコードになったが、Classを作るためのClassを作るのでメタプロっぽくなるのは仕方がないよね。
Rstruct.newの引数にはattributesとしてArray[Symbol]を受け取ることを想定していて、そのattributesを頑張ってClass.newclass_evalで文字列としてメソッドやらattr_readerやらにねじ込んでいる。

地味に大変だったのはクラス名を決めるところ。
self.class.nameでやるとModuleとなってしまう等うまく取れなかったので、callerで取得できるbacktraceから呼び出し元のプログラムを文字列として扱い、正規表現でクラス名を取り出すという野蛮なことをしている。
irbやpryだと死んでしまうのでrescueしてごまかしている。 mapしてreject(&:nil?)とかやるのループがもったいないからScalaでいうところのcollectPartialFunctionがほしい。あるいはflat_mapで潰してほしい。。

特にデバッグが面倒くさかったがなんとか動くようになり、こんな感じになった。

Value = Rstruct.new(:v)
v =  Value.new(100)
puts v
#=> Value(v: 100)
puts v.v
#=> 100

module Parent
  Point = Rstruct.new(:x, :y)
end

p = Parent::Point.new(10, 20)
puts p
#=> Parent::Point(x: 10, y: 20)
puts p.y
#=> 20

deconstructを実装していることによってパターンマッチが動く!

case Parent::Point.new(10, 20)
in Parent::Point[i, y] if i % 2 == 0
  puts "y: #{y}"
else
  puts "not matched"
end
#=> y: 20 

すごい、すごいぞパターンマッチ!

RailsとReact hookでGistみたいなWebアプリを作りかけた

Ruby(Rails)の練習がてらGistみたいなやつを作ろうとしていた(過去形)。
https://github.com/petitviolet/acoder

モチベーションはこの辺

  • Ruby on Rails 6を触ってみたかった
    • ActionTextとか
  • React hooks触ってみたかった
    • TypeScript
  • RubyでもGraphQLやってみたかった
    • Getting Startedくらい

作りかけた?

飽きた。
個人開発だし真面目にcommitしたりしなかったりPR作ったり作らなかったりでめっちゃ雑なプロセスだったと反省している。

以下メモと感想。

API

Ruby on Rails 6.0で動いている。

ActionTextを捨てた

目玉機能っぽいActionTextを使ってコードスニペットのエディタを作ろう!と思っていたがReactで書いているクライアントに対してActionTextを使って作ったビューをいい感じに埋め込むことが出来ず諦めた。

こういうことやりたい人っていないのかな、参考になりそうなのがうまく見付けられなかったし面倒くさかったので。

自前認証 -> devise_token_authで認証

最初は自前でTokenを使った認証を実装したが面白くないのでdeviseでやってみた。
が、cookie認証を実装する元気はなくヘッダにtokenを埋め込んで認証したかったので、deviseだけだとどうしても辛くdevise-token-authを使ってやってみることにした。

とにかくdeviseどころかRailsにまだ慣れていない自分には動作を追いかけるのが大変で辛かった。

確かにこういった認証機構を自前で実装するのは大変なのでdeviseみたいにオールインワンでやってくれるのは便利だと思う反面、結局のところ実装を読んで理解して使わないといけないだろうしそれだったら自分で実装した方がシンプルになるのでは...という感想。
少なくともぜひ使いたい!という感想は持てなかった。

GraphQL

ScalaではわりとGraphQLやっていたけどRuby(Rails)でも素振りしておくかというところ。
こんな感想を持ったりしていたのがコンテキスト。

とはいえとりあえず/graphql叩けるようにした、以上のことはまだしていないので素振りの回数は足りてない。

フレームワークのおかげでわりとさくっとGraphQLエンドポイントを動かすことが出来て楽っぽい。
Scalaでやったときは型のおかげでじっくり安全なAPIを作れたが、一方Rubyでやるとよく分からんがさくっと動いたみたいな感想になり、まあそれがRubyの良さでしょうと割り切ってしまえば問題なさそう。 例えばContextがただのHashだったり。 GraphQLそのものはある程度わかっているつもりだが、graphql-rubyの使い方を習得する必要がありそれはまた必要が発生してからでいいや。

Front

ざっと要素を並べるとこのあたり

  • TypeScript
  • React(hook)
  • styled-component
  • react-bootstrap, font-awesome
  • eslint + prettier

storybookなるものも使ってみようと思っていたが忘れていた。
TypeScript、文字列でkeyの存在チェックして暗黙的な型キャストみたいなことが出来るようになっていたのは発見。
けど、そんな文字列でマッチさせるなんてやり方よりパターンマッチしたいよね。 そして折角だからasync/awaitも一応使ったがあまりに小さいアプリなので恩恵は感じられなかった。 個人的にはPromiseのまま使う方が好き。というのもawaitって要するにAwait.result(f, Duration.Inf)でしょという理解なので雑に使うには怖くない?

UIのデザイン力は無いし身に付ける気も今のところ全くないのでbootstrapでレイアウトだけ適当に弄ったりした。

React hook

Vue使うかも悩んだけどhook触りたかったので。
以前React触ってたときはReduxとかpropsのバケツリレーが大変だった記憶があり、React hookを使うとなんか良さそうに書けるんじゃないかと思って手を出した。

ひと通り動かしてみて基本的なhook(useState, useEffect, useReducer, useContext, useMemo, useCallback)は触れるようになった。
単なる関数を組み合わせてコンポーネントを組み立てて、ひいてはアプリケーションを組み立てていく感覚は気持ち良い。

実装だとこんな感じ。useForm.tsx

Webpacker -> Webpack

最初はRailsにべったりなrails/webpackerを使っていたのだがいかんせんよくわからず自由を求めてwebpackに移行した。

大体変更はこの辺: #1 Webpacker to webpack
必要な変更が多くて難儀したがなんとか。

TrixEditor -> AceEditor

ActionTextは諦めたけどせめてTrixEditorを使ってみるか!と思っていたが使い勝手があまり良くなく感じ、AceEditorに乗り換えたりした。

デプロイ

Herokuで動かせるようにしてみた。 RailsといえばHeroku!

apiとfrontendという2つのプロジェクトが1つのリポジトリに置いてあるのでmakeでデプロイ出来るようにした。 バージョンをgitハッシュとしたかったのでデプロイと同時に環境変数に埋め込んで参照するようにしてみたらわりとよかったかも。

フロント力が足りずwebpack --mode=productionするとineffective mark-compacts near heap limit allocation failed - javascript heap out of memoryなどという無力感あるエラーを吐いて死んだ。 --max_old_space_sizeであったり--gc_intervalの値を調整するなどしたがダメで、よくわからないままwebpack --mode=developmentしたら動いたなんだそれ。

CI

時代はGitHub Actions!
いい加減触っておくか、みたいな感覚でYAMLファイルを書いてみて動かせるようにしてみた。
必要十分かなという感想。
肝心なテストはほとんどない。