「カリー化に反対する事例」

2026/03/22 22:03

「カリー化に反対する事例」

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

概要:
本稿は、関数型言語におけるマルチパラメータ関数の記述スタイル—命令的/パラメーターリスト、カリー化、およびタプル化—を対比しています。カリー化がエレガントで部分適用(

add' = add 1
Int → Int → Int
)を自然にサポートする一方で、余分なクロージャ生成が発生し、非対称の型シグネチャ(
P₁ → P₂ → … → R
)が一般的な高階合成を妨げる可能性があり、しばしば
uncurry
が必要になると説明しています。
タプル化関数(
f(p₁,p₂,p₃) = …
、型
(P₁,P₂,P₃) → R
)は中間クロージャを避け、合成を簡素化します。部分適用はタプルスタイルでも可能であり、穴オペレータや構文糖衣のような手法を使って実現できます。

本稿は、Haskell がすべてのスタイルをサポートし、Rust では構文トリックでエミュレートできること、また Coq/Agda のような依存型システムが自然に型依存性(

plus1 : ∀ n, fin n → fin (n+1)
)を表現するためカリー化タイプを好むと指摘しています。
著者は読者に利点と欠点を比較検討し、タプル形式や代替部分適用構文の探索が将来の言語設計やライブラリ実装に影響を与える可能性があることを示唆しています。 ライブラリや DSL を構築する実務者は性能と合成性の観点からタプルを選択するかもしれませんし、証明支援ツール開発者は表現力豊かな型関係のためにカリー化を好むでしょう。

本文

カリー化関数は、命令型言語から関数型言語へ移行する際に最初に出会う新しい概念の一つです。
純粋関数型言語では、n 個の引数を取る関数を「段階的に」定義します。すなわち、関数を第 1 引数に適用すると、残りの引数(2…n)を取る関数が返ります。その関数に第 2 引数を渡すと、さらに残り(3…n)の引数を取る関数が返ります。こうして全ての引数が与えられた段階で結果が返されます。

例えば、3 つの整数を足し合わせる

add
を定義するとします:

add x y z = x + y + z
-- これは次のように書いたものと同じです:
add = \x -> (\y -> (\z -> x + y + z))
-- add の型は以下の通りです:
add :: Int -> (Int -> (Int -> Int))

-- 左から右へ適用できます:
((add 1) 2) 3   -- 6 を返します

->
は右結合にし、関数呼び出しは左結合にすることで
Int -> Int -> Int -> Int
と書けます。さらに、関数適用を左結合にしておけば、
add 1 2 3
のように括弧を最小限で済ませることができます。


このスタイルの欠点について

カリー化したスタイルは洗練されている一方で、何かが失われています。
多引数関数を定義する際に、プログラミング言語は主に以下の三つの「スタイル」を提供します。

  1. 命令型スタイル(パラメータリスト) – 関数に組み込みの機能として存在。

    fn f(p1: P1, p2: P2, p3: P3) -> R { … } // 定義
    f(a1, a2, a3)                             // 呼び出し
    f : fn(P1, P2, P3) -> R                   // 型
    
  2. カリー化スタイル – Haskell のような純粋関数型言語で採用される。

    f p1 p2 p3 = …         -- 定義
    f a1 a2 a3             -- 呼び出し
    f :: P1 -> P2 -> P3 -> R // 型
    
  3. タプルスタイル – 1 つの引数としてタプルを渡す。

    f(p1, p2, p3) = …    -- 定義
    f(a1, a2, a3)          -- 呼び出し
    f :: (P1, P2, P3) -> R // 型
    

一部の命令型言語でもこれらのスタイルを模倣できますが、少々扱いにくくなります。例えば JavaScript でカリー化スタイルを書けば:

const f = p1 => p2 => p3 => …;
f(a1)(a2)(a3);

Rust でタプルスタイルを書くと:

fn f((p1, p2, p3): (P1, P2, P3)) -> R { … }
f((a1, a2, a3));
f : fn((P1, P2, P3)) -> R;

理論上は等価である

(P1, P2) -> R
P1 -> P2 -> R
は同型(isomorphic)です。
つまり、両者の関数には 1 対 1 の対応が存在します。では、なぜカリー化スタイルを選ぶのでしょうか?


部分適用

インターネットで「カリー化関数を使う理由」を尋ねると、主に「部分適用が簡単になる」ことが答えとして挙げられます。
部分適用とは、多引数関数の一部のパラメータを固定し、残りだけを取る新しい関数を返す操作です。

3 引数の

add
を例にすると:

add' = add 1
-- ここで add' = \y -> (\z -> 1 + y + z)
add' :: Int -> Int -> Int

add'' = add' 2
-- ここで add'' = \z -> 1 + 2 + z
add'' :: Int

add'' 3   -- 6 を返します

map
fold
のような高階関数と組み合わせるとさらに便利です:

length  = foldr (+) 0 . map (const 1)
length2d = foldr (+) 0 . map length

length2d [[1,4,2], [], [7,13]]   -- 5 を返します

カリー化が「特殊な」性質を持つと誤解されることも

実際には、パラメータリストスタイルやタプルスタイルでも部分適用は可能です。

add
をタプルスタイルで再定義すると:

add (x, y, z) = x + y + z
add :: (Int, Int, Int) -> Int

add'  = let x = 1 in \(y, z) -> add (x, y, z)
add'' = let y = 2 in \z -> add' (y, z)

add'' 3   -- 6 を返します

さらに、

$
(ホール演算子)を使って構文的に整えれば:

add'  = add (1, $, $)
add'' = add' (2, $)

add'' 3   -- 6 を返します

より複雑な例は次のようになります:

length  = foldr ((+), 0, $) . map (const 1, $)
length2d = foldr ((+), 0, $) . map (length, $)

length2d [[1,4,2], [], [7,13]]   -- 5 を返します

この形は「データの流れ」を明示的に示すため、読みやすくなります。


第 2 引数以降の部分適用

タプルスタイルでは第 1 引数だけでなく任意の引数を固定できます。カリー化スタイルではデフォルトでは第 1 引数しか固定できません。

例えば、

map
の第 2 引数(関数)を固定したい場合:

allColors = ["red", "green", "blue"]
forEachColor = map ($, allColors)

ネストされた関数呼び出しが多くなると制限がありますが、その際は明示的にラムダ式を書くことで対処できます。


カリー化スタイルは「部分適用を強力にするわけではない」

少し構文糖衣を付ければ、カリー化スタイルの利点を再現できます。
しかし、関数型プログラマがカリー化関数に対して持つ「雰囲気」や「直感的な美学」が根底にあると考えられます。


「カリー化は良い」という主張への反論

1. パフォーマンス

add 2 3
のように呼び出すと、最初のステップで新しい関数
\y -> add 2 y
が生成され、その後
3
に適用されます。つまり多引数関数を呼び出すたびに中間関数が作られます。ただし、十分な最適化器があればこのオーバーヘッドは除去できますので、重大な問題ではありません。

2. 型の形

カリー化型は「入力→出力」の形を持ちます。

P1 -> P2 -> P3 -> R
の場合、入力
In = P1
、出力
Out = P2 -> P3 -> R
となります。一方でタプル型
(P1, P2, P3) -> R
なら
In = (P1, P2, P3)
Out = R
とより直感的です。
この非対称性は、複数の出力を返す関数がタプルで、入力は段階化しているという矛盾を生み、関数合成を難しくします。

sayHi name age = "Hi I'm " ++ name ++ " and I'm " ++ show age
people = [("Alice", 70), ("Bob", 30), ("Charlotte", 40)]

-- エラー: sayHi は String -> Int -> String、person は (String, Int)
conversation = intercalate "\n" (map sayHi people)

-- これを動かすには uncurry sayHi を渡す必要があります

map
が期待する関数は
In -> Out
の形であるため、カリー化型は直接合成できません。2 引数の場合はそれほど問題ではありませんが、引数が増えると複雑さは増します。

3. 証明助手の例

Coq(Rocq)などの依存型証明助手で「状態を返す関数」について述べる命題を考えます:

Definition P {In Out : Type} (f : In -> State Out) :=
  … f に関する命題 …

推奨されるカリー化スタイルで定義すると、

f : P1 -> … -> Pn -> State R
となり、
In -> State Out
と全く一致しません。結果として
P(f)
は型エラーになり、毎回手動で関数を非カリー化(uncurry)する必要があります。


結論

多くの Haskell コードがカリー化スタイルで書かれているため、それに乗っ取ることは簡単です。しかし、新しい関数型言語や標準ライブラリを設計する際には、タプルスタイルと部分適用の代替構文を試してみる価値があります。

この記事を真剣に受け止めすぎないでください。実際、多くの高階関数(

map
,
fix
など)ではカリー化定義が非常に自然です。ただ、ほとんどの場合でタプルスタイルの方が合理的だと感じます。

他にもカリー化関数の利点・欠点について知っている方はぜひ教えてください。私自身は証明助手で頻繁に「uncurry」を使わざるを得なかった経験があります。


補足:依存型関数

稀ではありますが、カリー化スタイルが優れているケースも存在します。Gallina(Coq)や Agda のような依存型言語では、戻り値の型が入力に依存することがあります。

Definition fin (n : nat) := { x : nat & x < n }.
Definition plus1 (n : nat) (i : fin n) : fin (n + 1) :=
  (i.1 + 1 ; (* i.1 + 1 < n + 1 の証明 *)).

ここで

plus1
forall n : nat, fin n -> fin (n + 1)
と型付けされ、これはカリー化された依存関数です。
タプルスタイルにすると:

(* 仮想的な構文 *)
Definition plus1 (n : nat ; i : fin n) : fin (n + 1) :=
  (i.1 + 1 ; (* 証明 *)).

この場合、第二引数の型が最初の値

n
に依存する「依存タプル」を扱う必要があります。
これは実装上は可能ですが、型推論をさらに複雑にし、読みづらくなります。


参考文献

  1. Certified Programming with Dependent Types(Adam Chlipala 著)
    URL: http://adam.chlipala.net/cpdt/html/toc.html (MIT Press 版もあり)

© emilia‑h 2025‑2026 CC BY‑SA 4.0 で公開。

同じ日のほかのニュース

一覧に戻る →

2026/03/23 3:23

**PC Gamer 推奨RSSリーダー(37 MBの記事でダウンロードが止まらない場合)** - **Feedly** - クラウドベースでデバイス間同期が可能。 - カテゴリー分けやタグ付け機能が充実しています。 - **Inoreader** - 高度なフィルタリングと検索機能を備えています。 - オフライン閲覧モードもサポートします。 - **The Old Reader** - シンプルで軽量、Googleアカウント連携が可能です。 - 共有リストやコメント機能があります。 - **NewsBlur** - AIによるトピック分類と学習機能を提供。 - モバイルアプリも充実しています。 - **Reeder (macOS/iOS)** - Appleデザインに合わせた直感的なUIです。 - 多数のリーダーサービスと連携可能です。 **注意点** - 大容量の記事をダウンロードし続ける場合は、**「オフライン保存」機能**をご利用ください。 - **キャッシュクリア**や**ブラウザ拡張機能無効化**で問題が解決することもあります。 - それでも解決しない場合は、PC Gamerのサポートへ問い合わせるか、別のリーダーを試してください。

## Japanese Translation: PC Gamerの記事は、読者に通知ポップアップ、背景を暗くするニュースレターオーバーレイ、そして少なくとも5つの閉じにくいバナー広告でページを襲撃していることを示しています。ウェルカムマットを回避した後でも、その広告は記事のタイトルとサブタイトルの横に残ります。初期ページロードは37 MBです;5分以内にサイトはさらに約0.5ギガバイトの広告素材をダウンロードします。NetNewsWire、Unread、Current、Reeder など多くの RSS リーダーはこれらの侵襲的要素をフィルタリングでき、よりクリーンな閲覧体験を提供します。これはユーザーが PC Gamer サイトの煩わしさを避けるために広告なしの RSS フィードに切り替える可能性があることを示唆しており、出版社は読者の関与を維持するために過度な広告戦術を減らす圧力を受けるかもしれません。

2026/03/23 4:02

「最適化のゴールドスタンダード:ローラーコースター・タイクーンの内部を探る」

## Japanese Translation: クリス・ソーヤーの *RollerCoaster Tycoon*(1999)は、ほぼすべてのコードをアセンブリで書き、細部にわたる低レベル最適化を施したことで、滑らかなゲームプレイのベンチマークを確立しました。金額は最大想定範囲にちょうど合ったデータ型(ショップ価格は1バイト、総公園価値は4バイト)で保存されており、後にオープンソース再実装 OpenRCT2 ではこれらを統一的な8バイト変数へ移行し、現代のCPUアーキテクチャに合わせました。乗算・除算の代わりにビットシフト(`<<`/`>>`)が使用されており、コンパイラが自動で行うはずだった処理を手動で実装しています。 ゲームデザインの決定は性能制約と密接に結びついています。ソーヤーはデザイナー兼プログラマーとして、CPUフレンドリーな計算を優先する設計選択が可能でした。ゲストの移動はアトラクションへ向かう完全な経路探索ではなくランダムウォークに依存しており、多数のエージェントによる高価な計算を大幅に削減しました。パスファインディングは特定のシナリオ(例:乗物修理のメカニック、出口を探すゲスト)でのみ呼び出され、深さ制限が設けられています—デフォルトでは5つのジャンクション、条件に応じて7または8に増加し、フレームスパイクを回避します。混雑した道では同一タイルに複数のゲストが存在でき、衝突回避は完全に省かれ、近接による幸福度計算のみが影響を受けます。 OpenRCT2 はこの元のロジックをリバースエンジニアリングし、現代CPU向けに変数サイズを標準化し、パスファインダーの制限を拡張することで更新しました。これにより、レガシートリックが新しいハードウェアに適応できることが示されました。将来のアップデートでは、衝突チェックや厳密なデータサイズといった古い制約を緩和しつつ、今日のマシンで性能を損なわずにコア体験を保持することが可能です。 これらの洞察は、デザイナーとプログラマーの緊密な協働と意図的な低レベル最適化が、小規模チームでも高性能ゲームを構築できることを示しており、大手スタジオも採用すべきアプローチです。 ## Text to translate (including missing points):** Chris Sawyer’s *RollerCoaster Tycoon* (1999) set a benchmark for smooth gameplay by writing almost all of its code in Assembly and applying meticulous low‑level optimizations. Money values were stored in data types sized exactly to their maximum expected range (1‑byte for shop prices, 4‑bytes for total park value), and the original engine later shifted these to uniform 8‑byte variables in the open‑source reimplementation OpenRCT2 to match modern CPU architecture. Bit shifting (`<<`/`>>`) was used instead of multiplication/division by powers of two, a manual trick that compilers no longer perform automatically. Game‑design decisions were tightly coupled with performance constraints: Sawyer served as both designer and programmer, allowing design choices to favor CPU‑friendly calculations. Guest movement relied on random walking rather than full pathfinding toward attractions, drastically reducing expensive calculations for thousands of agents. Pathfinding was invoked only in specific scenarios (e.g., mechanics repairing rides, guests seeking exits) and had a depth limit—default 5 junctions, increased to 7 or 8 under certain conditions—to avoid frame‑spikes. Overcrowded paths allowed multiple guests on the same tile; collision avoidance was omitted entirely, with only happiness calculations affected by proximity. OpenRCT2 reverse‑engineered this original logic and modernized it—standardizing variable sizes for current CPUs and extending pathfinder limits—showing how legacy tricks can be adapted to new hardware. Future updates could relax some of these old constraints (such as collision checks or strict data sizing) without harming performance on today’s machines while still preserving the core experience. These insights underscore that close collaboration between designers and programmers, coupled with deliberate low‑level optimization, enables small teams to build high‑performance games—an approach larger studios might emulate.

2026/03/23 0:16

**バージョン管理の未来** バージョン管理は、従来型のリポジトリやブランチモデルを超えて進化しています。新たに浮上している動向としては、AI 主導の変更分析、分散したチーム間でのリアルタイム協働、および継続的デリバリー・パイプラインとの緊密な統合が挙げられます。コードベースがより大規模かつ複雑化するにつれて、これらの革新はワークフローを合理化し、マージコンフリクトを減少させ、全体的なソフトウェア品質を向上させることを約束しています。

## Japanese Translation: **Manyana** は、Conflict‑Free Replicated Data Types(CRDTs)がバージョン管理にどのように利用できるかを示すデモプロジェクトです。ユーザー体験を向上させます。 ファイルは *weave* として表現されます——1 つのデータ構造が、追加または削除された各行とメタデータを記録し、行順序を永続化し、同時挿入に対してマージ全体で一貫した順序を提供します。 CRDTs は順序非依存ですので、マージが失敗することはありません。衝突はファイルの同じ部分を編集したときだけ発生し、不透明なマージブロブではなく明確な競合マーカーが生成されます。 システムはまた、リベースが履歴を破壊せずに行えることも示しています:コミットは新しいベース上で再実行され、「プライマリー・アニサスター」注釈によって完全な祖先関係が保持されます。 チェリーピッキングとローカル Undo はまだ実装されていませんが、470 行の Python デモ(パブリックドメイン)は、CRDTs がバージョン管理における難しい UX 問題を解決し、現在のツールよりも明確な競合表示を提供できることを示しています。