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')

MySQLでgroup byなしでcountとorder byを同時に使うとエラー

MySQLとLaravelの話です。
MySQLSQL_MODEONLY_FULL_GROUP_BYが指定されていると、group by無しでcountとorder byを同時に使うと以下のエラーが発生します。

ERROR 1140 (42000): Mixing of GROUP columns (MIN(),MAX(),COUNT(),...) with no GROUP columns is illegal if there is no GROUP BY clause

MySQLのバージョン

Server version: 5.6.36 MySQL Community Server (GPL)
  • 5.6.36以前は確認していません
  • 5.7.17では発生しませんでした

再現

-- サンプルのテーブル
mysql> desc foo;
+-------+---------+------+-----+---------+-------+
| Field | Type    | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| id    | int(11) | YES  |     | NULL    |       |
+-------+---------+------+-----+---------+-------+
1 row in set (0.00 sec)


-- sql_modeを一旦未指定にする
mysql> set session sql_mode='';
Query OK, 0 rows affected (0.00 sec)

-- count と order byを同時に使っても問題なし
mysql> select count(*) from foo order by id;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)


-- sql_modeにonly_full_group_byを指定
mysql> set session sql_mode='only_full_group_by';
Query OK, 0 rows affected (0.00 sec)

mysql> select @@session.sql_mode;
+--------------------+
| @@session.sql_mode |
+--------------------+
| ONLY_FULL_GROUP_BY |
+--------------------+
1 row in set (0.00 sec)


-- count と order byを同時に使うとエラー
mysql> select count(*) from foo order by id;
ERROR 1140 (42000): Mixing of GROUP columns (MIN(),MAX(),COUNT(),...) with no GROUP columns is illegal if there is no GROUP BY clause

通常はそんなSQL書かないでしょうが、今回Laravelを使っていてちょっとハマりました。

クエリオブジェクトを使いまわしたかった

以下のような感じで検索条件を構築したクエリオブジェクトに対して、countと実際の検索を別々に発行したいケースです。

<?php
public function something()
{
    // \DB::table('foo')->where()...的な感じでクエリオブジェクトを作成
    $query = $this->createQuery();

    // 総件数を取得
    $count = $query->count();

    // 実際のレコードを取得
    $records = $query->get();
}

実際のレコードを取得するために$queryにはすでにorder_byの指定が入っています。
そのため上記のエラーが発生してしまいました。

ハマったのはログに出力されたクエリを直接MySQLに実行しても通るのに、PHP経由だとエラーになったためです。
(MySQL側ではsql_modeにonly_full_group_byを付けていなかったのでエラーにならない)

config/database.phpのstrict => true

config/database.phpにはstrictという項目があり、Laravel5.3以降ではtrueがデフォルト値のようです。

<?php

    'mysql' => [
        // (略)
        'strict' => true,
        // (略)
    ],

これがtrueだと、MySQL接続時に以下のSQLが走ってsql_modeが設定されます。 https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Connectors/MySqlConnector.php#L178

set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'

ということで、MySQL自体のsql_modeは上書きされます。

対応策

同じようなIssueではstrict => falseにすることが提案されています。
5.3 Query Builder count() Sql error - PR proposal

それが嫌な場合はcountを実行してからorder byを指定するとかが回避策になると思います。

Gauche - ファイル読み込みのサンプル

gaucheでファイルを読み込むやり方のメモ。

一行ずつ読み込んで処理する

(call-with-input-file
  "/Users/kanno/tmp/codes/a.scm"
  (lambda (in)
    (port-for-each (lambda (line)
                     ; 1行ずつ行う何かしらの処理
                     (print line))
                   (cut read-line in))))

一度に読み込んで処理する

Shiroさんより教えて頂いた。

; 1度に全部読み込んでリストにする
(use file.util :prefix file:)
(dolist [line (file:file->string-list "/Users/kanno/tmp/codes/a.scm")]
  ; 1行ずつ行う何かしらの処理
  (print line))

; dolistを使わずfor-eachでもよい
(for-each (lambda (line)
            ; 1行ずつ行う何かしらの処理
            (print line))
          (file:file->string-list "/Users/kanno/tmp/codes/a.scm"))

ちなみにリストではなく文字列そのまま受け取る場合はfile->stringというのもある。

(use file.util :prefix file:)
(file:file->string "/Users/kanno/tmp/codes/a.scm")