以前、StructをRubyで実装するというのをやった。
じゃあ次は、ということでEnumとAlgebraic Data Type(ADT)に相当するようなものが欲しいな〜と思ってGemとして作ってみた。
EnumはRuby言語標準としては用意されていないが、RailsにはActiveRecord::Enumというものがあり、カラムをenum的に管理しつつ便利メソッドを山程生やしてくれるものがあるが、そういうのじゃなくてデータ構造としてenum欲しいんだよねという気持ち。
そうなるといわゆるADTみたいなやつ、Scalaでいうところのsealed trait + case class(object)のあれがわりと好きなので欲しくなる。
といっても静的にパターンマッチの網羅性チェックとか出来ているわけでもないしどれくらい嬉しいかは不明。
EnumをEnumとして、ADTをADTとして明示できるようにしたかっただけかも知れない。
使い方
READMEに書いてあるがここにも書く。
まずrequireしておく。
require 'rstructural'
Struct
まずStruct。名前をずらすためにRstruct
としてある。
これは前回の記事のやつ。
module StructSample
Value = Rstruct.new(:value)
puts Value.name
puts value = Value.new(100)
puts value == Value.new(100)
puts value.value == 100
Point = Rstruct.new(:x, :y) do
def message
"Point: [#{x}, #{y}]"
end
end
p = Point.new(1, 2)
puts p
puts p.message
case Point.new(1, 2)
in Point[x, 2]
puts "here! x: #{x}"
else
raise
end
end
0個以上の属性を持つデータ構造を定義できる。
0個の属性を許す、という点でRuby標準のStructとは異なる、はず。
EOF = Rstruct.new
puts EOF
puts EOF.new
puts EOF.new == EOF.new
to_s(inspect)
した結果が同じなので分かりづらいがnewするとインスタンスにはなっている。
例として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
puts Status::OK.value
puts Status::InternalServerError.message
case Status.of(404)
in Status::NotFound
puts "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ですよという表明になり、const
とdata
メソッドが生える。
const
は属性を持たない単一オブジェクトで、data
は1つ以上のフィールドを持つデータ型。
Scalaでいうところのobject
とcase 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
puts Shape::Rectangle.new(3, 4)
puts Shape::Rectangle.new(3, 4).area
puts Shape::Circle.new(5).scale(2).area
case Shape::Rectangle.new(1, 2)
in Shape::Rectangle[1, Integer => j] if j % 2 == 0
puts "here! rectangle 1, #{j}"
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 }
puts Option::Some.new(10).map { |i| i * 2 }
interface(&block)
で共通のメソッドを定義出来るようにした。
とはいえ実装ではパターンマッチするなりが必要になるので別々に実装するでも十分な気もするが、定義が分散しないというメリットもあるので使えなくはなさそう。
ここまで来るとようやく単なるStruct
を使うだけではないメリットが...あるかもしれない。
実装について
パターンマッチを使いたかったのでRuby2.7.0で実装している。
Gemのrequirementもそう。
前の記事で実装したRstructural::Struct
をEnum
、ADT
両方で使いまわしているので新規でなにかテクニックを用いたかというとそうでもない。
まず、EnumやADTの各クラスにおいて独自のメソッドを生やしたいというのは容易に想像がつくのでblockを渡してメソッド定義できるようにした。
そのためにもまたblockが与えられていたらRstruct
が生成したClass
オブジェクトに対してclass_eval
するということをしている。
共通インタフェースをinterface(&block)
で定義できるようにしてあるやつも結局class_eval
しているだけではある。
またenum
やdata
メソッド経由で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がインタフェースになりがちだけど少しでもインタフェースに対して制約をつけるためにはこういうのがあると便利かなとは改めて思った。