- 左から右へプログラミングする方法では、コードを入力した瞬間からプログラムが有効な状態を保つため、エディタの自動補完などのツール支援を最大化できる
- Pythonのリスト内包表記は、宣言されていない変数と型推論の欠如により、自動補完機能を妨げる
- RustやJavaScriptは、プログラムを左から右へ自然に構成できるため、変数の利用やメソッド探索がより直感的になる
- CとPythonの関数型スタイルは、関数名や構造の発見しにくさによって、効率的なコーディング体験を損なう
- 複雑度の高いロジックでは、左から右へ展開されるコードのほうが読みやすく、保守性や拡張性に優れている
左から右へプログラミングする
コードは入力した瞬間から有効でなければならない
Pythonのリスト内包表記の限界
- Pythonのリスト内包表記構文
words_on_lines = [line.split() for line in text.splitlines()] は、宣言されていない変数(line)にアクセスする必要があるため、エディタが自動補完や型推論を十分に提供できないという問題がある
- コードを部分的に入力する過程では
words_on_lines = [line.sp のように入力すると、エディタは line の型を知らないため、メソッドを提案できない
- 変数名のタイプミス(
lime など)のような潜在的なエラーも検出しにくくなる
- 適切な提案を受けるには未完成のコードを書かなければならず、その過程が直感的でなく不便になる
Rustにおける左から右の構成
- Rustの例(
let words_on_lines = text.lines().map(|line| line.split_whitespace());)では
- 無名関数の宣言とともに変数(
line)が最初に登場した瞬間に宣言と見なされるため、すぐに自動補完とメソッド提案が可能になる
- 実際に
split_whitespace というメソッドも自動提案のおかげで簡単に見つけられた
- この方法では、プログラムが常に部分的にでも有効な状態を保つため、IDEやエディタがリアルタイムでコーディングを支援できる
段階的開示(Progressive Disclosure)とAPIの使いやすさ
- 段階的開示(Progressive Disclosure) は、利用者が必要な分だけ複雑さを体験する設計原則であり、プログラミングにも適用できる
- 例: 画像を追加するときだけ関連オプションが現れるワードプロセッサのUXに似ている
- C言語ではこのような支援が不足している
FILE *file に関連するすべての関数が file. から探索できるわけではないため、関数名のパターン(fread, fclose など)を覚えなければならず、機能を発見しにくい
- 一方で理想的な言語なら、
file. を通じたメソッド提案によって関連機能を簡単に段階的に発見できる
関数とメソッドの発見しやすさの違い
- Pythonの
map(len, text.split()) とJavaScriptの text.split(" ").map(word => word.length) の例を比較すると
- Pythonでは
len、length、size など関数名が予想しづらく、何度か試して初めて実際の動作が分かる
- JavaScriptでは
word. の後に .l だけ入力しても、エディタが length などのメソッドを提示するため、発見しやすさが高い
map のような高階関数でも、実際の戻り値とデータ型が即座に明確になる
複雑なロジックほど構造的に書く利点
- 複雑度の高いロジック(
filter、lambda が入れ子になった長いPythonコード)では
- コードの始まりと終わりを繰り返し確認する必要があり、条件式や括弧の対応などで可読性の低下と理解の難しさが生じる
- 同じロジックのJavaScript版では、コードを上から下へ、左から右へ順番に読んで理解できる
核心原則
コードは入力する瞬間ごとに有効でなければならない
text 単独の入力でもプログラムは有効な状態を保つ
text.split(" ") まで書いても、その後 .map(word => word.length) まで続けて入力するときも、全体として常に中間状態が有効である
- このようなコーディングパターンは、エディタのリアルタイム支援の可能性を高め、REPL環境では即座に結果を確認することもできる
結論
- APIと言語設計は、コードを左から右へ自然に入力しながら、中間段階ごとに有効なプログラムを作れるよう支援すべきである
- 良いAPI設計こそが、このようなコーディング体験の改善の鍵である
1件のコメント
Hacker Newsのコメント
FROMではなくSELECTで始まることだと思う。どのエンティティ(テーブル)を扱っているのかをすぐ把握しづらく、スマートエディタがクエリ作成をより効率的に支援するうえでも邪魔になる。FROM -> SELECT -> WHEREの順のほうが自然だ。特にSELECT節でカラム名を決めてWHEREでそれを参照するのだからなおさらそう思う。実際、SELECT * FROM tableの代わりにFROM tableだけ書けばSELECT節は省略可能でもよいはずだ。こんな不満を言うと小言の多い年寄りみたいに聞こえるかもしれないが、単なる個人的な郷愁だFROMが先に来るクエリ順を採用している。BigQueryにも最近パイプ/矢印構文が追加されたし、DuckDBのコミュニティ拡張もあるのでおすすめだ。DuckDB - PSQL, DuckDB - PRQLWHEREでカラム別名を使えない。selection(WHERE)はprojection(SELECT)より前に行われるからだ。ちなみにMySQL 8にはTABLE <table>という構文もあるので参考になるFROM -> WHERE -> SELECTだ。だからSELECTで定義したカラム別名はGROUP BY、HAVING、ORDER BYでは使えるが、WHEREでは使えないFROMが先に来る構造だ。IDEで他の節を書いているときもオートコンプリートのおかげですぐフィールド候補を受け取れるので、この構造はよいと思うFROMで始まる変種がもっと積極的に導入されるべきだし、それは難しいことでもないと思う。使い勝手を改善する試みが足りないfunctools.mapがあるように)returnなど){3} for {2} in {1}のような順でもよい。こうしたツールは「読みやすい構文」と「打ちやすい構文」のあいだの妥協点を提供してくれる。私はツーリングを活用してでも読みやすい構文を優先する側に一票を投じたい。必ずしも"for-in"構造にこだわる必要はないと思うlet foo = ...のように結果を変数に代入する代わりに、... =: fooのような形式も使ってみたいbake(divide(add(knead(mix(flour, water, sugar, butter)),eggs),12),450,12)のように入れ子にしていく代わりに、mix(flour, water, sugar, butter) %>% knead() %>% add(eggs) %>% divide(12) %>% bake(temp=450, minutes=12)のようにパイプを使えば、ずっと簡単で見やすいfrom some_library import child_moduleという構文は非常に直感的だ。JSではimport { asYetUnknownModule } from SomeLibraryのような構造で、ずっと直感的でないと感じるfromキーワードにこだわる理由がよくわからない。ただ こうすればいいと思うlen(list(filter(lambda line: all([abs(x) >= 1 and abs(x) <= 3 for x in line]) and (all([x > 0 for x in line]) or all([x < 0 for x in line])), diffs)))のように書くより、NumPy arrayを使ったほうが、メモリ上に新しいリストを作る必要がなく、行全体を一度に扱うのにもずっと向いている。たとえば こちらのほうがずっと「左から右」をよく体現しているline > 0は問題ないが、ブロードキャスト規則は複雑になりうる)ものの、筆者が挙げたJavaScriptの例や、C#、Java、Scalaのような型の厳密な言語のコレクションAPIのほうがすっきりしている。私の好みはKotlinで、次のように書けるのが好きだ