2021年3月31日水曜日

リンク集の見栄えを整える【Rails備忘録】

ぞんざいなリンクのリスト,その見栄えをなんとかしたい.次の頭の赤線で囲ったところは,単純に<ul>〜</ul>で囲っただけのリスト環境である.機能的には何ら問題ないが,簡素すぎて味気なさすぎる.

コードとしては,次に示すような,実に単純なものだ.

<h2>Teacher's Dashboard</h2>

<ul>

  <li><%= link_to 'Lessons (Rubrics) management', td_lessons_path %></li>

  <li><%= link_to 'Meetings management', td_meetings_path %></li>

  ...

これを,次のようにする.

ポイントは次の4つ.

  1. list-groupとlist-group-itemを使ってリストボタン化する
  2. octiconsを使ってアイコンのラベルを付ける
  3. context_tagを使って一部を色付けする
  4. html_safeで&raquo;や&nbsp;を有効にする

改良したコードはこんな感じになる.少し面倒だがひとつひとつはシンプルなので,コードを紐解くのはさほど難しくはなかろう.

<h2>Teacher's Dashboard</h2>

<div class="list-group">

  <%= link_to "#{octicon 'checklist', height: 24,

                         class: 'right left', 'aria-label': 'checklist'} &nbsp;

               #{content_tag :span, 'Lessons (Rubrics) management',

                             class: 'text-primary'} &raquo;".html_safe,

            td_lessons_path, class: 'list-group-item list-group-item-action' %>

  <%= link_to "#{octicon 'clock', height: 24,

                         class: 'right left', 'aria-label': 'clock'} &nbsp;

               #{content_tag :span, 'Meetings management',

                             class: 'text-primary'} &raquo;".html_safe,

           td_meetings_path, class: 'list-group-item list-group-item-action' %>

  ...


 

2021年3月24日水曜日

嘘グラフを見分ける簡単な方法

Twitterを見ていたら「COVID-19の感染者はほとんどが外国人だ」というトンデモを撒き散らしているツイートを発見して頭がクラクラしてしまった.下記がそのツイートで紹介されていた「嘘」グラフである.


完全な捏造というわけでもないらしく,BuzzFeed Japanの記事によれば,たんに国籍が分からない人たちを「国籍不明」でまとめているだけで,その内訳のほとんどは日本人だろうとのこと.まったく,差別意識が垣間見えるフェイク?ニュースで,そんなものを撒き散らすのは勘弁してほしいところである.

おかしいとすぐに気付くポイント

ところで,このグラフには,見た瞬間に「あれ?」と疑うポイントがある.そこに気付けるセンスをぜひ身につけていただきたい.

その奇妙な箇所に印を付けた.2箇所ある.ひとつは12月13日〜20日のあたり,もうひとつは2月28日〜3月7日のあたりである.


後者(右側)はビミョーなところかもしれないが,前者(左側)は明らかに,データが逆なんじゃないか?と推察される.このグラフを,おそらく手作業でチマチマ作られたのではないだろうか(その執念には恐れ入る).そのときに,何らかの作業ミスが発生してデータの入れ違いが発生したのだろうと推測する.なお,手作業でデータ処理をしていると私もたまにやってしまうことがあるので,なるべく手作業は入れないように気をつけるべきだ(まあ,これは余談).

もっとも,政府の発表ですら「あれっ?」と思ってしまう雑な取扱いがあるので,もはや何を信じてよいやらという面もなきにしもあらず,ではあるが.

データリテラシーを身につけておきたい

いずれにしても,見る人が見ればこういうところから嘘がバレるので,フェイクニュースやデタラメの嘘ついて人を騙そうとするのはやめたほうがよい.そして,我々としては騙されないように自らを守る術を身につけておきたい.

というわけで宣伝です(すいませんw).4月10日(土)の11時から「科学的思考が日本を救う―データ・サイエンスが拓く未来の社会―」という講演を予定しています(オンライン参加可.申込みはこちらからどうぞ).ぜひご参加ください.


2021年3月17日水曜日

N+1問題を解決する【Rails備忘録】

まずは実装して動作を確認することが優先であってパフォーマンス・チューニングは二の次だといえども,やらないよりは,やるに越したことはない.気付いたときに,対応できることはしておきたい.今回,無造作な実装だと非効率さがいささか目に余るかな?ということでN+1問題の解決に取り組んでみた.

N+1問題とは,簡単にいえば「Aに関連したBというデータがあったとして,Aがn個の要素を持っているときに,Aに関する1回のSELECTと,Bに関するn回のSELECTが発生するので(計n+1回のSELECT文が発行される)非効率だ」というものである.

具体例

例として次のコードを考えてみる.先日から作っているRuby on Railsのアプリケーションである.

ある学校(School)のUserを抽出してきて,それらに関連するPostを全て抽出,ハッシュテーブルに保管し,最後はJSONでレンダリングして出力というものだ(本当はこれだけだと実用性に乏しくもう少し工夫が必要だが,今回はN+1問題に注目するためコードを簡素化して説明する).

class Td::Api::UsersController < ApplicationController

  def index

    users = User.where(school: current_user.school)

    hash = {}

    users.each{|user|

      hash[user.username] = user_info: user, posts: user.posts }

    }

    render json: hash

  end

end

さて,このコードを動かすと,次のような処理が行われる.

  User Load (1.1ms)  SELECT "users".* FROM "users" WHERE "users"."school_id" = $1  [["school_id", 2]]

  ↳ app/controllers/td/api/users_controller.rb:5:in `index'

  Post Load (0.9ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 1]]

  ↳ app/controllers/td/api/users_controller.rb:9:in `index'

  Post Load (0.9ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 2]]

  ↳ app/controllers/td/api/users_controller.rb:9:in `index'

  Post Load (0.9ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 4]]

  ↳ app/controllers/td/api/users_controller.rb:9:in `index'

  Post Load (0.8ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 6]]

  ↳ app/controllers/td/api/users_controller.rb:9:in `index'

  Post Load (1.0ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 7]]

  ↳ app/controllers/td/api/users_controller.rb:9:in `index'

Completed 200 OK in 65ms (Views: 9.3ms | ActiveRecord: 25.1ms | Allocations: 28826)


user_idが1, 2, 4, 6, 7の5名が該当し,それぞれに対して5回のSQLが発行されていることが分かるだろう.users.each{ ... } の繰り返しにおいて,都度,SQLを発行しているのでこうなってしまう.

問題解決

これを,次のようなコードに修正する.最初にUserのリストを抽出するwhere()の処理に対して,includes(:posts) を付けてやる点がミソ.

class Td::Api::UsersController < ApplicationController

  def index

    users = User.where(school: current_user.school).includes(:posts)

    hash = {}

    users.each{|user|

      hash[user.username] = { user_info: user, posts: user.posts }

    }

    render json: hash

  end

end

動作は次のようになる.SQL文の発行回数が2回になっていることを確認されたい.

  User Load (0.9ms)  SELECT "users".* FROM "users" WHERE "users"."school_id" = $1  [["school_id", 2]]

  ↳ app/controllers/td/api/users_controller.rb:5:in `index'

  Post Load (1.0ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN ($1, $2, $3, $4, $5)  [[nil, 1], [nil, 2], [nil, 4], [nil, 6], [nil, 7]]

  ↳ app/controllers/td/api/users_controller.rb:5:in `index'

Completed 200 OK in 64ms (Views: 3.0ms | ActiveRecord: 20.5ms | Allocations: 24834)

問題は無事解決.めでたし,めでたし.

応用問題

さて,話はこれだけでは終わらない.実はPostにはそれぞれCommentが紐付けられており,ひとつのPostに対して複数のCommentsが関連付けられている.

さてどうしよう.これも,無造作にやるとN+1問題が発生してしまう.ていうかN✕(M+1)+1問題?というか,まあ,なんというか.

で,こうする.「3つ以上ネストするときどうすんの?問題」はとりあえず置いておく.

class Td::Api::UsersController < ApplicationController

  def index

    users = User.where(school: current_user.school)

                .includes(posts: :comments)

    hash = {}

    users.each{|user|

      post_hash = {}

      user.posts.each {|post|

        post_hash[post.id] = { post_info: post, 

                               comments: post.comments }

      }

      hash[user.username] = { user_info: user, posts: post_hash }

    }

    render json: hash

  end

end

やってみた.関連のSQLは3つ発行されるだけである.まずはこれでOKということにしておこう.

  User Load (1.1ms)  SELECT "users".* FROM "users" WHERE "users"."school_id" = $1  [["school_id", 2]]

  ↳ app/controllers/td/api/users_controller.rb:5:in `index'

  Post Load (1.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN ($1, $2, $3, $4, $5)  [[nil, 1], [nil, 2], [nil, 4], [nil, 6], [nil, 7]]

  ↳ app/controllers/td/api/users_controller.rb:5:in `index'

  Comment Load (1.4ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN ($1, $2, $3, $4, $5, $6)  [[nil, 1], [nil, 2], [nil, 3], [nil, 4], [nil, 5], [nil, 6]]

  ↳ app/controllers/td/api/users_controller.rb:5:in `index'

Completed 200 OK in 111ms (Views: 2.4ms | ActiveRecord: 50.8ms | Allocations: 34417)

だいぶ無駄を省くことができるようになった.

発展問題

「3つ以上ネストするときどうすんの?問題」も片付けておこう.CommentにはそれぞれUserが紐付けられているので,処理するときにさらにcomment.user.fullname なんて感じでフルネームを参照したくなる.このコードを書くと,またSQLがひとつ発行されてしまう.

一対多のとき,関連名は複数形で書いた.その逆のときは,単数形で書けばよい.UserにはScoresも紐付けられていて,さらにScoreにはRubricも対応,RubricはさらにLessonに関連付けられているというような状況のとき,includes()を丁寧に書くと,次のようになる.多段でネストするときは,連想配列をネストさせればよい,ということらしい.

users = User.where(school: current_user.school)

  .includes(posts: {comments: :user}, scores{rubric:lesson})

関連名を記述するとき,複数形と単数形をきちんと使い分けている点に注意しよう.また,PostsとScoresの関係のように,複数の関係が存在するときは,並べて書けばよいようだ.これらのテーブルがどのような関係にあるかは,先の記事を参考にされたい.

おまけ

(得られたJSONは json pretty printer で整形して確認するといいよ!)



2021年3月14日日曜日

ActiveRecordが便利すぎる【Rails備忘録】

いまRuby on Railsで作ってるアプリ,ERD描いたらこんな感じになってきてけっこう複雑化してきたんだけれども,ActiveRecordが優秀なもんで作ってて楽しい.備忘録がてら「こんな感じでできちゃうぞ」っていうメモを書いておく.

注目してほしいのは,ProjectとSchoolとUserの関係である.ひとつのProjectには複数のSchoolが参加していて(通常は2校だが別に3校以上でもいい),それぞれのSchoolにはUserが属している.なお,Userはstudentだったりteacherだったりadminだったり(場合によってはteacher兼adminだったり)する.ProjectとSchoolとUserの関係を,図で示しておこう.


今回,本稿でいろいろ考える状況は,Userが投稿するPostの取得である.1. ある特定のUserが投稿したPostのリスト(A)を得たい場合,2. ある特定のSchoolから発信されたPostのリスト(B)を得たい場合,そして,3. ある特定のProjectに関連するPostのリスト(C)を得たい場合を考える.得られるリストを集合として考えると,A ⊆ B ⊆ C となるはずだ.

そのUserが投稿したPostの一覧を得る

Userはメッセージ(Post)を投稿する.各PostにはCommentを付けられるが,とりあえずそれは置いておこう.あるUser(@userで示されるUser)の投稿したPostの一覧を取得したいときは,こうすればいい.

@posts = Post.where(user: @user)

これは簡単だろう.このコードに対応するSQLとして,次のようなものが発行される($1には対象とするUserのIDが指定される).そのままである.分かりやすい.

SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1

メッセージ投稿の一覧(Postテーブル)から「田中」の発言を抜き出すという状況を図示してみよう.こんな感じになる.赤枠で囲ったレコードが,指定された条件(Userが「田中」)に合致し抽出されたものである.簡単だ.

次からちょっとややこしくなる.

そのSchoolに属するUserが投稿したPostの一覧を得る

複数のUserによる投稿の一覧を得たい.先の例では@userが一人だったが,今回は,あるSchoolに属する複数のUserが投稿したPostを得たいということである.気分としては,こんな感じ(なんと,このコードでも動く).

@posts = Post.where(user@users User.where(school@school))

さて,PostとUserを内部結合させるメソッドjoins()を使ったコードは次のとおりである.@schoolで示されるSchoolに属するUserが投稿したPostの一覧を取得したいときは,次のようにすればよい.

@posts = Post.joins(:user).where(users: {school: @school})

PostのテーブルをUserテーブルにINNER JOINしてから,Userの条件として「schoolカラムが@schoolだ」というものを指定すればよい,というわけである.

SELECT "posts".* FROM "posts" INNER JOIN "users" ON "users"."id" = "posts"."user_id" WHERE "users"."school_id" = $1

いささかややこしくなったが,素直に読めばよい.すなわち,Userのschool_idが指定されたものであるようなUserに対して,そのUserのIDとPostのuser_idが一致するようなPostを取ってこい,ということだ.したがって,当初の目論見どおり,指定したschool_idに関連付けられたUser(たち)の投稿したPostが全て得られるということになる($1には対象とするSchoolのIDが指定される).

UserのテーブルにはSchoolが関連付けられている.すなわち,田中,佐藤は学校Aの生徒であり,山田と高橋は学校Bの生徒であり,という具合である(次図の右上).

PostとUserを内部結合して「学校A」に関する発言を抜き出せという状況を図示すると,次のようになる.内部結合したテーブルにおいてSchoolが「学校A」となっているレコードが赤枠で囲まれている(抽出されている).こうやって図示すると,それほど難しくはないことがわかるだろう.

なお,先のコード(これでも動く,としたもの)だと,次のような少し冗長なものになる.

SELECT "posts".* FROM "posts" INNER JOIN "users" ON "users"."id" = "posts"."user_id" WHERE "posts"."user_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."school_id" = $1)

まあ,動けばよいと割り切れば,分かりやすいコードのほうがよいかな?

そのProjectに参加しているSchoolに属するUserが投稿したPostの一覧を得る

さらにややこしいケースである.Projectには複数のSchoolが関連付けられている.さらにSchoolをJOINする?JOINのネストはなんだかよく分からなくなってしまいそうで面倒だなあ(あとでちゃんと検証する).

まずは,シンプルに考えよう.先のコードで,@schoolが複数になっているという場合である.さて,これをどう書けばよいだろうか.

実は,次のように書くことができる.ProjectとSchoolのクラスにはhas_many,belongs_toの関係が指定されているので,あるプロジェクト @project に関連付けられたSchool(たち)は,@project.schoolsというコードでシンプルに得られる.それを,先のコードの@schoolの代わりにそのコード断片を入れてあげるだけでよい.簡単すぎる\(^o^)/

@posts = Post.joins(:user) 

             .where(users: {school: @project.schools})

さすがにこれに対応して発行されるSQLは,少し,というか,かなり複雑なものになる($1には対象とするProjectのIDが指定される).

SELECT "posts".* FROM "posts" INNER JOIN "users" ON "users"."id" = "posts"."user_id" WHERE "users"."school_id" IN (SELECT "schools"."id" FROM "schools" WHERE "schools"."project_id" = $1)

しかし,まあ,我々は気にすることはない.(パフォーマンス・チューニングが必要になるようなケースならいざしらず)プログラムのコードが分かりやすく書けていれば問題ないのだ.

JOINのネストという方法できちんと書くならば,次のように書く.joinsメソッドで複数の関連を並べるときは,joins(関連名1: :関連名2) と書けるらしい(3つ以上ネストさせるときはどうするの?…… 調べてみたら「a: { b: :c }」というようにハッシュをネストすればよいようだ).

@posts = Post.joins(user: :school)

             .where(schools: {project: @project})

このとき発行されるSQLは次のとおり.SELECT文がネストしないぶん,SQL的にはこちらのほうがすっきりしているね.

SELECT "posts".* FROM "posts" INNER JOIN "users" ON "users"."id" = "posts"."user_id" INNER JOIN "schools" ON "schools"."id" = "users"."school_id" WHERE "schools"."project_id" = $1

図にするとこうなる.「『Proj. X』に関するPostを全て抜き出せ」という状況である.右端の表は,各学校が参加しているProjectの関係を示す表である.図では,Projectが「Proj. X」となっているものが選択されている.やはり,図示してみると,それほど難しくない印象を受けるのではないか.INNER JOIN,恐るるに足らず,である.

いやはや,いずれにしてもActiveRecordの威力を実感した次第.ありがたやありがたや.

2021年3月10日水曜日

参照先?参照元?

Wordで文書を書いていると,たまに「エラー!参照元が見当たりません」というエラーにぶち当たることがある(図).

この図は,次の手順を実施した後の画面である.

  1. 図版を入れ,キャプションで図表番号を挿入する.
  2. 「相互参照」で図の番号(図1)を本文中に挿入する(「図1を参照してください.」となる)
  3. 図版をキャプションとともに丸ごと削除する.
  4. 本文中の「図1」をハイライトし,コンテキストメニューから「フィールドの更新」を実施する.

参照されているはずの図表番号がないので,めでたく?「エラー!参照元が見つかりません」というエラーになる.

日本語おかしくない?

このエラー,印刷しようとした瞬間,あるいはPDF化しようとした瞬間に発生することがあり,やっかいなエラーである.卒論やら修論やらでよく見かけるので,ちゃんとチェックしてから印刷せえよと長年苦々しく思っていたのだが,そのタイミングで生じてしまうので,致し方ないといことが分かった.善後策としては,まずPDF化して,このエラーが生じていないことを確認して,そのあとで印刷(ないしは電子的に提出)するしかない.

まあバッドノウハウっぽいワーカラウンドはともかくとして,このエラーメッセージ,なんかおかしくないですか?参照している先が無くなっちゃっているので,「エラー!参照『先』が見つかりません」なんじゃないのかと思うんだけどなーと長いこと考えていた.

もともとは……

なんでこんなエラーメッセージなんだろうと不思議に思っていたら,もともとの英語版だと「Error! Reference Source Not Found」なのだ.Sourceだから「元」に訳しちゃったわけね.英語と日本語で感覚が違う「go / come」の違いみたいなものだろうか.

いずれにしても,ソフトウェア作成者には教養が求められるよねえ,というお話.

2021年3月8日月曜日

widowやorphanとは何か?

widow【名】夫を亡くした[夫と死別した]女性

orphan【名・形】孤児(の),親のない(人),親をなくした(人)

それぞれ,英和辞書をひくと最初に出てくる意味である.しかし,ここで論じたいのはそのようなものではなく,印刷用語,校正用語としてのwidowやorphanだ.

ちゃんとした辞書をひくと,widowやorphanの意味として,次のような説明もきちんと書かれている(いずれも「英辞郎」より引用).

widow 《印刷》ウィドー,孤立行◆ページの末尾に残された,段落の最初の1行(2行目以降は次のページに送られている).または,ページの最上部に残された(前のページや段組から続く)段落の最後の1行.

orphan オーファン(の),孤立行(の)◆ページの末尾に残された,段落の最初の1行.2行目以降は次のページに送られている.

これらは印刷,出版業界の方ならご存知なのだろうが,著述業の方々でも意外と知らない人が多いのではなかろうか.とくに最近は,電子化によってページという概念が希薄になってきたため,若い人は知らなくてもおかしくない状況なのだろう.

ITのツールが旧来の文字文化を破壊していることがあるという点は,その是非は置いておくとしても,意識していたほうがよいのではないか.たとえば「段落の先頭は1字,字下げする」という習慣.これも,ワープロが破壊してしまったのではと感じている.現に,この文章がそうではないか.かたじけない.

まあ,しかし,まだまだ最後はPDFにして形の整えられた電子文書にするという状況はしばらくは廃れないであろう.であれば,旧来の組版文化はきちんと知っておいたほうがいい.ワープロソフトのデフォルト設定がダメダメなのが問題なのだが,機能としてはちゃんと用意されているので,工夫すればそれなりにきちんとした文書を作ることができる.図のあたりの機能をうまく設定すればいいので,慣れておくとよいだろう.

最後に,page widow といえば,乾くるみの「六つの手掛り」という本がとても面白いのでオススメしておきたい.いろいろと楽しい仕掛けがてんこ盛りのうえ,最後にハッとする仕掛けがあって,考えることが好きな読者には楽しい本ですよ.

2021年3月7日日曜日

動き続ける健気なストップウォッチ

昨日,急遽,某オンラインイベントのLT(Lightning Talk)に出場することとなった.「5分のタイマーが必要だなあ」とiPhoneのストップウォッチを開いてみたら,とんでもなく大きな時間が刻まれていてビックリした(下図).いったいいつから時間を刻んでいたものか.

「あれえ?おかしいなあ」と感じたのはしばらくしてからである.1000時間を越えているということは,1ヶ月以上前に「開始」ボタンが押された計算になる.しかし,数日前に,OSのアップデートをしたはずだ.そのときは,さすがにアプリは終了していたはず.はて,時を越えて健気にタイマーは動き続けていたのだろうか?

検証してみた

そもそも,ストップウォッチを常に使い続けていたわけではないので,別のアプリを使っていたときにはバックグラウンドに回っていたはず,というかスリープするんじゃないの?でもスリープ時にストップウォッチが止まっちゃうのは困るよね?

そんなわけで,確かめてみた.

まず,時計アプリを起動して,ストップウォッチを開始する.タイマーが動いている状態でホームボタンをダブルクリックし,アプリ切り替え画面にする(図).さらに,時計アプリを上にスワイプして終了させる.さて,しばらくしてそのあと時計アプリを起動すると,どうなるだろうか?


結論.30分後にアプリを起動したらきっちり30分,時間が刻まれていた.もちろん,その間,アプリは起動していないことが明らかなので,刻一刻とタイマーが動いていたわけではなく,再起動時にUnixエポックをみて再計算されたものであろう.

ということで,落ち着いて考えてみればどうということはないが,少しびっくりした次第.

2021年3月2日火曜日

xargs考・続き

昨日の記事を紹介したところ,いくつかコメントを頂いて,なかでもハッとさせられたのはxargsだと-Pオプションで並列実行させられるので,イマドキのマルチコア環境だと効率的に処理できるのでは?というもの.

というわけで,やってみた.

12個のファイルを作って,そのあと6つずつ並列に処理をさせている.-Pオプションを付けないと,"echo @; sleep 1"が順次実行される.sleep 1 が効くので,1秒毎に処理が行われていく様子を確認することができる.このケースでは,全部実行し終わるのに12秒かかる.

一方,上記の例では,-P6として「6つ並列に実行せよ」と指示している.すると,12個のうち半分の6個が一気に実行される.1秒後に残りの6つが実行されるので,結果として12個のファイルを2秒で処理できている.

まあ,この例は少し恣意的ではあり,並列実行がどのCPUに割当られて本当に並列に実行されるかどうかは環境依存でもある.しかし,マルチコア環境だと効果的に働きそうだということは分かるだろう.

2021年3月1日月曜日

xargs考

たくさんあるデータを,シェルスクリプトで纏めて処理したいというニーズは多い.たとえば,たくさんの画像ファイルのサイズを一括で変換したいなど(これは本当によくある).で,こうやる.あ,次の例は「あるディレクトリに大量のJpegファイルがあって,そのサイズを半分にしたい」という設定ね.

$ for f in *.jpg; do

> convert -scale 50\% $f `basename $f .jpg`_s.jpg

> done


xargsでやろう

ところがxargs派の人々は「それxargsでおk」という.すなわち,

$ ls *.jpg \

| sed -e 's/.jpg//g'  \

| xargs -I@ convert -scale 50\% @.jpg @_s.jpg

とやるのだ.basenameを使ったほうがわかりやすいという人は,

$ ls *.jpg \

| xargs -I@ basename @ .jpg \

| xargs -I@ convert -scale 50\% @.jpg @_s.jpg

でもOK.ただし,私は,ちょっとコレには受動態が続く文章のような気持ち悪さを感じる.ま,それはそれとして.

xargsに対してはどうも好き嫌いがあるようで,周辺に聞き込みしたところ,オジサンたちより若者に支持されている傾向があるような……(まあサンプル数が少ないので,半分冗談ですw).で,なんでxargsに抵抗感を感じるのかなと考えてみた.抵抗感のもとは何か,それは,xargsは「一刀両断」ということなんだな.つまり,「並べた処理対象をパイプで流し込んで,コマンド一発!」という,データ集合に対してmapでドン!といったイメージというか.

for派の主張

「for文」派の思考過程は,こんな感じ.先の例みたいな簡単な例ではなくて,もう少しフクザツなケースを考えてみるよ.

  1. まずひとつのデータを対象として,試行錯誤してみる.「cat file01.xxx | commandA | commandB | ... > file01.yyy」という感じ.これは,シェルスクリプトがインタラクティブに処理できるメリットを最大限に享受できて,いちいち中間の結果を確かめながら作業できるので,作業効率がよい.
  2. ある程度処理がうまくいくことを確認したら,for文で囲む.上の例でいえば,「for f in *.xxx; do cat $f | commandA | commandB | ... > `basename $f .xxx`.yyy; done」とする.

xargsを使ってコレをやろうとすると,2.の過程で一連の作業をパッケージングしなければいけない.関数にするとか.名前付けてあげないと「mapでドン!」ができないから.そこに,思考のギャップがあって,それに違和感をオジサンたちは感じるんじゃないだろうか.

抽象度を高めてmapでドン方式のほうが,並列化が簡単だとか,いろいろメリットがあるのもわかる.しかしそれは一方でデメリットでもあって,記述のイメージと実際のデータ処理がマッチしてないので想像力を求められるという面もある.パイプでコマンドを繋いでいくというシェルの書き方はデータの流れとコマンド列が見た目で対応していて実に自然な感じなのに,xargs使うといきなり並列化しちゃうので,混乱するのだ.(私を含め)オジサンたちはそこについて行けない.

具体例と対策

まあ,話をもう少し身近な例に戻そう.ちょっと時間がかかる処理を大量のデータに対して適用するときは,進捗状況をechoで確認しながらやることが多い.たとえば冒頭の例で示すと,こうやる.

$ for f in *.jpg; do

> echo -n converting $f ...

> convert -scale 50\% $f `basename $f .jpg`_s.jpg

> echo done

> done

これxargsで,どうやるの?教えてxargsの偉い人!

……と,ここまで書いて公開したら,いろいろとコメントでアドバイスを頂いた.面白い議論ができたというところ.その議論でxargsに関してたいへん理解を深めることができたが,結論としては,次のように -t オプション(もしくは--verbose,実行内容を標準エラー出力に表示するもの)を指定すればよいとのこと.

$ ls *.jpg \

| sed -e 's/.jpg//g'  \

| xargs -t -I@ convert -scale 50\% @.jpg @_s.jpg

なんでもよいのでいくつかファイルがあるところで次を実行してみると,動作がよくわかる.これでxargsに親しむことができれば,いつまでもforeachに拘るオジサンから成長できるんじゃないかな……

$ ls | xargs -t -I@ sh -c 'echo @; sleep 1'

(続きを書きました)