4 ポイント 投稿者 GN⁺ 2025-08-20 | 1件のコメント | WhatsAppで共有
  • 左から右へプログラミングする方法では、コードを入力した瞬間からプログラムが有効な状態を保つため、エディタの自動補完などのツール支援を最大化できる
  • 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では lenlengthsize など関数名が予想しづらく、何度か試して初めて実際の動作が分かる
    • JavaScriptでは word. の後に .l だけ入力しても、エディタが length などのメソッドを提示するため、発見しやすさが高い
    • map のような高階関数でも、実際の戻り値とデータ型が即座に明確になる

複雑なロジックほど構造的に書く利点

  • 複雑度の高いロジック(filterlambda が入れ子になった長いPythonコード)では
    • コードの始まりと終わりを繰り返し確認する必要があり、条件式や括弧の対応などで可読性の低下と理解の難しさが生じる
  • 同じロジックのJavaScript版では、コードを上から下へ、左から右へ順番に読んで理解できる

核心原則

コードは入力する瞬間ごとに有効でなければならない

  • text 単独の入力でもプログラムは有効な状態を保つ
  • text.split(" ") まで書いても、その後 .map(word => word.length) まで続けて入力するときも、全体として常に中間状態が有効である
  • このようなコーディングパターンは、エディタのリアルタイム支援の可能性を高め、REPL環境では即座に結果を確認することもできる

結論

  • APIと言語設計は、コードを左から右へ自然に入力しながら、中間段階ごとに有効なプログラムを作れるよう支援すべきである
  • 良いAPI設計こそが、このようなコーディング体験の改善の鍵である

1件のコメント

 
GN⁺ 2025-08-20
Hacker Newsのコメント
  • SQLの欠点の1つは、クエリがFROMではなくSELECTで始まることだと思う。どのエンティティ(テーブル)を扱っているのかをすぐ把握しづらく、スマートエディタがクエリ作成をより効率的に支援するうえでも邪魔になる。FROM -> SELECT -> WHEREの順のほうが自然だ。特にSELECT節でカラム名を決めてWHEREでそれを参照するのだからなおさらそう思う。実際、SELECT * FROM tableの代わりにFROM tableだけ書けばSELECT節は省略可能でもよいはずだ。こんな不満を言うと小言の多い年寄りみたいに聞こえるかもしれないが、単なる個人的な郷愁だ
    • PSQLとPRQLは実際にFROMが先に来るクエリ順を採用している。BigQueryにも最近パイプ/矢印構文が追加されたし、DuckDBのコミュニティ拡張もあるのでおすすめだ。DuckDB - PSQL, DuckDB - PRQL
    • SQLがこう書かれるのは、関係代数の基礎で射影(Projection)を常に先に書くからだ。だから標準ではWHEREでカラム別名を使えない。selection(WHERE)はprojection(SELECT)より前に行われるからだ。ちなみにMySQL 8にはTABLE <table>という構文もあるので参考になる
    • 実際にはほとんどのSQLエンジンの内部処理順はFROM -> WHERE -> SELECTだ。だからSELECTで定義したカラム別名はGROUP BYHAVINGORDER BYでは使えるが、WHEREでは使えない
    • C#にはSQLへコンパイルされるDSL(LINQ-to-SQL)もあり、これもFROMが先に来る構造だ。IDEで他の節を書いているときもオートコンプリートのおかげですぐフィールド候補を受け取れるので、この構造はよいと思う
    • Azureのデータ分析クエリ言語であるKustoも、パイプを使う似た形式だ。Kustoクエリの概要 .NETのLINQスタイルも同様だ。正直、SQLにもFROMで始まる変種がもっと積極的に導入されるべきだし、それは難しいことでもないと思う。使い勝手を改善する試みが足りない
  • Pythonがなぜこんなに愛されているのか理解しがたい。2人以上で作業すると、この言語はどこまでも苦痛になる。筆者が指摘した点は氷山の一角にすぎない
    • 人々がLisp系言語に殺到しない理由と似ていると思う。数学的な厳密さがそのまま可読性を意味するわけではない。Pythonのlist/dict/set comprehensionsは、型が決まるforループと同じだ。みんなPythonの型のゆるさを心配するのに、戻り値の型を明確に定める唯一の構文(リスト内包表記)が槍玉に上がるのは妙だ。Rustを含むたいていの他言語でも"from iter as var"という順番ではない。それに各言語の関数呼び出し構文を比べるのも面白い(Pythonにもfunctools.mapがあるように)
    • 何かを理解できないからといって、それが美徳になるわけではないと思う。Pythonが愛されるのには確かに何か理由がある。もちろん欠点もはっきりしているが、それだけでは意味がない。長所と短所を全体として比較すべきで、他の言語ともそう比較すべきだ
    • 私もPythonは好きだ(ただし小規模チーム、短く寿命の短いプログラムという前提で)。静的型がないので実装は速いが、強い型システムのおかげで完全には壊れない。この点がデータサイエンスで人気な理由だと思う。探索するときにとても便利だ。一方で、長期的に複数チームで保守したり大規模なプログラムには明らかな欠点がある。結局、万能言語はなく、少なくとも「試してすばやく進める言語(Soft)」と「長期間の管理に向いた言語(Hard)」の2つは必要だ
    • 私も以前はこの意見に完全に同意していたが、型注釈と型検査のおかげで、他人が書いたPythonコードとの協業がずっとやりやすくなった。いまだに大規模プロジェクト向きではないと思うが、型が付いたことでPythonは私のお気に入りのスクリプト言語になった
    • 私も共有コードベースではリスト内包表記のようなものを避け、とても単純なPythonスタイルを目指している。「やり方は1つだけ」とされる言語だが、実際にはやり方が多すぎる。リスト内包表記は個人的には楽しく満足感があるが、みんなが1本の道を行くべきなら、この構文はないほうがよいと思う
  • 「プログラムはタイプしているその瞬間に有効であるべきだ」という主張には共感するが、現実にはコードを常に左から右へ、1行ずつ順番に書くわけではない。途中で別の部分を先に書いたり、変数宣言を後回しにすることも多い。たとえば変数を使っておいて、かなり後になってから宣言することもある
    • コードは1回書かれて何十回、何百回も読まれるのだから、順番に読めるコードは、ジャンプを要するコードよりずっと読みやすいと思う
    • 実はこの議論は記事の核心から少し外れてはいるが、興味深い視点だ
    • 完全に同意する。新しいファイルを作るときだけコードを最初から順番に書く。フィールドを追加するときは、わざわざクラス定義から行かず、そのフィールドを使うコードから先に作る。条件文を改善するときも、一時的に無効な(エラーになる)状態になることがよくある
    • この意見にも同意するが、これに関連する重要な原則は「まだコーディングが終わっていないのだからコンパイル自体をさせない」という構造はやりすぎだということだ。エラーは非ブロッキングであるべきなのに、一部の言語は未完成のコードを丸ごと拒否してしまう(例: 未使用変数、欠けたreturnなど)
    • IDEが自分の実際のコード記述順をあまり理解していないと感じることがあり、少し不便だ
  • 一部のIDEにはコードテンプレート機能があり、略語を入力するとコード構造へ展開され、タブで各プレースホルダーを埋めていける。このとき、タブ移動順は必ずしも左から右である必要はなく、たとえば{3} for {2} in {1}のような順でもよい。こうしたツールは「読みやすい構文」と「打ちやすい構文」のあいだの妥協点を提供してくれる。私はツーリングを活用してでも読みやすい構文を優先する側に一票を投じたい。必ずしも"for-in"構造にこだわる必要はないと思う
  • 最近のHacker Newsでは、Pythonにはpipe演算子が欠けているというのが一種の共通認識らしい。私はMathematicaからRに移ったとき、パイプの価値をすぐ理解した。データサイエンスで段階的なデータ変換コードを書くとき、本当に直感的で読みやすい。Pythonはさまざまな分野で使われているが、データ分析以外の文脈でもパイプに利点があるのか気になる。なぜPythonがパイプを導入しなかったのか理解したい
    • パイプ演算子からさらに一歩進めて、reverse assignmentも試してみる価値があると思う。let foo = ...のように結果を変数に代入する代わりに、... =: fooのような形式も使ってみたい
    • 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)のようにパイプを使えば、ずっと簡単で見やすい
    • Pythonのpandasでpipe構文を使うと次のようになり
      result = (df
       .pipe(fun1, arg1=1)
       .pipe(fun2, arg2=2)
      )
      
      Rでは
      result <- df |>
       fun1(., arg1=1) |>
       fun2(., arg2=2)
      
      どちらも十分読みやすいが、Rはデータフレームの外でもパイプがよりうまく機能するのが利点だ
  • この論争は、FP(関数型)vs OOP(オブジェクト指向)言語論争や、vimとemacsの論争のように、ほとんど宗教戦争に近い。vimは演算子が先で、emacsは選択順が先だ。英語のように「英語らしく読める」言語は、たいてい動詞が先に来る構造だ(Lisp/Schemeがそうだ)。一方、ドイツ語やタミル語のように動詞が最後に来る言語は、OOPスタイル(名詞先行)と相性がよい。たとえばタミル語では"water drink"の順で、英語は"drink water"だ。だからvimをより快適に感じる人がいてもおかしくない。どちらのスタイルが優れているというより、道具や人の性向に合わせて作られている面もあるし、今では言語モデルでたいていのことは何とかなると思う
    • 「英語のように読めるよう設計するなら動詞先行か」については、命令型言語ならそうだが、宣言型言語を英語のように読ませるなら主語が先だ
    • 「ドイツ語は動詞が常に後ろに来るのか」については、実際には単純な文では動詞は2番目に来る("I drink water" → "Ich trinke Wasser")ので、完全に文末というわけではない
    • vimでは演算子が先だという話については、実はKakouneは逆に動作し、この方式のほうがはるかに論理的だと思う。Kakouneについての説明
  • 一方で、Pythonのfrom some_library import child_moduleという構文は非常に直感的だ。JSではimport { asYetUnknownModule } from SomeLibraryのような構造で、ずっと直感的でないと感じる
    • JSではnamespace importで次のように書けば
      import * as someLibrary from "some-library"
      someLibrary.someFunction()
      
      実際にIDEのオートコンプリートがうまく働くのでよいと思う。MDNのnamespace import解説
    • fromキーワードにこだわる理由がよくわからない。ただ
      import SomeLibrary {
        asYetUnknownModule
      }
      
      こうすればいいと思う
  • ReScriptはまさにこの理由で、APIをdata-lastからdata-firstへ変更した。優れた型推論のおかげで、ほとんど常に正確で型に合ったオートコンプリートが得られ、開発体験がとてもよい。もちろん、参照なしで関数を宣言すると(型がわからないので)依然として問題はあるが、型を追加するか先に呼び出せば解決する。関連するブログ記事もおすすめだ。Data-firstとdata-lastの比較
  • 私はこういう見方を以前から主張してきたが、実際にRubyのほうがずっととっつきやすく感じられた理由ともつながっている。特に私はPythonもRubyも本番レベルで深く使ったことはないが、Pythonがなぜあれほど広くインストールされ使われ始めたのかはよくわからない。Rubyにも欠点がないわけではないが、スクリプト作成でPythonが経験したような複雑な変化に直面する人はそれほどいないように思う。少なくともRubyにはこの10年、大きなバージョン衝突事件はなかった
  • 全体として記事で指摘された点には全面的に同意する。文脈が先に来て左から右へ読める構造のほうが、LLMやオートコンプリートにも合っていると思う。ただし、例のコードは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を使ったほうが、メモリ上に新しいリストを作る必要がなく、行全体を一度に扱うのにもずっと向いている。たとえば
    sum(1 for line in diffs
      if ((np.abs(line) >= 1) & (np.abs(line) <= 3)).all()
        and ((line > 0).all() or (line < 0).all()))
    
    こちらのほうがずっと「左から右」をよく体現している
    • numpy版もまだ多少は暗号っぽい(line > 0は問題ないが、ブロードキャスト規則は複雑になりうる)ものの、筆者が挙げたJavaScriptの例や、C#、Java、Scalaのような型の厳密な言語のコレクションAPIのほうがすっきりしている。私の好みはKotlinで、次のように書けるのが好きだ
      diffs.countIf { line -> 
        line.all { abs(it) in 1..3 } and ( 
          line.all { it > 0} or
          line.all { it < 0}
        )
      }