世界を動かす技術を、日本語で。

左から右へのプログラミング

概要

  • プログラムは 入力途中でも有効 であるべきという原則の重要性
  • Pythonのリスト内包表記やC言語の関数呼び出しの エディタ補助の難しさ
  • RustやJavaScriptの 左から右への構築 による補完性の高さ
  • API設計とプログレッシブディスクロージャー の観点からの比較
  • 良いAPI設計 がユーザー体験を大きく向上させる事例の紹介

プログラムは入力途中でも有効であるべき

  • Pythonのリスト内包表記 は、書き始めの段階でエディタの補完が機能しない問題
    • 例:[line.split() for line in text.splitlines()]line は宣言前で補完不可
    • split()メソッドが存在するかも書き終わるまで不明
  • エディタの補完機能 は変数や型が明確になった時点でしか有効にならない現状
  • Rustの例 では、左から右へとプログラムを構築することで、逐次的に補完が可能
    • 例:text.lines().map(|line| line.split_whitespace())
    • lineを宣言した瞬間からメソッド補完が可能
  • Haskell の例(map words $ lines text)は原則から外れるが、最もエレガント

プログレッシブディスクロージャーとAPI設計

  • プログレッシブディスクロージャー :必要な複雑さだけをタイミングよく提示する設計原則
    • 例:Wordで画像を挿入した時にだけ画像回り込みオプションが出現
  • C言語の関数呼び出し は、構造体にメソッドがないため、関数名を知っていなければならない不便さ
    • 例:FILE *fileの場合、file.readのように補完できず、freadなど関数名を探す必要
    • 関連するcloseメソッドの存在を直感的に知ることができない
  • Pythonや他言語 でも同様の発見性の低さが課題
    • 例:len, length, size, countなど長さ取得関数の命名が統一されていない

JavaScriptとRustの良い例

  • JavaScript では、text.split(" ").map(word => word.length)のように、左から右へと構築しながら補完が機能
    • word.lと入力した時点でlengthが補完候補に出現
    • map関数も型に合ったものだけが補完
  • Rust も同様に、チェーン式で型安全かつ補完が効く
  • Python は関数型スタイル(map(len, text.split()))で可読性は高いが、発見性や補完性で劣る

複雑なロジックにおける可読性

  • Pythonの複雑な内包表記 は、ネストや条件式が増えると可読性が著しく低下
    • 例:len(list(filter(lambda line: ... , diffs))) のように括弧や条件の対応が分かりづらい
  • JavaScript では、diffs.filter(...).length のように左から右へ意味が明確に伝わる
    • ロジックの流れが直線的で理解しやすい

結論:良いAPI設計の重要性

  • プログラムは常に有効な状態であるべき という設計思想
    • 入力途中でも型やメソッドが明確になり、エディタ補完やREPLでの即時確認が可能
  • API設計 はユーザー体験を大きく左右
    • 左から右へと構築できるAPIは、発見性・補完性・可読性が高い
  • より良いAPIとエディタ体験 を目指すことの重要性

Hackerたちの意見

プログラムは、入力された通りに有効であるべきだよね。もし開発者がいつも左から右へ、1文字ずつ、1行ずつコードを書くなら、すごくいいと思う。でも現実は、いろいろなところに飛び跳ねながら、いくつかのことを埋めたり、他のことを unfinished にしたりしながら戻ってくることが多いんだ。時々、変数で動作するコードを書いた後、1分後にその変数を宣言することもあるし、テスト用の値を割り当てたりもする。

その通り。新しいファイルの時だけ、コードを順番に書くよね。もしクラスに新しいフィールドを追加しようと思ったら、必ずしもクラス定義から始めるわけじゃない。アイデアが浮かんだ時に IDE がそのフィールドを使ってたから、まずはそのコードを書くことが多い。条件チェックを強化したい時も、if や else を並べ替えている間は、そのコードが有効じゃない時期があるよね。

うん、強制するのはちょっと無理がある制限に思える。

この記事のテーマとはちょっと違うけど、面白いよね。

これは「彼を料理させてあげよう」ってやつだね。記事を読んでいくと、著者がプログレッシブ・ディスクロージャーの哲学について主張してる。最後の段落でまとめていて、合理的な意見だと思う。> 「テキストを入力した時点で、プログラムは有効です。text.split(" ")を入力した時点で、プログラムは有効です。text.split(" ").map(word => word.length)を入力した時点で、プログラムは有効です。プログラムを構築する過程で有効であるため、エディタがサポートしてくれるんです。REPLがあれば、プログラムを入力しながら結果を見ることもできる。CoPilotやエージェントコーダーの時代に、エルゴノミクスがどれほど重要かは分からないけど、LSPをコーディングすることは確かにその主張に満足感を与えるだろうね。

コードは一度書かれて、何度も読まれるからね。順番に読めるコードは、ジャンプしなきゃいけないコードよりも速くて読みやすい。

これには同意するけど、もう一つの原則に繋がるんだよね。多くの言語がこれを無視してるけど、書き終わってないからってコンパイルに失敗するのはおかしいよ!他のブロッキングしない方法で失敗すべきだと思う。でも、いくつかの言語は、戻り値がなかったり未使用の変数があるとエラーを出しちゃうから、それができないんだよね。

あと、相互再帰もね。 ;)

IDEが俺が実際に入力する順番とは違う順番で入力すると思ってるのは、確かにちょっとイライラするね。

SQL はこの問題があって、SELECT リストを FROM/JOIN の前に要求するんだよね。入れ替えられる SQL 系のものも見たことがあるけど、全部入れ替え可能にすべきだと思う。

なんで Python がこんなに人気なのか分からない。1人以上が関わると、痛い言語だよ。著者が言ってることは氷山の一角に過ぎない。

初心者にはいい言語だけど、残念ながら多くの人が他の言語を学ぶのは難しくて無駄だと思ってる。

依存関係が必要になると、1人の開発者には痛いかもね(virtualenv...)。

共有コードベースで作業していると、リスト内包表記を避けたシンプルなスタイルのPythonを書いてしまうんだよね。やり方が一つしかないはずの言語なのに、実際にはやり方がめっちゃ多い。リスト内包表記を書くのは確かに満足感があって、ちょっとしたゴルフみたいな感じもあるけど、もし一つのやり方があるべきなら、あれは必要ないと思う。

これは私の経験とは違うけど、私たちはGoogleスタイルガイドやリンター、静的型検証を使ってプログラムの書き方の選択肢を減らしてる。Pythonは「一つの正しい方法」からは確かに逸脱してるけど、私が普段使う言語の中では、JavaScriptと同じくらいの問題があって(C++よりはずっと少ないけど)、ユーザーごとに異なるクセに対処する必要がある。

以前はこれに完全に同意してたけど、型アノテーションやチェックのおかげでずっと合理的になったよ。大きなプロジェクトには選ばないけど、型があれば他の人のPythonコードを扱うのがずっと楽になった。厳格な型チェックと巨大な標準ライブラリを持つPythonは、今や僕のお気に入りのスクリプト言語だね。

それは、許容度が高くて多くの人にとって最初の言語だから、みんなが知っている簡単なものに固執するんだと思う。

なんか、修正しようとすると、別の痛みがあるだけなんだよね。オリジナルのスタイルでプログラミングしてるコードベースで働いたことがあるけど、あれは本当に大変。すごく neat なんだけど、40個も深いチェーンに入っちゃうとデバッグが地獄。言語の機能を使うしかないんだけど、pysparkみたいなやつは、12個の変換を追跡する必要があるときにはスケールしないし、結局は命令型スタイルのループに戻って、何がどこで起こってるかをログに残すしかないんだよね。

SQLも同じ問題を抱えていて、年齢を感じさせるね。クエリはFROM句から始めるべきで、そうすればどのエンティティが関与しているかすぐにわかるし、賢いエディタが合理的なクエリを書くのを手助けしてくれる。順番はFROM -> SELECT -> WHEREが理想で、SELECTは通常カラムに名前を付けるから、WHEREがそれを参照することになる。SELECT * FROM tableみたいな無駄を避けて、単にFROM tableと書いてSELECT句を暗黙的にすることもできる。気にしないで、私はただの恨みを持ったおじいさんだから、洞窟に戻るよ…

PRQLは面白いかもしれないよ。 https://prql-lang.org/

同意するけど、一部のSQLエディタは、最初にFROM句を入力するとSELECT句のコード補完を提供してくれるよ。

PSQL(とPRQL)はこの順序を使ってるし、最近BigQueryにも似たようなパイプ/矢印の表記が追加されたよ。DuckDBのコミュニティ拡張をチェックしてみて!: [0]: https://duckdb.org/community_extensions/extensions/psql.html [1]: https://duckdb.org/community_extensions/extensions/prql.html

そうそう、C#のSQLにコンパイルされるDSL(LINQ-to-SQL)も同じことをしてるよ。理由は、他の句を入力しながらIDEのコード補完でフィールドを提案できるようにするためなんだ。

そう書かれているのは、関係代数から来ているからで、通常(いつも?)プロジェクションが最初に書かれるんだよね。 「順番は FROM -> SELECT -> WHERE で、SELECT は一般的にカラムに名前を付けるから、WHERE でそれを参照することになる。SQLの標準では、WHERE句でカラムのエイリアスを使うことはできない。なぜなら、選択(また関係代数)はプロジェクションの前に行われるから。」 「SELECT * FROM table みたいな無駄を避けて、FROM table だけ書いて、SELECT句を暗黙的にすることもできる。正直言うと、MySQL 8 では TABLE を使えるから、これは SELECT * FROM のエイリアスなんだ。」

Kusto、Azureのデータ分析用クエリ言語もパイプを使ったその形式を採用しているよ。 https://learn.microsoft.com/en-us/kusto/query/?view=microsof... あと、.NETのLINQアプローチもね。確かに、SQLがFROMから始まるバリエーションを持つべき時期だと思うし、それをサポートするのはそんなに難しくないはず。改善する意欲がないように感じるね。

これは歴史的な決定で、SQLは宣言型言語だからね。正直言って、SQLの順番:FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT について、長い間混乱してたことを認めるよ。独学の開発者として、何が足りないのか分からなかったけど、今はメカニズムが明確になった。もし誰かが特定の名前でSELECTを扱う必要があるなら、CTEを使うべきだと思う。WITH src AS (SELECT * FROM sales), proj AS (SELECT customer_id, total_price AS total FROM src), filt AS (SELECT * FROM proj WHERE total > 100) SELECT * FROM filt;

ここでの主な懸念は、選択を始める人は、どこから選ぶかを知る前に何を選びたいかを知っていることが多いってこと。だから、カラムのソースに対するオートコンプリートは、ソースからのカラムに対するオートコンプリートよりもずっと役立つと思う。おそらく、ほとんどの人がオートコンプリートを必要とするカラムの総数はかなり限られているんじゃないかな?本当にそれが必要なら、ほとんどのカラムはタブで補完できるはずだし。大きなデータベースを扱っている少数の人たちは、クエリで合理的に取得できるほとんどのものをカバーするビューのセットを持っていると思う。

ちょっとした指摘だけど、selectはwhereのフィールドを宣言しないよ。selectはhavingのフィールドを宣言するんだ。whereで使えるフィールドを宣言するのはスキーマだよ。

自動補完の時は、まずFROM句から始めて、その上にSELECT句を追加することもできるよ。私はいつもこう始めるかな:select * from as limit 5

一部のIDEはコードテンプレートを提供していて、略語を入力すると、それに対応するコード構造がプレースホルダー付きで展開されるんだ。その後、Tabキーでプレースホルダーを次々に埋めていく。ここで重要なのは、プレースホルダーのタブ順が左から右である必要はないってこと。だからTFAの例では、{1}の中の{2}のために{3}の順番があってもいい。一般的に、読みやすい構文とタイプしやすい構文の間にはトレードオフがあって、私はツールサポートなしで読みやすい構文を持つことが好きだけど、タイプしやすくするためにはツールを使う必要があるってこと。これは上記のfor-in構文の主張ではなく、左から右への入力が厳密に必要ではないという主張なんだ。

一方で、Pythonには「from some_library import child_module」っていうのがあって、これはいつも便利だよね。JSでは「import { asYetUnknownModule } from SomeLibrary」っていうのがあるけど、こっちはあんまり役に立たないよね。

「from」ってキーワードに対するこの執着が理解できないんだよね。普通に「import SomeLibrary { asYetUnknownModule }」でいいじゃん。

逆に、JSの名前空間インポートを使えば、こんな感じで書けるよ:[1]: import * as someLibrary from "some-library" someLibrary.someFunction() これは、僕の経験ではIDEのオートコンプリートとうまく連携するよ。[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

ここでの合意は、Pythonにはパイプオペレーターが欠けてるってことみたいだね。MathematicaからRに移行するときに、これがどれだけ便利かすぐにわかったよ。データがいくつかの異なるステップで変換されるデータサイエンスのコードを書くとき、すごく読みやすくて直感的になるんだ。Pythonはデータサイエンスだけじゃなくて、もっといろんなことに使われてるのは知ってるから、他の文脈でもパイプが意味を持つのか聞いてみたいな。ただ、なんでパイプがPythonにまだ入ってないのか理解したいだけなんだ。

結果 = (df .pipe(fun1, arg1=1) .pipe(fun2, arg2=2) ) が結果 fun1(., arg1=1) |> fun2(., arg2=2) よりもずっと読みづらいかどうかは分からないけど、Rのやり方はデータフレームだけじゃなくて他でも使えるから、結構いいよね。

Rのパイプオペレーター(正確には、tidyverse R、これ自体が別の言語みたいなもんだけど)は、私にとっての「キラーアプリ」の一つだね。データを扱うのがめちゃくちゃ楽で簡単なんだ。クッキーのレシピを「コーディング」する二つの方法を示した教科書を覚えてるよ: 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) こっちの方がずっと簡単だよね!

パイプオペレーターの次のステップは、結果をキャッチするための逆代入文だと思う。'let foo = many lines of code'みたいなコードを見ると、どんどんイライラしてくる。'many lines of code =: foo'みたいに書かせてほしいな。

著者が求めてるのは、スラッシュコンビネーターの言語サポートだと思う。別名「パイプオペレーター」とも呼ばれてるやつね。要するに、このコンビネーターは、ネストされた呼び出しを表現できるようにするんだ。例えば、f(g(h(x)))を、h(x) |> g |> fのように書けるように。中置演算子を定義できる言語にとってはね。 編集:中置演算子を定義できない言語では、同じ目的を果たすandThenというファンクターメソッドがよく使われるよ。例えば、h(x).andThen(g).andThen(f)みたいにね。

これはほぼFP対OOPの宗教戦争みたいなもんだね。vim対emacsみたいに… vimではオペレーターが先だけど、emacsでは選択が先。何かを「英語のように読める」ように設計すると、動詞が先に来る構造になることが多い。Lisp/Schemeに見られるようにね。他の言語、例えばドイツ語やタミル語は動詞が最後に来るから、OOPの「名詞ファースト」な構文とよく合うんだ。(タミル語では「水飲む」って直訳だけど、英語では「drink water」だよね。)だから、タミル語で考えるとForthの方がSchemeより読みやすいかも。vimの方がemacsより使いやすいと感じるのもそのせいかも。どちらも特に優れているわけじゃないし、適切にツールを作ることができる。最近の言語モデルとも相性がいいしね。

「英語のように読めるようにデザインしたら、動詞が最初に来る構造になるだろうね。」 「命令文ならそうかもしれないけど、宣言文で英語のように読めるようにデザインしたら、主語が最初になるよ。」

Rustのfor..inループも同じ問題があるよね。イテレートしてるものの型や形状が「in」の部分に行くまでわからない。幸運なことに、呼び出すメソッドがないから、何もできないんだ。同様に、Pythonのリストや辞書、セットの内包表記は、特定の構造を簡単に作るためのforループの構文シュガーだね。functools.mapsを使えば、Rustの例と全く同じ動作が得られるよ。もしこれが超重要なテキスト入力のマイクロ最適化だったら、みんなLispみたいに純粋関数でやってるはずなのに、なぜか機能言語は最も人気があるわけじゃないんだよね。