ユユユユユ

webエンジニアです

コンパイラ概論で遭遇した用語集まとめ

 The Elements of Computing Systems を読み進めている。今週からいよいよ大詰めのコンパイラ編に突入した。

 概念としてのコンパイラのはたらきには、言語学の知識が絡んできていてとても面白い。たしか学部2年生のころに言語学入門の講義を履修したのであった。必ずしも優秀な生徒であった自信はないが、計算機科学の観点から言語学にアプローチするのは、ジョブズにとってのカリグラフィよろしく、点と点がいままさに線になろうとしているようでたいへん面白い。

 とはいえ、スッと飲み込むにはやはり難しい。まずは用語集を整理しなければ理解のスタートラインにすら立てない気がしている。そこでゼミの予習のような気持ちに立ち返って、まずは用語集を作ってみることにした。

 基本的にテキストに沿って整理している。英語のテキストをきちんと読み下して、日本語で整理している格好である。

 用語の日本語訳については wikipedia の多言語ページを参照している。ほとんどの項目について wikipedia に詳細な解説もあるが、直接の引用は行っていない。難しすぎて理解が及ばないのである。そのためまずはテキストに固執してきちんと精読している。

「計算機科学大辞典」のような、学者向けに重要概念や用語をコンパイルした文献があればだいぶ便利だったはず。ネット検索で出てくるのは玉石混交な情報ばかりで、きちんと頼れる情報源が見つけられなかった。 wikipedia をつい引用したくなる学部生の気持ちはとても共感できる。

 大学図書館あたりで文献を調べてみるというのはありなのかもしれないが、あいにくもはや自由に出入りできる身分ではないし、パンデミックな時節柄、一般利用者として押しかけるのも迷惑だろうから、それは控えた。いずれ機会があればきちんと調査してみたいなとは思っている。

 前口上はこれまでで、以下が用語集である。学生のノートを覗き見るような気持ちで眺めてみてほしい。

formal language - 形式言語

 人工的に単純化され、形式化された言語のこと。プログラミング言語を含むが、それにとどまらない。

 形式言語において自然言語の複雑さは捨象され、シンタックスの構造を精確に形式立てることができる。一般にプログラミング言語は、「文脈自由文法( context-free grammar )」と呼ばれる規約によって規則づけられる(後述)。

lexical analysis - 字句解析

 scanning, tokenizing とも。コンパイラの文脈においては、プログラムのソースコードを解析して、その文字列を分割可能な最小単位まで分割することを意味する。分割可能な最小単位のことをトークンとも呼ぶため、「トークン化( tokenizing )」とも呼ばれる。

terminals and non-terminals - 終端記号と非終端記号

「終端記号( terminal )」は「トークン」と同じ意味で、それ以上分割不能な最小単位のことを指す。対して「非終端記号( non-terminal )」とは、「終端記号」の組み合わせによって生ずる、より高次な表現である。

 例として、プログラミング言語における count, <=, 100 といった表現はそれぞれ、これ以上分解できない「終端記号」である。これらが集合し、 count <= 100 という表現を構成するとき、この要素は「非終端記号」である。

context-free grammer - 文脈自由文法

 形式言語における文法規則のこと。 count, <=, 100 という「終端記号」を利用して、 count <= 100 という「非終端記号」を表現できると上に書いたが、この count <= 100 が有効なことは当然、文法によって保証されていなければならない。

 あらゆる文脈自由文法は、「どのように終端記号を組み合わせて非終端記号を構成できるか」というボトムアップな指向性と、「どのように非終端記号を終端記号に分解できるか」というトップダウンな指向性の両面を規約する。

parsing - 構文解析

 入力されたテキストが文法に適っているかどうかを検証する行為のこと。

 コンパイラ構文解析を行うとは、与えられたソースコードの文字列に文法規則との完全な対応関係を与えるということである。またコンパイルエラーとは、ソースコードを文法規則に当てはめることに失敗したということに他ならない。

parse tree - 構文木

 構文解析の結果を表現するデータ構造のこと。文法規則は一般に階層構造をとるため、それを表現するにおいても階層構造をもつ木構造が利用される。

recursive descent parsing - 再帰下降構文解析

構文木」を作成するアルゴリズムのうち、トップダウンに実行されるアルゴリズムのひとつに「再帰下降構文解析」がある。

 構文解析プログラムが「非終端記号」に遭遇するたびに、それを「終端言語」に再帰的に解析しつづけることで、すべてのトークンをもれなく処理しきることができることとなる。

LL(1) parser - LL(1) 構文解析

 再帰的に構文解析する際には、解析前に解析対象の中身を知る必要がしばしば生じる。

 例えばこれから解析しようとしている表現が while 文であるのか if 文であるのかによって、解析の仕方は異なる。そこであらかじめ解析対象の表現の先頭のトークンを覗き見て判断を下すのが、 LL(1) と呼ばれる方式である 。要するに、先頭のトークンが while であれば表現全体が while 文であることは明らかであるし、 if についても同様である、ということになる。

 先頭から k 個のトークンを先読みして初めて構文解析が可能な言語も成立しうる。こうして一般化される構文解析の方式のことを LL(k) と呼ぶ。ただし k が 1 に近いほど構文解析が容易であることは言うまでもない。

Go で仮想マシンを作った

 アセンブラを実装した前回の記事に続いて、 The Elements of Computing Systems にしたがって仮想マシンを実装した。その所感を本記事にまとめる。

 以下でも述べるが、仮想マシンとはいっても VirtualBox のような代物ではない。より限定的な意味における仮想マシンであるし、実装も学習用のものにすぎない。異なる目的を持ってこの記事を訪れていただいた方は注意のこと!

TL;DR

 レポジトリを作成して公開している

github.com

利用するテクスト

 The Elements of Computing Systems は Nand2Tetris という愛称でも親しまれている、コンピューター・サイエンスの著名な教科書である。

www.nand2tetris.org

 基礎回路の設計から初めて、論理演算、CPU設計、機械語アセンブリコンパイラそしてOSと、演繹的な手順でコンピューターを実装する過程が演習として読者に与えられる。

コンピュータシステムにおける仮想マシンの位置付け

 高級言語機械語コンパイルするにあたって、「直接機械語コンパイルし、マシンに実行させる」というやり方が考えられるが、これではプラットフォームの数だけコンパイラを実装しなければならないことになる。

 そこで、いちど中間コードにコンパイルした上で、機械語コンパイルし直す方式を考える。簡単にいえば、中間コードをインターフェイスとして定義することで、モノリシックなアーキテクチャを二つの疎結合なプログラムに分離するわけだ。こうすることで、プラットフォーム間の移植にあたっても、コンパイラを丸ごと作り直すのではなく、中間コードのインタプリタだけを実装すれば良くなる。端的にいって、より手軽に移植可能になるわけである。

 このようにして要請される、中間コードのインタプリタのことを仮想マシンと呼ぶ。このパラダイムは70年代に pascal 言語が切り拓いた領域で、90年代以後に JavaC# がおのおののコンパイル方式に採用したことで、揺るぎない影響力をもつようになったのだそう。

 今回はテキストが提供する Hack と呼ばれる言語の仕様に沿って、その中間コードを実行する仮想マシンを実装した格好となる。

なぜ Go を選んだか

 アセンブラの実装に Go を選択した前回の記事をもっぱら踏襲している。

jnsato.hateblo.jp

実装

 テキストの指示に従って、スタックで実装している。このスタックひとつのおかげで、ネストした関数の秩序正しい処理や、再帰呼び出しの制御が適切にコントロールできている。この魔法のような仕組みを実装を通して学べたのが最大の収穫である

 実装の詳細についていうと、プログラムは構文解析を行う parser パッケージと、解析された構文からアセンブリを自動生成する codewriter パッケージの二つからなる。 parser については、もっぱら空白文字と改行文字だけを区切りとみなす仕様の明快さによって、簡単に処理系を作成することができる。

 問題は codewriter によるアセンブリコード生成の自動化である。レジスタとメモリを操作するコードを泥臭く書いていくのだが、しょっちゅうアドレスを取り違えてはプログラムがクラッシュしたり、無限ループを脱出できなくなるなど、大変な目にあった。

 デバッグするにも1クロックごとにハードウェアが何をどう実行しているのか愚直に調べるほか手掛かりを得られず、何度も心が折れそうになりながら実装していた。数百行におよぶ低レベルの生成コードを読んで、どこで何を間違えたのか探し当てようとする作業は、目隠しをされたまま終わりの見えないトンネルを進むような心地だった。これは人生で一番辛いデバッグ作業のひとつに数えても遜色ない。

 無事追えられたことが奇跡のようにすら思える、それくらい苦しく、厳しい実装だった。もちろん、それを終えられた達成感も滅多に味わえない甘美なものである。

反省点

CodeWriter 構造体の設計

 構造体に内部状態を保存するためにプロパティをどんどん追加してしまったのはあまり満足のいく実装ではない。 Go の作法としてもバッドプラクティスだろうと直感している。洗練度に欠ける設計ではあるが、アセンブリの生成に精力を奪われて、コードの良し悪しにまで気をはらう余裕を失ってしまっていたし、それをリライトする元気も最後には残らなかった。

冗長なボイラープレート

 スタックに対する push/pop の操作をあちらこちらで頻繁にアセンブリで記述している。決まり切った表現と知りつつも、レジスタの状態に依存していつでも動作が狂いうる手前、DRY原則に沿って整理する勇気を持てなかった。

 結果として、似たような記述が乱立しており、行儀のいいコードとは評価しがたいだろう。他方で、仮に似た記述を整理したとして、それによってかえって出力コードを予期しづらくなるだろうという思いもあった。実際、似た記述と思って一箇所にまとめた結果、あとから判明したバグによって、再度別のルーチンとして分離し直さなければならないような場面もあった。

 要するに、実装途中の段階で早々にコードをまとめるよりも、最後まで実装してみることを優先した格好である。よりよい手法もあるのだろうが、個人的には、ボイラープレートを量産してでもひとつひとつの構成要素がまとまりを持つ方が好みであると改めて実感した。ちょうど React コンポーネントの定義時に、その都度ボイラープレートを書いていく必要がどうしても出てしまうのも、同じ背景ではないだろうかと考えた。

文字列の扱いのぎこちなさ

 文字列を組み立てるのがどうにもぎこちない思いであったが、これは Go の仕様上そうする他ないと割り切った。それは次のようなコードである。

initializeSP := "@256\n" +
    "D=A\n" +
    "@SP\n" +
    "M=D\n"

  codewriter パッケージをのぞいてもらえれば一目瞭然だろう。生成される行の単位で文字列を結合していっているわけだが、改行文字はつけ忘れるし、+の打ち忘れでコンパイラに怒られるしで、お世辞にも書きやすいとはいえなかった。

 ヒアドキュメント的なものがあればいいなと何度も思ったが、標準パッケージには見受けられなかったので、文字列の結合で押し通した。とはいえもっといいやり方がないかはぜひ知りたいところである。

終わりに

 もっとも基本的なデータ構造としてとうに知った気になっていたスタック構造が、高級言語のフロー制御をもっぱら一手に引き受けて管理しているということを知るのは、目からウロコの思いだった。それを実装してみて、ハードウェアが確かに期待した通りの動作を自律的に行っているのを眺めていると、人類の発明は偉大だという感慨に打たれた。

 そしてそれを他ならない自分の手で実装できたのだという達成感は、初めての Hello World! を遥かにしのぐ驚きと喜びを与えてくれるものだった。テキストに従って進めただけとはいえ、無事にやり遂げられたことを誇りに思う。

 一方で、学習用途の、限定された機能セットしか持たない言語の実装ですら、たいへん難渋するものだということがよくわかった。

「複雑なスキームを実現するためには、誰かがハードワークを背負わなければならない。つまり高級言語でより抽象的な表現を可能にするために、低級言語のレイヤでより複雑な実装を行うのだ」という記述がテキストに述べられていた。

 日ごろは高級言語のレールに沿って仕事をしている。複雑な仕様であってもそれなりに難なく実装できるし、バグが起こってもロジカルに影響範囲を切り分けて対処することができる。しかしその平穏は、まさに今回実装したような、低レイヤにおける驚くべき着想と恐ろしく手のこんだ実装のおかげで実現せられているのだという学びが、今回の収穫である。

 こうしたパラダイムそのものを考案してくれた先人たち(とりわけ Pascal の設計者である ニクラウス・ヴィルト氏 )、そして今日でもこうした低レイヤでの実装を行ってくれている、世界のどこかの技術者たちに、心からの敬意を持つ。そして彼らが用意してくれた仕組みのおかげで、パワフルな高級言語の恩恵に浴することができるという幸福をよく自覚して、最大限に無駄のない、エレガントなコードを書きたいものだと思いを新たにもしている。

Go でアセンブラを作ってみた

 先週のポストで、プライベートの学習がうまく進められていないことを書いた。不思議なもので、言語化してアウトプットしてみたら気が楽になったのか、ここ一週間はだいぶ調子がよくなってきている。

 で、先月来少しずつ読み進めている The Elements of Computing Systems にそって、アセンブラを実装したので、この記事ではそれを紹介しようと思う。

TL;DR

レポジトリを公開している。

github.com

利用するテキスト

 The Elements of Computing Systems は Nand2Tetris という愛称でも親しまれている、コンピューター・サイエンスの著名な教科書である。

www.nand2tetris.org

 基礎回路の設計から初めて、論理演算、CPU設計、機械語アセンブリコンパイラそしてOSと、演繹的な手順でコンピューターを実装する過程が演習として読者に与えられる。

コンピュータシステムにおけるアセンブラの位置付け

 アセンブラを実装するのは上記テキストの第6章になる。これに先立つ第5章までにハードウェアの論理回路の実装は終えられており、ソフトウェア層の実装の嚆矢として与えられる課題がアセンブラということになる。

 アセンブリ言語で記述されたコードをCPUに理解可能な機械語に変換するアセンブラこそ、ソフトウェアとハードウェアの架け橋となるソフトウェアだという位置付けが与えられている。

なぜ Go を選んだか

 テキストでは API 仕様書だけが与えられ、実装についてはどの言語で行ってもいいと指示されている。つまり C で実装してもいいし、 Ruby で実装してもよかったわけである。

 アセンブラの実装、と書くと高いハードルに感じられても、本質的にはテキスト処理プログラムにすぎない。手慣れた Ruby で実装してもよかったのだが、特に実装スピードを重視したい訳でもないし、手癖で実装しても勉強になるだろうかという思いがあった。

 他方で Go については、以前より文法を学んで簡単なプログラムを作ったりしていたが、

  • まとまりのあるパッケージを作成したことがない
  • 文字列やストリームの処理をやったことがない

という未熟さを、アセンブラのテキスト処理の実装を通して克服できそうな期待があり、これを選択した。

実装

 API の外形的な振る舞いは仕様としてテキストに与えられているので、それをひとつずつ実装していけばそれでよかった。API命名、入力、出力の型が明確に仕様化されているため、命名に呻吟する必要はないし、 Go らしいテーブル駆動テストで進めやすく、非常にやりやすい。一部入力チェックでエラーが起こりうるようなところだけエラーを返すような設計に変更したが、 API ごとにテスト駆動でガシガシ進められるのは快感であった。

 またモジュールレベルで十分にテストができたおかげで、それらモジュールを呼び出すメインパッケージのコーディングもだいぶ楽に進められた。逆にメインパッケージのテストの粒度やテスト項目に何を定義すべきかで迷ってしまうくらいであった。ほとんどの入出力がモジュールレベルでテスト済みのとき、呼び出し元でもう一度統合テストのようなものを書くべきなのか? とか、もし統合テストを書くのであれば、テーブル駆動テストではなく、もっとそれに適したやり方があるのではないか? などの迷いである。

 とはいえ、単体の API をテスト駆動で開発するサイクルをいくつも繰り返す形で、わかりやすい進捗を残しながら開発できたため、プライベートの開発にしてはずいぶん効率的に進められた満足感はある。

動作

 レポジトリの README に書いた通りにはなるが、テキストに沿って次のような動作確認をしている。

  1. テキストが提供するアセンブリコードを自作アセンブラに入力し、出力を保存する
  2. テキスト付属のアセンブラに同じアセンブリコードを入力し、出力を比較する

例えば、 testdata/Add.asm に配置した足し算プログラムを機械語に変換するには

the-hack-assembler < testdata/Add.asm

とファイル内容を標準入力として与えることを想定している。

反省点

不器用なストリーム処理

 仕様上、入力されたストリームを二回舐める必要があるのだが、 RubyIO#rewind に相当する API が Go にはなさそうで、仕方なくストリームを先頭から読み直すためだけに入力を文字列として内部に保持してしまっている。

 具体的には、入力されたストリームを ioutil.ReadAll() で全行スキャンした上で、それを文字列として内部に保持しつつ、 parser.Reset() が呼び出されるごとに新しい io.Reader として定義し直すことで、複数回読み取ることができるようにしている。

 しかし例えば数万行のアセンブラを変換するとなったときに、せっかくストリームで受け取れる入力をわざわざ文字列に変換して、プログラムが終了するまでずっとメモリにのせておくのは効率が悪いなあと思っている。

コードレビューを受けたかった

 上記の件のみならず、基本的なグッドプラクティスもよく知ることができないままに Go を書いてしまっている。メンターとして頼れる知人がいないため、独自実装で進め切ってしまったが、できればコードレビューを受けながら進めたいところだった。

終わりに

 以上、アセンブリ言語のコードを機械語に翻訳するプログラムを Go で作成した話を書かせてもらった。

 学部生向けのテキストにのっとって実装しているわけだが、そもそもアセンブラってなんだっけ? というレベルから改めて知識を整理した上で、実装まで行うことでその知識を血肉化するという意味で、非常に有意義な勉強ができたと思っている。

 情報系専攻の学生は学部時代にこんなことをやっているんだなあ、というのがわかるのもよい。もっとも、学ぶ意欲においては並の学部生を圧倒しているはずと自信を持って、無意味なルサンチマンは持たないようにする。

 Go についていうと、最初の10行を書く上ではゆっくりと手探りしながら着手したが、通常のテスト駆動開発のサイクルに軌道を乗せてからあとは、非常に楽に進められた。標準パッケージの知識が少ないため、例えば文字列の簡単な操作にしても逐一ドキュメントを見ながらの開発になったわけだが、そのせいで劇的に作業が遅くなったとは思わない。テストを書くために RSpec のような独自 DSL を学ぶ必要がないのも魅力的だし、とてもいい印象だけ得られた。

 強いていえば、上述した IO#rewind の件のように、 RubyAPI はやっぱり開発体験優位に設計されているのだなあというのは感じた。しかしまあ、これは別言語が別 API を備えているというだけの当たり前の話で、特に優劣の判断材料とするには及ばない。

 今回学んだアセンブラ以後の章ではバーチャルマシンとコンパイラ、そして OS を作成することになっている。それらも同じように Go で開発してみたいなと思っている。

AtCoder のレートが茶色になった

 週末の ABC179 にてようやく茶色コーダーになれた。9度目の参加にしての昇級であった。

atcoder.jp

 戦績を振り返ってみると、初参加が5月3日の ABC166 で、ここまで四ヶ月半かかっている。そしてその5月こそ毎週参加できていたが、その後は忙しさがこうじて参加数が落ちている。月に一度参加するくらいの頻度だ。

 過去問を解く「精進」についても、6月から8月にかけては毎日続けられていたけれど、仕事にかまけてある日断絶してしまい、その後は遠ざかっている。心が折れた...というよりは、突然『真田丸』にハマってしまって、終業後の優先順位を奪い取ってしまったのが根幹だったりする。

 競技に対する手応えでいうと、慣れのおかげでパフォーマンスを出せているにすぎず、アルゴリズム構築能力としてはそれほど成長できているかは疑わしい。

 C問題までの、自分の能力で確実に解ける問題を短時間で確実に解く、という意味では安定しており、そのおかげでレーティングを伸ばせてはいる。他方で、D問題以降の、ストレッチしないと解けない問題に対しては、手も足も出ないことも多い。終了後に解説を見れば腑に落ちることも多いため、自分の能力をはるかに超えた考察が要求されているのではないはずと思っている。実装についても問題はない。ただ、自分の頭で着想を生み出すことがどうにも難しい。

 悔しいと思うこともある。その悔しさをバネに「精進」にのめり込むのが成長の近道なのだろうが、現状そうできていないのは、「まあこんなものだろう」という諦めなのだろうか。そうであれば、嫌な大人になってしまったなあと思わざるをえない。

 とはいえ、可処分時間の半分は労働に費やさねばならないのは揺るぎない制約であるし、残りの時間をどう配分するかにも限度はある。学生時代に出会えていればまた違った付き合い方もあったのだろうが、それは悔いてもどうしようもないことなので、慌てずに自分なりのやり方で付き合っていく他ないのだろう。

 いずれ、いまからトッププレイヤーを目指すわけでもあるまいし、今後も変わらず趣味としてのんびりと取り組んでいくつもりである。次の目標としては、緑を目指すのはさておき、D問題の解答率を伸ばせるようにすることかな、と思っている。

下降気味のモチベーション

 8月に読むテキストを書いた。本当であれば月末に振り返りの記事を書くはずだった。結局、8月のうちに終わらせることはできなかったし、9月ももうすぐ半分といういまの時点でもまだ半分と進められていない。

jnsato.hateblo.jp

 やっぱり、仕事をしながら勉強も進めるというのは容易でない。だからこそそれは理想であるのだと痛感している。

 案件のリリースがあった関係で、先月の稼働時間は大きく増えていた。楽しさはあれどおのずとストレスも溜まり、終業後や週末の空いた時間にまでコンピュータに向かうのは控えていた。代わりにジムで身体を動かしたり、料理をしたりしていた。まあまあ健康的な生活は守れたわけだが、なんだか人生が守りに入ってしまっているような気がしてどこか収まりの悪さを感じている。

 仕事に学びの機会を求めるのが筋違いであることは感じざるを得ない。もちろん学びはある。しかし密度は期待できない。日々の業務はどうしても、自分の知識を小分けにしてアウトプットしている感触であって、濃度のあるインプットは結局自分で求めるしかなさそうだ。

 そうわかっていても、終業時間間際に依頼が来ると対応しないわけには行かないと考えてついサービスしてしまうし、週末も何かまずいことが起きていないかと、つい slack を覗いてしまう。そうして少しずつ疲弊してしまい、自分だけの楽しみのためにプログラミングをする時間が減ってしまう傾向がある。

 仕事とはそういうものなのだろうし、結局のところは自分自身の問題に他ならないのだが、どう落としどころを見つけていくべきなのかはわからない。単純に、春以来あったモチベーションの波がピークを打って下降気味なだけで、しばらくすればまた気持ちよく向上心を持てるようになるのかもしれない。というか、そうあることを祈るくらいしかしていない。

 なにかとっかかりがあれば取り戻せる気はする。それがなにであるのか、意識的に探しあてないといけないと感じている。

Amazon RDS for PostgreSQL のタイムゾーンはほとんど常に UTC であることに要注意

 postgres の current_timestamp 関数を使ったクエリをアプリケーションに実装していたところ、本番データベースにタイムゾーンのずれによる時刻の不整合を招きいれてしまった。

TL;DR

  • RDSで非 UTC の時刻を扱うユースケースでは current_timestampcurrent_date といったタイムゾーン依存の関数は使うべきでなく、プログラム経由で動的に生成するのがよい。
  • 状況を再現させるコードをレポジトリに公開しているので、詳しくはこちらを参照いただけるといいかと思う

github.com

再現する条件

アプリケーションのタイムゾーンとして UTC を使っていないこと

 筆者の属するプロジェクトでは rails (activerecord) を使っており、タイムゾーンの設定は次のように JST を指定している。ここで UTC を指定しているような場合、特に問題はないだろうと思われる。

# config/application.rb

module SomeApplication
  class Application < Rails::Application
    ...
    config.time_zone = 'Tokyo'
    config.active_record.default_timezone = :local
    # config.active_record.default_timezone = :utc
    ...
  end
end

データベースに Amazon RDS を利用していること

 RDS のタイムゾーンについて、AWSの公式FAQに次のようなエントリがある。

aws.amazon.com

すべての Amazon RDS DB インスタンスは、デフォルトで UTC/GMT 時間を使用します。タイムゾーンの変更はオプションです。

データベースレイヤーで UTC タイムゾーンを使用するのがベストプラクティスです。UTC は夏時間 (DST) を持たないため、後でシフトするときに時間を調整する必要はありません。

ローカルタイムゾーンを使用する必要がある場合は、代わりにアプリケーションレイヤーでタイムゾーンを変換します。

より具体的には

UPDATE users SET updated_at = current_timestamp;

のように current_timestamp を直接埋め込んだクエリを発行すると、データベースのタイムゾーン、すなわち UTC が採用されて、アプリケーションのタイムゾーンと齟齬が生じる。

 アプリケーション層でローカル時間を生成してクエリに渡すには、次のようなアプローチをとる必要がある。以下は activerecord を前提に記述するが、やるべきことはどの言語/フレームワークを採用していても変わらないはず。

ActiveRecord::Base.sanitize_sql_array(['UPDATE users SET udpated_at = ?', Time.current])

 もっとも、多くの場合はこのようなまわりくどい書き方をする必要がない可能性も高い。 activerecord であれば、単に user.touch と記述すれば上記のユースケースは満たせる。

 とはいえ筆者の場合、ある程度の大きさのバッチタスクの一部にて、大量のレコードをインスタンス化せずに一括更新する箇所でこの問題に遭遇した。似たような条件に出くわすことはないとはいえず、またそのような場合には十分な注意が必要となる。

AtCoder Problems の Training モードで中難度をコンプリートした

 4月半ばに AtCoder の問題を解くことを初めて、ほぼ4ヶ月となる。

 5月にかけて集中して過去問に取り組んで、その後すこし熱がおさまっていた。これはもっぱら Project Euler に心変わりしていたというのが大きい。直近ではむしろ Project Euler に取り組むのが後回しになってしまっているものの、代わりに AtCoder の過去問は一日一問取り組むように自分に課してきた。そのひとつの区切りがやってきたので、記録しておく。

 一日一問のペースで100問を解いたわけだから、ほとんど3ヶ月かけてやってきたことになる。言ってみれば同じ程度の難易度の問題に繰り返し挑んでいたわけだから、それほど劇的に腕がよくなったという感覚はない。

 ただそれでも、はじめの頃を思い返すと、そもそもの方針すら思い浮かばずに頭も手も止まってしまう、ということは減った気がする。困難な問題に出会うこともあるが、しっかり考えて向き合えばそれなりの速さで解法にたどり着けるようになっているのかもしれない。

 もっとも、解法は見つけられても、エッジケースを特定できずにいつまでも正解できないということは高い頻度で起こる。そのあたりが自分の課題とわかってはいても、どういう練習を積めばそれを手早く発見できるかはわからない。

 コンテストにはぼちぼち参加している。スケジュールが合わないことが多いのがほんの小さなフラストレーションである。それほど週末に予定を詰め込んでいるわけでもないのだが、コンテスト日に限って忙しい。土曜開催と日曜開催のランダムさに振り回されているようなところはある。とはいえ、あわててレーティングをあげる必要もないし、やったところでそんなに劇的には上がらないだろうから、引き続きのんびり取り組んでいくつもりでいる。