One
Common Lispでもワンライナーしたい
自己紹介
● twitter: @sin_clav
● github: @t-sin
● 野生のCommon Lisp使い
● おしごとはnil
今日お話しすること
● Common Lispでワンライナー
やったらツラかった(動機)
● ワンライナーを楽にしてやるぜ(結果)
● どうワンライナーするのか(使い方)
● その内部構造(設計・実装)
シェルのワンライナー
よくお世話になりますよね
よくやるワンライナー(1)
● アクセスログ中のhoge APIへのアクセス
● そのログ中のhoge APIのアクセス数
$ cat /var/log/nginx/access.log | grep /api/hoge
xx.xx.xx.xx - - [21/Oct/…] "GET / HTTP/1.1" ….
xx.xx.xx.xx - - [22/Oct/…] "GET / HTTP/1.1" ….
xx.xx.xx.xx - - [24/Oct/…] "GET / HTTP/1.1" ….
$ cat /var/log/nginx/access.log | grep /api/hoge |
wc -l
● 3
よくやるワンライナー(2)
● CSVファイル中の2列目の合計
$ cat data.csv
id1,1
id2,2
id3,3
$ cat data.csv | awk -F , '{sum+=$2}END{print sum}'
6
よくやるワンライナー(2)
● CSVファイル中の2列目の合計
shell以外の言語(awk)おぼえないとダメ…?
$ cat data.csv
id1,1
id2,2
id3,3
$ cat data.csv | awk -F , '{sum+=$2}END{print sum}'
6
Common Lispでやってみる
● CSVファイル中の2列目の合計
● ぜんぶCommon Lispで。
$ cut -d ',' -f 2 data.csv | ros run -e '(print
(loop for line = (read *standard-input* nil :eof)
until (eq :eof line) sum line))' -q
6
$ ros -s split-sequence -e '(with-open-file (in
"data.csv") (print (loop for l = (read-line in
nil :eof) until (eq l :eof) sum (parse-integer (nth
1 (split-sequence:split-sequence #, l))))))' -q
● 6
ながすぎてしぬ
われわれはどうすればいいのだ……
ワンライナーっぽさのために
● 入力まわりのタイプ数を減らす
– *standard-input*
– with-open-fileうんぬん
– 行に対してのloop
● 「処理」の合成っぽく書けるとよい
ワンライナーを支援するワン!
● CSVの2列目合計をoneで。
● ぜんぶoneで。
$ cut -d ',' -f 2 data.csv | ros one '(one:for* - <
one:read* +> + 0)'
6
$ ros one '(one:for* #P"data.csv" < one:read-line*
$ #/(split-sequence #, _) $ #/(nth 1 _) $ parse-
integer +> + 0)'
6
Oneとは
「処理」を
連結して
入力をwrapする
ライブラリです!!
Oneの機能概要
ゆるいお気持ちでお聞ききください
Oneの基本構文
● 前段の入力を、次の記号で受ける
CL-USER> (one:for 入力 [記号 パラメータ]*)
Oneの基本構文
● 前段の入力を、次の記号で受ける
CL-USER> (one:for 入力 [記号 パラメータ]*)
オブジェクト,
stream,
pathname
パイプ的な記号
$, ?, <, >, +>
1引数関数,
シンボル
Oneの基本構文
● 前段の入力を、次の記号で受ける
● 1引数lmbda式用リーダマクロ
CL-USER> (one:for 入力 [記号 パラメータ]*)
オブジェクト,
stream,
pathname
パイプ的な記号
$, ?, <, >, +>
1引数関数,
シンボル
;; (lambda (input) (search "hoge" input)) と同じ
#/(search "hoge" _)
入力
記号`$`: 処理の合成
● 前段の入力にパラメータ(処理)を合成
● 例
CL-USER> (one:for 入力 $ 関数or関数名 ...)
CL-USER> (one:for 1 $ print)
"1" ; 入力にprintが適用された
CL-USER> (one:for 1 $ 1+ $ print)
"2" ; 1+の後にprintが適用された
記号`<`: 入力の上を繰り返し
● 前段の入力の上をloopする(関数で)
● 例
CL-USER> (one:for 入力 < 関数or関数名 ...)
CL-USER> (one:for '(1 2 3) < cdr $ princ)
123 ; cdrでloopしたものが出力された
CL-USER> (one:for #P"nums.csv" < one:read-line* $
print)
"id1,1"
"id2,2" ; read-line* (:EOFで終わる) でloopした
記号`?`: 条件によるフィルタ
● 前段の入力のうち述語でtになるもののみ通す
● 例
CL-USER> (one:for 入力 ? 述語 ...)
● CL-USER> (one:for '(1 2 3) < cdr ? oddp $ print)
1
3 ; 奇数だけが出力された
記号`>`: 処理結果を溜め込む
● 前段の入力をリストに溜め込んで処理する
● 例
CL-USER> (one:for 入力 > 変換する関数 ...)
CL-USER> (one:for 1 > identity $ print)
(1) ; 入力がリストになる
CL-USER> (one:for '(1 2 3) < cdr > identity $
print)
(1 2 3) ; 前段の入力すべてがリストになる
CL-USER> (one:for '(1 2 3) < cdr > #/(apply #'+ _)
$ print)
6 ; 合計された
記号`+>`: 処理結果を畳み込む
● 前段の入力をバッファせずに畳み込む
● 例
CL-USER> (one:for 入力 +> 2引数関数 [初期値] ...)
CL-USER> (one:for '(1 2 3) < cdr +> (lambda (a b)
(+ a b)) 0 $ print)
6 ; 足し込まれた
; 長い。lambdaの部分が+だけで書けるといいかも
実際の使用例
ここからが本当にやりたかったことです
ワンライナーを支援するワン!
● CSVの2列目合計
● 標準入力をソート
$ seq 1 5 | shuf | ros one "(one:for* - < one:read-
line* > #/(sort _ #'string<))"
1
…
5
$ ros one '(one:for* #P"data.csv" < one:read-line*
$ #/(split-sequence #, _) $ #/(nth 1 _) $ parse-
integer +> + 0)'
6
シェル芸(1): 響け!ユーフォニアム
『【ファン迷惑】「響け!ユーフォニアム」という文字列だけで遊
ぶシェル芸人達』, https://togetter.com/li/1041621
$ echo 響け!ユーフォニアム | ros one '(one:for - <
one:read-line* $ #/(cons _ (length _)) $ #/(cons
(repeat-sequence (car _) (1+ (cdr _))) (cdr _)) $ #/
(batches (car _) (1+ (cdr _))) < cdr $ #/(format t "~a~
%" _))'
響け!ユーフォニアム響
け!ユーフォニアム響け
!ユーフォニアム響け!
ユーフォニアム響け!ユ
ーフォニアム響け!ユー
フォニアム響け!ユーフ
ォニアム響け!ユーフォ
ニアム響け!ユーフォニ
アム響け!ユーフォニア
ム響け!ユーフォニアム
シェル芸(2): サンシャイン池崎ゲーム
https://twitter.com/ziuziu/status/918070729341587457
$ echo いまはもううごかないおじいさんのとけい | ros one
'(one:for* - < one:read-line* $ #/(ppcre:regex-replace-
all "い" _ "イェー!"))'
イェー!まはもううごかなイェー!おじイェー!さんのとけ
イェー!
Oneの中身は?
(ちなみにここまででスライド26枚です)
設計のポイント
● 部品となる「処理」を独立させる
● 処理間を渡るデータはLispオブジェクト
● 省タイプ化
– 入力に対する制御構文(loopやif)を隠す
– 入力の煩雑な処理(長い名前等)を隠す
● 省メモリ
パイプを見直す
$ cat access.log | grep html | sed 日時 | sort
パイプを見直す
$ cat access.log | grep html | sed 日時 | sort
grep sedcat sort
パイプを見直す
$ cat access.log | grep html | sed 日時 | sort
grep sedcat sort
grep sedcat sort( )) ) )(((
パイプを見直す
$ cat access.log | grep html | sed 日時 | sort
grep sedcat sort
grep sedcat sort( )) ) )(((
(sort (sed (grep (cat "access.log"))))
パイプを見直す
$ cat access.log | grep html | sed 日時 | sort
grep sedcat sort
grep sedcat sort( )) ) )(((
(sort (sed (grep (cat "access.log"))))
関数合成だ!
〜パイプからLispへ〜
省メモリを考える
● 入力を全部メモリに置いてはダメ
● 入力はすぐ次の処理に流す
;; ダメなコード
;; (access.logが10GBあったら…?)
(sort (mapcar #'sed
(mapcar #'grep
(cat "access.log")))
#'string<)
(sort (loop
:for line in (cat "access.log")
:when (grep line)
:collect (sed line))
#'string<)
処理フローの組み立て(1)
● 内部でこんなコールグラフをもつ、いっこの
lambda式をつくりたい
cat
grep
grep
grep
grep
sed
sed
sed
sort
< ? >
: 入力の流れ
: 処理のlambda式
処理フローの組み立て(2)
● lambda式を連ねてフローを作る
– こんな関数でパーツを…
– 連ねる
;;`$`はだいたいこんな感じ
(defun compose (fn2 fn1)
(lambda (input)
(funcall fn2 (funcall fn1 input)))
;; cat | grep | sed
(compose (compose #'grep #'sed) #'cat)
感想
● lambda式の入れ子地獄はデバッグつらい
● シェルやREPLで、まあまあ使える
● まだまだ改善の余地ありそう
– `+>`に2引数lambda式を書くところなど
Oneをためしに使ってみてね。
`$ ros install t-sin/one`で入るよ!

One - Common Lispでもワンライナーしたい