GraphQLのライブラリをなぜか書いている話 GraphQLは良いぞ!

RailsのモデルをフロントのJavaScriptで持っているデータとリアルタイム同期させるためのgem(ar_sync)を作っている。
そのために、俺の考えた最強のシリアライザ(ar_serializer)を作っていたら、ほぼGraphQL互換(mutationはないけど)ぽい作りになった。
せっかくなのでGraphQLのパーサを書いて、GraphiQLが動くところまでできた。 GraphQLは良いぞ
f:id:tompng:20180828231056p:plain

ar_serializerについて

https://github.com/tompng/ar_serializer
queryは、as_jsonとかincludesの引数みたいな(見慣れたものに近い)記法で書く
GraphQLのqueryと機能的にはだいたい同じ
モデルにガリガリfield書いてく 個人的には書きやすいと思う(typeも指定しなければ適当にassociationとかから引っ張ってくる)
N+1回避しつつqueryで柔軟にjson作る、のが目的で作っているので、graphql-batch内臓みたいなもの
aliasの内部表現がgraphqlと逆(response_key:field_name field_name: {as: response_key} ちょっとここは直したい)
権限管理用にnamespaceってものをつけた ArSerializer.serialize model, query, use: current_user.namespace_for_role
GraphQLのparserは気合いで書いたので、対応してない記法も多分ある(graphql-rubyも文字列escape周り似たようなものだけど)

雑感

GraphQL、すごくよくできてる。最高!

少なくともqueryの部分は。
理想のものを作ろうとした結果、もうすでにあるものにたどり着いて、自分は何をやっていたんだろうという気分にさせられた。

graphql-ruby 大変そう

ActiveRecord(かActiveModel)で使うの前提だとしてgraphql-rubyは記述がちょっとだるそう
graphql-batch 微妙じゃない?(graphql-rubyが提供すべき機能がないせいで無理してない?)

Query Language 別にJSONで良くなかった?

argumentsの書式がjsonじゃないので、文字列で埋め込めない
{post(${JSON.stringify(param)}){name}} などができない。代わりに、 {post($param){name}} とvariablesを使えばいいけど
それならもともと独自言語じゃなくてJSONだったらよかったんじゃないかなぁ、param埋め込みも自由だしvariablesなんて要らなかったのでは
確かに読みやすい言語だけど、そのparserを各言語用にみんなそれぞれ作らなきゃいけないし...

mutationはメリットが良くわからない

accepts_nested_attributes_forでできることを別に解決してないように見える
postに対して、commentを10件つける、ということがGraphQL使えばうまくいく、わけでも無さそう
RESTで、返り値(@post)に対するgraphqlのqueryを指定できれば良くない?
(取得系はGraphQL 更新系はREST+返り値のquery がいいんじゃないかな、特にRailsだと)

Relay connectionsもちょっと引っかかるところがある

ページネーション実現するならこうなるだろうな、とは思うけど
複数のpostsに対して、commentsを(after指定で)取れてもあまり意味無さそうで、
特に使わない方向に自由度が高くなっているような(別にいいんだけど、なんかもやっとする)

{
  allPosts {
    comments(first: 5, after: "id15") {
      edges{
        cursor
        node{ id text }
      }
    }
  }
}

REST apiでpagination実現+データの形式はGraphQLのqueryで指定、でも良いような気がする

どうしよう

ar_syncを、ar_serializer捨ててgraphql-ruby依存で作るべきかどうか悩み中(答えはnoな気がする)
とはいえ、mutationもrelayもまだ使ってないから使ったら感想変わるかもしれない

TRICKボツ案集

TRICK2018に出そうと思ったけど作らなかった、途中まで作ってやめた、ボツ案集

ruby迷路パズル

StartからGoalまで、実行可能な順にたどるルートを探す迷路 実行すると(探索して)正解を表示する
実験段階で探索全然終わらなかったから諦めた

# イメージとしてこんな感じ(左がコード、右が正解)
pu))ell pu  ell
)ts:h)o  ts:h o
def*[{!       !

アスキーアートでバージョン表示するQuine

rubyバージョン間の挙動違いを使って分岐する
判定コードのリストは作ったけどRUBY_VERSION==でできることを長ったらしく書いてるだけな気がしてやめた

# こういう判定コードを全バージョン間で見つけたかった
def a x:
1;end
def a(x: 1); end # 2.0
def a(x:); 1; end # > 2.1
# 2.1
true.frozen?
# 2.2
class Object;def dig*;end;end;[1].dig(0)
# 2.3
(1<<10).class==(1<<100).class
# 2.4
1.round(1)/2
# 2.5

成長する植物Quine

育成要素があるQuine 種から育って、花が咲いて、種を落として元に戻る(たまに枯れてもいいかも)
結局やらなかった(成長するQuineはTRICK2015で既出だし いつかやるかも)

bmp画像とrubyのpolyglot

invalid multibyte char (UTF-8) をうまいこと避けないといけない
けど__END__とか#使えば楽勝ぽいから面白みが少ない

裏側に人間がいてやっと成り立つChatBot Webサーバ

偽チェスロボットの「トルコ人」みたいに、裏に人がいるWebサーバ
存在意義が意味不明だけど、あんまりTRICK向きじゃない気がした
一応動くところまでは作ったものがこちら
https://gist.github.com/tompng/0ff8cf33046cb312dd66d55195c91eee
実行して、ブラウザでlocalhost:3000を開くとテキスト会話できる
javascript無しでブラウザ画面をリアルタイム更新する技を使ってるところが見所
(javascript完全排除できるけどpost後のinputのクリアにだけ使った)

TRICK FINAL 5作品入選

RubyKaigi 2018で、意味不明なプログラムコンテスト「TRICK FINAL」に6作品投稿して5作品が入選しました!
1つくらいは入選するかなと思ったけど予想以上に評価されて嬉しい(3位悔しい)

https://git@github.com/tric/trick2018

どうやって作ったか、git log公開します。
興味あればどうぞ。難読化前のコードも含まれてるので読みやすいはず。
(試行錯誤していけそうだぞ、とわかった時にinitial commitしてるのでその時点で割と出来上がってるけども)
イデアは過去にやったことのブラシュアップだったり考えたけどやらなかったのを思い出して、記憶のゴミ箱から拾い集めました。

1作目 png viewer(3位)

https://gist.github.com/tompng/fc18db09114f4e3ba52de593303b8580
フォーマット(Zlib+method_missingでアスキーアート化)をまず決めて、そのあとでネタとして画像viewerを選んだ
予約語が現れるのを避けるためにSyntaxErrorをcatchして再生成試すようにした。もっと別の方法があったかもしれない

2作目 infinite monkey(選外佳作)

https://gist.github.com/tompng/e24fd79f303393244d003225baba0931
ランダムな6文字をevalしたらたまたまruby tric と出るような乱数のseedを力任せにCPUぶん回して探すことで作った

3作目 wineglass(5位)

https://gist.github.com/tompng/c87ff0b298867491ce47343fe9a5a4e2
method_missing -, +, -@, +@を使ってワイングラス・おちょこ用のDSLを作った
整形は割と簡単で、まずある程度コードゴルフ→整形→短くしたコードを一部崩して長くして瓶の形を細かく調整
ワインボトルで余った文字を水滴の部分で吸収できて整形しやすかった。

4作目 christmas(11位)

https://gist.github.com/tompng/923ad9fb233cdb37fbc032ff6200c98e
Merryの部分が3DデータをMath系の関数で表現したもの、残りがレンダラになってます。
レンダリングの前処理がすごく重いので、その待ち時間にMerry Christmas!の文字を出してます。(プログレスバーみたいな感じ)
かなり力を入れたけども、rubyらしい黒魔術成分はないです。

5作目 brainf*ck(7か8位)

https://gist.github.com/tompng/0a9a407b57d34b75c0b46c6a8b7e56c5
部品を横に繋げても動くコード(かつQuineみたいな)
誰もやってないすごい良いアイデアのつもりだったけどColinさんのライフゲームが上下左右連結で完全に上を行ってた

6作目 whitespace(10位)

https://gist.github.com/tompng/c445259784f99a5bb52d44f9092b450c
全角の空白文字を混ぜたら見た目と実際のコードが乖離できるサンプル。
空白をこっそり混ぜた、一見まともに見えるプルリクを送りつけてmergeさせる攻撃が成立する実証コードかもしれない

1位、2位の二人が本当に強かった。
アニメーション(escape sequence入り)がQuineにできるってのがかなり衝撃だったし、
縦横繋げるのも今後は当たり前の技術になってしまうのかなぁ

短くてすごいコード書ける人にはすごく憧れる(書こうと試してできたのが佳作のあれ)
parse.y読み込んだとか なるほどそこまでやれば思いつく(かもしれない)のかー
次回があればああいうのを書きたいな

YAPC::Okinawaに参加してきた

前夜祭

前夜祭で「綺麗なコードの書き方」についてLTしてきました。

(先週あったLTのイベント(okinawa-nuyaga)で話した内容をブラッシュアップして)
内容はタイトル詐欺で、ちょっと錯乱拡大解釈した、綺麗なコードについての話です。
out.bmp
https://github.com/tompng/executable_image
(perl、配列、リファレンスあたりからあんまり理解してなくて結構苦労した)

本編

sinya8282さんの文脈自由文法の話が楽しみだった&面白かった。
あとは最近気になってるGraphQL、Dan KogaiさんのPerl6の数字の話を聞いたり、勉強会・コミュニティの話聞いたり、 準備片付け、疲れで仮眠など。

GraphQLの話

client側よりの話だった
未来だと思うんだけど、既存のRDBRailsにどう載せるの、ってのが気になってる。
(N+1起きるでしょ、notificationsあたりの実装どうなってるの、繋ぎの別技術がしばらく要るのでは)
apollo serverが何をしてくれるものなのかわかってなかったりあまりついていけなかった。
backendのDB何使ってるんだろ

HTTP2の話(聞けなかった)

HTTP1がHTTP2より速いケース(パケットロス)もあるとか(をTLでちらっと見ただけ)
TCPが賢くないのが悪いような気がした。(だって同じ伝送路だよ、おかしい)
再送がすごく遅れてもいいから帯域ほしい、ってのはTCPのやってくれることとは目的が違うから
TCP以外の別のプロトコルが速いってことかな(TCP複数接続束ねたものを別のプロトコルと見立てたり)
HTTP2 over TCP vs HTTP1 over TCPx10

文法と曖昧さの話

プログラミング言語の曖昧さ
if elsif if else の最後の else はどのifにかかるのか
rubyだと method1 arg1,arg2,method2 do end の `do end ``はどっちにかかるか
ありえる構文木が複数あっても、最初に見つかった方を使うから1つに確定してると思うんだけど
昔それのバグ(ブロックが時々method2にかかる)踏んだりしたから、やっぱりなるべく無曖昧であってほしい。
C言語とか、仕様自体が曖昧(実行順序が未定義)とかなかったっけ?実行順序はまた別かもしれないけど。

無曖昧な言語の組み合わせが何通りあるかの話

s → ε | (s)s な書き換えでできる言語の例だと(= ((()()))() みたいな括弧が対応してるもの)
プログラム的には
組み合わせ(n文字) = Σ 組み合わせ(i文字) * 組み合わせ(n-2-i文字)
みたいに再帰的に計算したくなるけど、
S(z) = 1 + z*S(z)*z*S(z) # S(z)は母関数 組み合わせを係数にもつ多項式
でできるぽい。いきなりεを1に、|を+に置き換え出した時は頭の中に?が沢山並んだ。
(多項式の積を展開する部分がΣに、z*z(係数を2つずらす)が(n-2-i)の2つずらしに、1が再帰の終了条件に対応してそう)
表現が違うだけだけど四則演算が便利に使えてすごい(小並感 数学忘れてる感)
これが何にどう使えるのかあたりの話はよくわからなかった。

memo=[];
count = -> n {
  memo[n] ||= n.odd? ? 0 : n.zero? ? 1 : 0.step(n-2,2).sum{|i|count[i]*count[n-2-i]}
}

数値の話

1.2345 で書いた数値が全部floatじゃなくrationalになるのは、一見便利そうだけどあんまりメリットないよなー、とか
log(2)とかも全部floatじゃなくlogに2を渡したものとして内部で表現してたら面白いなーって話を他の参加者としたりした。

> 1/3 == Num(1/3)
True
> 1/3 == 0.3333333...(数十個)
False

がTrueにならなくて不思議だったけど(限界超えたらfloatになるんじゃないの?と)

0.33333333333333333333  #=> 0.33333333333333333333
0.333333333333333333333 #=> 0.33333333333333330372739
# (1行目より2行めの方が小さくなったように見えるけど<比較すると大小関係正しいので表示上の問題?)
(0.33333333333333333333).WHAT #=> (Rat)
(0.33333333333333333333*1).WHAT #=> (Num)
0.333333333333333*3 #=> 0.999999999999999 (Rat)
0.3333333333333333*3 #=> 1 (Rat) 正確な値じゃなくなった

なるほど?
大小関係逆転するのも見つけた。

my $a = 0.33333333333333333333333;
my $b = 0.333333333333333333333333;
say($a > $b) #=> True
(1/2**62/2).WHAT #=> (Rat)
(1/2**62/4).WHAT #=> (Num)

分母は64bitまで

ボランティアスタッフ

してきました。他イベントも含めボランティアスタッフをすること自体初めてで、かなり疲れた(今週体調よくなかったのが主因だけど)
ボランティアスタッフではないけど早めに会場に着いて手伝う、みたいな手軽なボランティアなら今後もやれそう(だけどそれじゃ回せないだろうからなー)

コアスタッフの皆さんありがとうございました。

来週は

沖縄Ruby会議02があるのでぜひ http://ruby.okinawa/okrk02/

RailsのActionCableでリアクティブプログラミング

コメントを投稿したら、いいねをしたら、他の人のブラウザ画面にリアルタイムに反映される
そういうWEBアプリを簡単に作れる仕組みを作りたくて今gem作ってます。
https://github.com/tompng/ar_sync AR(Active Record)Sync 名前は変わるかも)

ActionCableで自前でbroadcast頑張ればできるけど、データ同期するモデルが増えると複雑になって破綻すると思うので、
宣言的に定義しておけば全部うまくやってくれる仕組みが必要なはず。

このgemでできること

  • 欲しいJSONデータの形をクライアント側からリクエストできる
  • だけどSQLのN+1問題が起きない、または楽に回避できる
  • そのデータがRails側のmodelと同期していてリアルタイムに更新されていく
  • Vue(もしくはReact)と繋げば、リアルタイムにデータ同期されるWEBアプリの完成

多分、GraphQLやMeteorがやってることに近いかな
Railsと何らかのJSフレームワークを使ってるところに簡単に乗せれるところを目指してます。

使い方

モデルの定義

データ同期するカラムと親子関係を記述

class User < ApplicationRecord
  has_many :posts
  sync_self
  sync_data :name
  sync_has_many :posts # 逆の関係(sync_parent)も定義する
end

class Post < ApplicationRecord
  belongs_to :user
  sync_self
  sync_data :title, :body, :created_at, :updated_at, :user
  sync_parent :user, inverse_of: :posts # sync_has_many :postsの逆
end

API作成

class SyncApiController < ApplicationController
  include ARSync::ApiControllerConcern
  api(:profile) { |_params| current_user }
  api(:user) { |params| User.find params[:id] }
  api(:post) { |params| Post.find params[:id] }
end

さくっとView作成

postsまで、postsについたcommentまで、commentについたいいね数まで、など
任意の深さのデータを要求して、vueに渡すだけ

<script>
  new ARSyncData({
    currentUser: {
      api: 'profile',
      query: ['id', 'name', { posts: ['title', 'created_at'] }]
    }
    post: {
      api: 'post',
      params: { id: 3 },
      query: ['title', 'body', 'created_at']
    }
  }).load((vueData) => {
    new Vue({ el: '#foobar', data: vueData })
  })
</script>
<div id='foobar'>
  <h1>
    {{post.title}} by {{post.user.name}}
    <small>date: {{post.created_at}}</small>
  </h1>
  <p>{{post.body}}</p>
  <hr>
  <h2>my posts</h2>
  <div v-for='post in currentUser.posts'>
    <a :href="'/posts/' + post.id">{{post.title}}</a>
    <small>date: {{post.created_at}}</small>
    <!-- remote: true なformやボタンを足すだけで動く -->
    <a :href="'/posts/' + post.id" data-remote=true data-method=delete>delete</a>
  </div>
</div>

仕組み

Rails

ActiveRecordのafter_commitでhookをかけて、
そのデータが必要なクライアントにだけ、データのpatchをActionCable経由でbroadcast

JS側

初期データをAPI経由で読み込んで、ActionCableで必要なkeyだけsubscribe
送られてきたpatchをデータに適用して、最新に保つ
VueやReactがそれを勝手に画面に反映してくれる

デモページ

http://arsyncdemo.herokuapp.com/
2つのウィンドウで開くと、いいねやコメントがリアルタイムに反映される様子が見れます
デモのソースコード
https://github.com/tompng/ar_sync/tree/master/sampleapp

今後

  • それ、**でできるよ みたいなのあるのかな?
  • 関連してそうなライブラリとか色々調べる
  • どうしよう

デスク完成

脚を片方ずつダボとボンドで接着&固定して
f:id:tompng:20171202174447j:plain
f:id:tompng:20171204202438j:plain
テーブルの脚は完成
f:id:tompng:20171207185140j:plain

あと、天板に電動ヤスリかけて、塗装して(写真撮り忘れた)
滑り止めスポンジを挟んで天板を乗せて(あとで固定するかも? しないかも?)

完成(ギー沖にあるよ)

f:id:tompng:20171211110334j:plain

今のところぐらつきは無さそう
次作るなら、作りやすいように形ちょっとだけ変える https://tompng.github.io/ghtable/?ver=2

カンマ区切り、shellscript vs ruby

Qiitaのこの記事を読んだ

シェルスクリプトを何万倍も遅くしないためには —— ループせずフィルタしよう - Qiita

「カンマ区切りデータの2列目に含まれる小文字の母音("aeiou")を数える」 というお題で速さを比較してたので、せっかくだからrubyで書いて比べてみた

テストデータ

File.write 'data.dat', ([%(abc,atmark,123), %(def,colon,456)] * 1000_000).join("\n")

ruby vs shellscript

#!/bin/sh
cut -d, -f2 |tr -dc 'aeiou' |wc -c
puts STDIN.each_line.map { |line| line.split(',', 2).last.count('aeiou') }.sum

結果

./v5.sh < data.dat       1.86s user 0.03s system 175% cpu 1.079 total
ruby slow.rb < data.dat  2.10s user 0.05s system  98% cpu 2.188 total

rubyの方が倍くらい遅かった

ruby(リベンジ)

count = 0
target = 'aeiou'
s = ''
STDIN.each_line do |line|
  s << line.split(',', 2).last
  if s.size > 0x10000
    count += s.count target
    s = ''
  end
end
puts count + s.count(target)

String#countは短い文字列に対して何度も呼ぶよりconcatしてまとめて呼ぶ方が良さそう

1000000.times{''.count 'very_long_string'} #=> 0.73秒
1000000.times{''.count 'a'} #=> 0.16秒
('a'*1000000).count 'a' #=> 0.001秒

結果は...

./v5.sh < data.dat    1.86s user 0.03s system 175% cpu 1.079 total
ruby a.rb < data.dat  1.28s user 0.05s system  97% cpu 1.363 total

あとちょっと及ばない
それとsplitしないバージョンも書いてみたけど速度出なかった

ここでshellscriptの171% cpuに注目 rubyでも並列させてみることに

rubyリベンジ2

path = ARGV[0]
filesize = File.size(path)
cores = 4
pipes = Array.new cores do |id|
  per = (filesize + cores - 1) / cores
  from = per * id
  to = [per * (id + 1), filesize].min
  rio, wio = IO.pipe
  fork do
    file = File.open(path, 'r')
    file.seek from
    length = to - from
    length -= file.gets.size if id != 0
    count = 0
    target = 'aeiou'
    s = ''
    file.each_line do |line|
      length -= line.size
      s << line.split(',', 2).last
      if s.size > 0x10000
        count += s.count target
        s = ''
      end
      break if length < 0
    end
    wio.puts count + s.count(target)
  end
  rio
end
puts pipes.map { |io| io.gets.to_i }.sum

(STDINやめてFileにするインチキだけど)
fork→file.seekして4分割それぞれ数える→集約、分割の境界をうまいことどうにかする

./v5.sh < data.dat    1.86s user 0.03s system 175% cpu 1.079 total
ruby b.rb < data.dat  2.51s user 0.07s system 329% cpu 0.782 total

とようやくshellscript超えできた

他の結果

go速い、C速い(-O3つけなかったらgoよりだいぶ遅くなるけど)
goroutineで(cpu327%も使って、ほんの少しだけだけど)Cより速く動いた
SSDかHDDかでどれくらい変わるんだろ

% cc -O3 a.c && time ./a.out < data.dat
% go build a.go && time ./a < data.dat
% go build b.go && time ./b < data.dat
% time ./v5.sh < data.dat
% time ruby a.rb < data.dat
% time ruby b.rb data.dat # これだけSTDINじゃなくfileをコマンドライン引数で渡してる

GO B:   0.13s user 0.01s system 327% cpu 0.043 total 25.1倍
C:      0.04s user 0.01s system  93% cpu 0.053 total 20.4倍
GO A:   0.05s user 0.01s system  93% cpu 0.062 total 17.4倍
RUBY B: 2.51s user 0.07s system 329% cpu 0.782 total 1.38倍
SHELL:  1.86s user 0.03s system 175% cpu 1.079 total 基準
RUBY A: 1.28s user 0.05s system  97% cpu 1.363 total 0.79倍

コードはこちら https://gist.github.com/tompng/2b26c0bf1c1f81b0f8827271d1cb900a
(b.goが特に長ったらしいけどうまいやり方ないかなぁ)