GaucheでRubyのArray#product同等の関数

結論

Gaucheではcartesian-productを使うと良い。
以下は、この関数を見落としていたことによる悪戦苦闘の記録。

背景

業務中にちょっとしたスクリプトで「複数の配列の直積」が必要になった。

例:

  • 入力: [[1, 2], [3, 4], [5]]
  • 出力: [[1, 3, 5], [1, 4, 5], [2, 3, 5], [2, 4, 5]]

2配列ぐらいなら2重ループでもいいのだけど、6配列ぐらい必要だったのでループはきつかった。

RubyのArray#product

業務中は作業速度優先で、さっさとRubyで対応した。Rubyならそのものproductというのがある。

irb(main):001:0> [1, 2].product([3, 4], [5])
=> [[1, 3, 5], [1, 4, 5], [2, 3, 5], [2, 4, 5]]

Gaucheにproductがあるのか調べる

業務後にGaucheでどう書くか調べてみた。
冒頭に書いた通り標準ライブラリに存在するのだけど、この時点では見落としていた。

まず(apropos 'product)で引っかからないので早とちりしたが、ドキュメントにちゃんと書いてあった。

Macro: apropos pattern :optional module
名前がpatternにマッチするような定義された変数のリストを表示します。 moduleにモジュールオブジェクトまたはモジュール名を与えた場合は、 そのモジュール内で定義されている変数のみが表示されます。moduleが 省略された場合は、カレントモジュールから「見える」変数が全て表示されます。

カレントモジュールから「見える」変数が全て表示されます。

cartesian-productutil.combinationsにあるので、useしないと見えなかった。
(どこにあるか分からない段階でuse出来るわけはないので、これは仕方ないが)

gosh> (use util.combinations)
gosh> (apropos 'product)
cartesian-product              (util.combinations)
cartesian-product-for-each     (util.combinations)
cartesian-product-right        (util.combinations)
cartesian-product-right-for-each (util.combinations)

とはいえDashproductで引っ掛ければcartesian-productが引っかかるので、これは単純に見落とした。

そんなわけでproduct関数がないと思い込み、練習ついでにGaucheで書いてみようと思った。

Rubyで仮実装

最初からGaucheで書けるほど慣れていないので、まずはRubyで書いてみた。
自己再帰で書いたりしたのだけど最終的にはinjectを使うようにした。

# 2要素の直積
def product_2dim(xs, ys)
  xs.inject([]) do |acc, x|
    acc + ys.map do |y|
      [x, y].flatten
    end
  end
end

# injectで畳み込み
def product(*lists)
  lists.inject([[]]) {|acc, ls|
    product_2dim(acc, ls)
  }
end

Gaucheで実装

上記のRuby実装をGaucheで置き換えた。

(define (product . lis)
  (define (append-last a b)
    (if (null? a)
        (list b)
        (append a (cons b '()))))

  (define (product-2dim xs ys)
    (fold (^(y acc)
            (append acc (map (cut append-last y <>) xs)))
          '() ys))

  (fold product-2dim '(()) lis))

期待通りには動いているように見える。でも…

ベンチマーク取ってみた

(let ((a (iota 100 1))
      (b (iota 100 1))
      (c (iota 100 1)))
  (time (product a b c)))

; (time (product a b c))
;  real 203.288
;  user 279.670
;  sys   16.460

遅い。非効率な実装なのは感じていたけど、ここまで遅いとは。
ちなみにもちろんcartesian-product使えばちゃんと早い。

(use util.combinations)
(let ((a (iota 100 1))
      (b (iota 100 1))
      (c (iota 100 1)))
  (time (cartesian-product (list a b c))))

;(time (cartesian-product (list a b c)))
; real   0.347
; user   0.330
; sys    0.020

おわりに

自分で書いたproduct実装は遅いのを抜きにしても可読性が悪い。これ絶対数日後に読んでも何がしたいか解読に時間がかかるパターン。
色々書いて読んで覚えていくしかない。

cartesian-product自体Gaucheで書かれているので、とても参考になる。

(define (cartesian-product lol)
  (if (null? lol)
      (list '())
      (let ((l (car lol))
            (rest (cartesian-product (cdr lol))))
        (append-map!
         (lambda (x)
           (map (lambda (sub-prod) (cons x sub-prod)) rest))
         l))))

Laravelにおける複文のSQLインジェクション対策

背景

LaravelアプリケーションでSQLインジェクションのテストを行っていて、わざとSQLインジェクションが起こるようなコードを書いたのに実行されなくて調べた。
(まずはSQLインジェクションが発生することを確認してから、その対応を入れて発生しなくなったことを確認しようと思っていた)

SQLインジェクションのクエリ

典型的な以下の文字列を渡した。

'; delete from admins; -- 

だが実行してみると構文エラーが発生した。

SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'delete from admins; --%'' at line 5 (SQL:

理由

LaravelはデフォルトでPDO::ATTR_EMULATE_PREPARESをfalseにしているため。
これがfalseだとMySQLで複文(;区切りで複数のクエリを発行すること)を許可しない。

参考:

試しにPDO::ATTR_EMULATE_PREPARESをtrueにしたら意図通りSQLインジェクションが発生した。

ということでLaravelはデフォルトで複文対策が出来ている模様。

おまけ

パターンごとにパケットキャプチャした結果。

PDO::ATTR_EMULATE_PREPARES=true + \DB::select($query, $params)(プレースホルダーを通して実行した場合)

Prepared Statementは呼ばれない。シングルクオートはエスケープされる。

where name like '%\'; delete from admins; --%'

f:id:kanno_kanno:20170518220118p:plain

PDO::ATTR_EMULATE_PREPARES=true + 文字列に直接パラメータを埋め込んで実行

華麗にインジェクションがキマる。

where name like '%'; delete from admins; --%'

f:id:kanno_kanno:20170518220635p:plain

PDO::ATTR_EMULATE_PREPARES=false + 文字列に直接パラメータを埋め込んで実行

本文に書いた通り複文を許さないので構文エラーになる。

f:id:kanno_kanno:20170518220734p:plain

PDO::ATTR_EMULATE_PREPARES=false + \DB::select($query, $params)(プレースホルダーを通して実行した場合)

基本はこれで書くはず。もしくはEloquent使うなら意識すらしないかも。
構文エラーにならずPrepared Statementとかも呼ばれて実行される。

f:id:kanno_kanno:20170518220442p:plain

Gaucheの無名関数について - lambda, ^, ^c, cut

背景

とあるネット記事のGaucheのコードを読んでいて^ _ ( 何か処理 )というのがあってこれは何だと思って調べたメモです。
^自体は知っていたのですが、^ _という別の記法もあるのかなと不思議に思ったので。

無名関数

公式ドキュメント: 4.3 手続きを作る

Gaucheの無名関数はlambdaで書きます。

; 無名関数の定義
gosh> (lambda (a b) (+ a b))
#<closure (#f a b)>

; 無名関数を即実行
gosh> ((lambda (a b) (+ a b)) 1 2)
3

lambda^という略記もあります。

Special Form: ^ formals body …

^はlambdaの短い別名です。これはGauche独自の拡張です。

gosh> (^(a b) (+ a b) 1 2)
#<closure (#f a b)>

gosh> ((^(a b) (+ a b)) 1 2)
3
gosh>

lambdaもしくは^は引数部分を(a)ではなくaにすると可変長引数を受け取ります。

variable : 手続きは不定個の引数を取ります。 実引数は新しいリストに集められて、そのリストがvaribleに束縛されます。

((lambda a a) 1 2 3) ⇒ (1 2 3)

; 引数をそのまま返すだけの無名関数
gosh> (^ (x) x)
#<closure (#f x)>

gosh> ((^ (x) x) 10)
10

; 引数定義をカッコで指定しないと、(#f . x)という定義になっている
gosh> (^ x x)
#<closure (#f . x)>

; 実行してみると、リストで渡っているのが分かる
gosh> ((^ x x) 10)
(10)
gosh> ((^ x x) 10 20)
(10 20)

; ちなみにもちろんカッコ付きの方だと引数の個数が合わないとエラーになる
gosh> ((^ (x) x) 10 20)
*** ERROR: wrong number of arguments: #f requires 1, but got 2
    While compiling "(standard input)" at line 8: ((^ (x) x) 10 20)
gosh>

冒頭の^ _はこれを意図した書き方だったのかもしれません。
他の言語でもイディオムとして良くあるように、引数を使わないことを意味するため_にしていたのだと思います。実際使っていなかったので。

またこれらとは別に、^cというのもあります。

Macro: ^c body …

(lambda (c) body …)の短縮表記です。 cには#[_a-z]に含まれる任意の一文字が使えます。

(map (^x (* x x)) ‘(1 2 3 4 5)) ⇒ (1 4 9 16 25)

; 以下の2つは同じなはず
gosh> ((^x (+ 1 2)))
3

gosh> ((^_ (+ 1 2)))
3

スペースがあると前述の(#f . x)になるので注意が必要です。最初この違いに気付けず、スペース付きのまま試して期待通りにいかず悩みました。

gosh> (^x (+ 1 2))
#<closure (#f x)>

gosh> (^ x (+ 1 2))
#<closure (#f . x)>

ちなみに部分適用に向いたcutというのもあります。

Macro: cut expr-or-slot expr-or-slot2 …

[SRFI-26] 手続きを簡潔に書ける便利なマクロです。 いわゆる部分適用を実現するために使えます。

; 引数なしの定義
gosh> (cut + 1 2)
#<closure (#f)>

; 定義して即実行
gosh> ((cut + 1 2))
3

; 2引数の定義
gosh> (cut + <> <>)
#<closure (#f #<identifier srfi-26##<identifier srfi-26#x.126a9c0>.1293ee0> #<identifier srfi-26##<identifier srfi-26#x.126a9c0>.1293e00>)>

gosh> ((cut + <> <>) 1 2)
3

本来はmapとかfor-eachとかそういうやつで使うべきな気がします。

; こう書くより
gosh> (map (^a (+ a 1)) '(1 2 3))
(2 3 4)

; こう書いた方が一時変数置かなくて済むのでスマートでしょ的な
gosh> (map (cut + <> 1) '(1 2 3))
(2 3 4)

余談: 調べ方

今回のような記号(^)に関するリファレンスを調べる場合でもaproposinfoで簡単に辿り着けるのがとても良いです。

; ^を含むリファレンスを探す
gosh> (apropos '^)
^                              (gauche)
^-generator                    (gauche)
^_                             (gauche)
^a                             (gauche)
^b                             (gauche)
^c                             (gauche)
^d                             (gauche)
^e                             (gauche)
^f                             (gauche)
^g                             (gauche)
^h                             (gauche)
^i                             (gauche)
^j                             (gauche)
^k                             (gauche)
^l                             (gauche)
^m                             (gauche)
^n                             (gauche)
^o                             (gauche)
^p                             (gauche)
^q                             (gauche)
^r                             (gauche)
^s                             (gauche)
^t                             (gauche)
^u                             (gauche)
^v                             (gauche)
^w                             (gauche)
^x                             (gauche)
^y                             (gauche)
^z                             (gauche)
define-^x                      (gauche)

gosh> (info '^)
; リファレンスが開く

余談

公開前にプレビューで推敲したらlambdaに著作権発生してた。

f:id:kanno_kanno:20170515231923p:plain

尿膜管遺残/臍炎にかかって回復するまでの記録

背景

去年「尿膜管遺残」(当初は臍炎と診断)という病気にかかり1ヶ月近く仕事を休みました。
当時痛みと不安が増す中で、同様の症状にかかった個人ブログを読んでとても励まされました。
その時「同じ病気にあった人の参考になるように自分も体験記を書こう」と思い記録を始めます。

回復後に投稿しようとしたもののズルズルと後回しにしていて次第に忘れていたのですが、昨日Simplenoteを整理した時に見つけたので今度こそ投稿します。
これは発症して数日後に記録を始めており、それ以前の日記は記録を始めた時に思い出して書き残したものです。


1日目

へその辺りに軽い筋肉痛のような痛み。何だろうと思いつつ深くは考えない。

2日目

痛みは大してないが膿が出てきた。

3日目

最寄りの病院へ。皮膚科がやっていなかったので内科を選択。目視だけで臍炎と診断される。
抗菌剤と軟膏をもらう。
午後、職場で何か臭うなと思っていたらへそにすごい膿が溜まっていて驚く。

4日目

痛みも膿も増すばかり。薬が効いていないのではと疑う。
常時から痛み始める。

5日目

前回とは違う病院で診てもらうことにした。
基本的に紹介状が必要とされている病院に当日診察が可能か、電話で確認。四つくらい担当者を変えて同じような説明をした挙げ句、「紹介状もらってください」と言われる。平常心を保つ。

最寄り駅の前回とは違う病院に行く。経緯を説明したところ、「ここは前回の病院と同系列なので診てもらうならそちらの病院で」と言われる。平常心を保つ。

さらに別の病院に行く。診察の際に、へそ周りを指圧される。痛い。へそを見るためにピンセット的な器具でいじられる。めっちゃ痛い。
とはいえ、ここは受付も先生もとても丁寧で好印象だった。
触診だと前回と同じく臍炎としか判断できないので、紹介状書くから総合病院で診てもらってくださいとのこと。そうだよね。
前回とは別の抗菌剤と軟膏をもらう。

電話予約したら最短で4日後だった。

6日目

へそが常に痛く、力を入れたり振動を加えると更に痛い。
横になる時も痛いし起き上がる時も痛い。
歩くだけで痛いし、そんな状態なのでまともに便も出せない。必然的に小食になっていく。
ヨーグルトか飲むゼリー系で過ごす。

7日目

痛みでまともに寝れず、一時間ぐらいで目が覚めてしまう。
体の向きを変えたら楽になるかと右に向けたところ膀胱ら辺に激痛。
左に向くと激痛はないものの、別に楽にはならなかった。
痛みで気力を削がれ、ここ数日まともに作業できていない。
YouTubeでお笑い動画を見てしまい、笑いを堪えきれず激痛が走る。

8日目

へその肉が明らかに腫れている。
痛すぎて家の中ですら満足に歩けない。
薬が効いてこの痛みなのか、効いていないのか分からない。
横になる時の痛みさえ乗り越えれば、仰向けが一番マシ。ただ起き上がるのに痛みを乗り越えなければならないので、起き上がる気力が沸かなくなる。
かといってずっと仰向けだと背中が痛くなってくる。

まとまった睡眠を確保できないので日中もこまめに寝るようになる。

9日目

タクシーで総合病院へ。先生はすごい良かった。
CTを撮り尿膜管遺残だろうと診断。今回は皮膚科だったので、3日後に泌尿器科へ再度訪れることに。
抗菌剤と痛み止めをもらう。痛み止めは抗菌剤と併用できる代わりに効き目は強くないらしい。
背筋を伸ばせないためか腰痛も出始める。それに伴い左下半身も少し痛み出す。
タクシーで帰る。
関係ないが総合病院は面倒臭い患者が多い気がする。対応してる職員はストレスすごそうだな、と見てて思った。

試しに痛み止めを飲んだら少し楽になった気がする。仮眠する。起きる。痛み止めが切れたのかメチャクチャ痛い。
痛み止めを飲むと切れたときの反動がすごそうで躊躇するようになる。

10日目

1日のほとんどを仰向けで過ごす。
仰向けだと背中が痛くなってくるが、体を横にするとへそか膀胱が痛い。

へその肉?が腫れすぎてへそを占領している。
たまに膿に混じって微量の出血もある気がするが、ほんとに微量なので判別付かない。
あててるガーゼに膿がびっちり付く。
動くのがつらいのでシャワーを浴びるのも一苦労。坊主だから楽だけど、髪の毛あったら大変だと思う。

11日目

やはり1時間くらいで目が覚めては寝直すのを繰り返す。
痛すぎてハァハァと気持ち悪い息遣いになる。
横になってしばらくしてから体を起こすと30分以上は痛みが強い。尿が溜まっているせいか特に(たぶん)膀胱が痛い。
そういえば数日前から膿の臭いが弱くなっている気がする。量は変わらず多い。
マンガだったらへそから何か産まれるのではないかという痛み。
ガーゼを取り替える際、膿で軽く引っ付いてしまい腫れ物から出血。
腫れ物の大きさが、ついにへその外に飛び出すほどになる。
ネットで事前に調べてなかったら、もっと不安になってたはず。
シャワーでゆっくりへそを綺麗にする。一番痛みが和らいでいる状態かもしれない。
腫れ物の先端が上を向くようになり、ガーゼに触れるのが単純に痛い。(腫れ物は先端だけ痛い)
今まではへその内側に当たってた訳だから、それが激痛の原因だった?常時の痛みは減った気がする

痛みや腫れ物に比べると膿の量は増えてないかもしれない。それでもまだまだ出るが。

12日目

3時間ほど眠れた。最近の中では良く寝れた方だ。
そんなに厚いガーゼではないせいか、表から見ても薄黒く変色している。変えようとしたが思った通り腫れ物にくっついている。
ガーゼをしたままシャワーを軽く当てて剥がす作戦。染みるのを堪えながらゆっくり剥がす。
ガーゼがくっつかない方法を調べたところ、今は湿潤ドレッシングというのがメジャーらしい。今まで付けてた軟膏は量が少なかったかもしれない。今度は軟膏べったり付けといた。

やはり常時の痛みもマシになっている。病院に行く前に回復の傾向が見られたのは精神的にも良い。

尿道口が少し赤くなる。

待ちに待った泌尿器科へ。問診票で身長と体重を書いたが、その後で実際に計測された。何故。

再発防止と、最悪がんになる可能性があるので手術で尿膜管を除去しましょうという話に。今度は大学病院の紹介状を渡される。これで通算4つ目の病院である。
ネット知識と病状をもとに早い段階から手術を覚悟していたので、驚きより「やっとか」という気持ちの方が強い。紹介状予約(総合病院)→検査→紹介状予約(大学病院)だけで1週間経っている。
俺は最短で動いたし担当医の方々は良心的だったにも関わらずなので、業界的な問題なのだろう。

前回、今回と病院にて37~38度の微熱を確認。家に体温計がないので定かではないが、そういえば最近ずっと頭がぼーっとしている。栄養不足や寝不足かと思っていたが発熱だったのかもしれない。

大学病院の初診日まで待ち。

仰向け時はへそより膀胱の方が痛くなってきた。ジャーキング(寝てるときビクってなって起きるやつ)が頻繁に起きるようになってきた。

夜寝ようと思ったところでへそがいたくなる。数時間前にガーゼを変えたばかりだが膿と出血で汚れていた。慎重にシャワーで洗い流す。たまに染みて痛い。
ちなみに痛み止めがあまり効かなくなっている気がする。

綺麗にして気付いたが、午前中より更に突起している。へそから斜めに飛び出るような感じだったのが、今では凛々しくへそに対して直角だった。成長が早い。どうしよう。

シャワーあがりだとほとんど膿は出ないのでガーゼを避けたい気もするが、突起してる以上は貼らないとまずそうなので我慢。軟膏をたっぷり塗らないとくっつくが、たっぷり塗るとシャワーで落とすのに時間がかかる。
突起がへその外に出てきてからはガーゼに触れることが痛む。

13日目

痛みは引き続きあるが、やはり和らいでいる。以前よりは少しマシに歩ける。
昼過ぎにシャワーでへそを綺麗にする。膿はまだ出ている。最近は1日2-3回はシャワーでへそを洗っている(ガーゼを変えるタイミング)

少し動けるようになったので外に出る。しかし動いたらやっぱり痛かった。熱っぽさもあって軽くめまい。侮れない。座って作業するくらいなら出来そうだが、数分を超える移動はつらい、そんなレベル。

14日目

3時間単位で寝れるようになった。横になっていれば痛みはほとんどない。

15日目

予約日なので朝から大学病院へ向かう。今回は歩いて駅まで向かう。痛むけど歩けるという事実が、回復傾向にあることを実感させる。

紹介元の病院からCT画像が渡っていないということで、後日取り直すことに。もちろん料金もかかる。不覚。
今日は、へその洗浄をしてもらう。なかなかに痛くて額に汗をかく程に食いしばる。
それから採血、採尿、心電図、レントゲンを行って終了。採血はミスも含めて3本打った。
CT撮り直しは4日後、術前外来は1週間後。日があるが、今の痛みなら何とかなるかという気持ち。

昼過ぎから発熱もあり具合が悪くなる。単なる栄養失調かもしれない。昼飯の量を若干増やしてみる。

へそからの出血がいつもより多く、ガーゼから漏れ出ていた。
夜シャワーを浴びていると、へそからピーナッツぐらいの白い塊がにゅるりと出てくるのを目撃する。力を入れたわけでもないのに勝手に出てきた。全然痛くはなかった。
ネットで似たような報告を見ていたので不安は覚えなかった。これが膿の塊?らしい。
白い塊が出たせいで緩やかに出血。しかしこれをきっかけに痛みはほとんど無くなった。
お笑い動画で笑っても大丈夫になった。

16日目

ほとんど痛みなし。血は出ているのでシャワーで綺麗にしてからガーゼを取り替えておく。判別しにくいが、まだ少し膿は出ているかも?
動けるようになったので仕事復帰の準備を始める。

外に出て少し経つと軽くクラクラするので、栄養をちゃんと取るようにする。

17日目

血は出ている。飛び出たへその形は元に戻るのだろうか。ガーゼの取り替えは一日一回で十分になる。

18日目

CT撮影のためだけに病院へ。CTの結果は当日に分かるので術前外来の日にまとめてやれば良かったと後悔。

19日目

平和。


日記はここで途絶えている。

痛みがなくなったため、日記を付けるのを辞めてしまいました。
このあと術前外来に行って「回復しました」と伝えたら、総合病院の先生から「手術しないで様子見でいいんじゃない?」と言われて結局手術はしていません。
これもネットで仕入れた知識ですが、手術する/しないは先生によって判断が異なるようです。

今のところ再発することもなく健常です。
ただへその形は完全に戻っておらず、飛び出してはいないものの内部が若干腫れた感じになっています。痛みはありません。

追記: コメント欄にある通り、今ではへその形も元通りになりました

当時のつぶやき

Gaucheで指定した数の数字の配列を作るiota

「1から10までの配列から奇数だけを取り出す」というお題を見てGaucheだとこう書くかなって考えた時に、Gaucheで「数字を指定して範囲を取り出す関数って何だっけ」となったのでメモ。

iota

iotaだった。最初rangeとかtimesとかの単語でドキュメント引いて見つからなくてアレってなった。

Function: iota count :optional (start 0) (step 1)

[SRFI-1] startから始まり、stepずつ増加する、 count 個の要素からなる数値のリストを返します。countは 非負の整数でなければなりません。startとstepが ともに正確数であれば、結果は正確数のリストになります。そうでなければ 結果は非正確数のリストです。

ちなみに語源はギリシャ語のイオタらしい。

Scheme iota手続きの語源

the index generator → index → i → ι → iota

iotaの由来(素数夜曲)

素数夜曲」、書店で目立つので存在知っていたけど数学的な本で難しそうと思ってスルーしていた。 これタイトルにLISPとあるけどサンプルコードはSchemeだったのか。


追記: Gauche作者のShiroさんからリプライ頂きました


使用例

冒頭のお題を解くのは簡単。

gosh> (filter odd? (iota 10 1))
(1 3 5 7 9)

カッコを入れ子にしたくなければ$を使うと良い。

gosh> ($ filter odd? $ iota 10 1)
(1 3 5 7 9)

$ arg …

関数適用をチェインするマクロです。Haskellの$にヒントを得ました (意味は異なりますが)。 マクロ引数arg …中に$が出現すると、それが 関数の最後の引数の区切りとなります。例えば次のコードでは、 関数fの最後の引数が(g c d …)となります。

おまけ: 他の言語での解答

この程度だとほとんどのスクリプト言語で1行でサクッと済ませられそうだなと思ったので、ぼくが少なからず書ける言語でどう書くか試してみた。

Ruby

(1..10).select &:odd?

Python

[x for x in range(1, 10) if x % 2 != 0]

Perl。半年ぶりぐらいなのでちょっと書き方忘れてた。

grep { $_ % 2 != 0 } (1..10);

PHP。ちょっとつらい。

<?php
array_filter(range(1, 10), function($n) { return $n % 2 != 0; });

Vim script。久々すぎて「Vim ScriptだっけVim scriptだっけ」ってなった。

filter(range(1, 10), 'v:val % 2 != 0')