5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【luarrow】Luaでパイプ演算子やHaskell的関数合成演算子を使う【Lua】

Last updated at Posted at 2025-11-30

preview-1.png

Luaアドベントカレンダー2025 1日目の記事です。
よろしくお願いします!

🚀 はじめに

Luaでプログラミングをしていると、このようなコードに遭遇することはありませんか?

print(foo(bar(baz(x), hoge)))

これは古典的な言語ではしばしば出会う、読みにくいコードです。
ただこのレベルでは、問題ないことが多いでしょう。

しかし高階関数を使うと、事態は深刻化します。
関数型プログラミングがようやく注目されていることもあり、現代の言語では高階関数は多用され、古典的な関数呼び出しでは、可読性が大きく下がります。

そして積もりが重なり、メンテナンス性は著しく低下します。

local result = reduce(filter(map(list, function(x)
  return x * 2
end), function(x)
  return x > 10
end), function(acc, x)
  return acc + x
end, 0)

やがて機能追加やバグ修正という本質的な作業よりも、そのためのコードリファクタリング・あるいはそのコードをよけて作業するための副次的な作業の方が、コストが高くなるのです。
末期的には、生産性は0に近づきます。

(「Clean Architecture 達人に学ぶソフトウェアの構造と設計 Robert C. Martin(著), 角征典, 髙木正弘(訳)
36ページ「崩壊のサイン」の章から引用。1


そこで私はLuaライブラリ、luarrowを開発しました。

luarrowはLuaにパイプライン演算子(|>)やHaskell風関数合成(., $)を、Luaなりに導入するライブラリです。
演算子オーバーロードを活用し、ネストした関数呼び出しを代表とした、Luaのコードを美しく、読みやすい関数適用フローの方法を提供します。

preview-2.png

✨ 特徴

全体像とその詳細のREADME.mdやドキュメントに記述していますので、筆者が特に強調したい点を説明します。

全体と詳細を読みたい方は、以下を参照してください。

🔀 真のパイプライン演算子

しばしば、パイプライン演算子がほしい場合に演算子を実装できず、パイプライン関数で実現することがあります。

local pipe = require('foo-package').pipe -- 何かしらのパッケージ

local result = pipe(
  list,
  map(function(x) return x * 2 end),
  filter(function(x) return x > 10 end),
  reduce(function(acc, x) return acc + x end, 0)
)

2

しかしluarrowはこれをよしとせず、演算子オーバーロードを用いるというアイデアを採用しました。

local arrow = require('luarrow').arrow

local result = list
  % arrow(map(function(x) return x * 2 end))
  ^ arrow(filter(function(x) return x > 10 end))
  ^ arrow(reduce(function(acc, x) return acc + x end, 0))

3

これは以下のPHPコードと同等です。

$result = 42
  |> (fn($x) => $x - 2)
  |> (fn($x) => $x * 10)
  |> (fn($x) => $x + 1);

pipe関数でもよしとするユーザーはそれでいいですが、
luarrowはそうでないユーザーに対し、構文的美学を提供します。

luarrowは「関数型プログラミングへのロマン的追及」を大切にしています。

🔗 Haskellスタイルの関数合成演算子

また、Haskellスタイルの関数合成演算子および関数適用演算子も提供しています。

local fun = require('luarrow').fun

local function plus_one(x) return x + 1 end
local function times_ten(x) return x * 10 end
local function minus_two(x) return x - 2 end

-- ポイントフリーによる関数定義
local k = fun(plus_one) * fun(times_ten) * fun(h)

-- 関数適用演算子
-- luarrowのfunにおいて、Pure Luaの k(42) と同等(arrowの%との混同に注意)
local result = k % 42
print(result)  -- 401

これはHaskellの

k = plus_one . times_ten . minus_two
result = k $ 42
main = print result  -- 401

と同様です。
(ただし、Haskellにおいてはk 42と書いても問題ありませんが、luarrowでは%が必要です。)

これは数学的に伝統のあるf ○ g ○ hのような関数合成にも倣っています。

⚡ ほぼオーバーヘッドなし(LuaJIT環境)

LuaJITを視野に入れるため、luarrowはLua 5.1準拠で設計されています。4

LuaJITはゲームエンジンなど、多くのLua処理系で採用されています。
これらの処理系においてluarrowは、前述のように読みにくい通常の関数呼び出しと、同様の速度を実現します。

実際、LuaJITで実地計測をしたところ、0.0005秒以下という結果が出ています。

しかしながらこれを裏返すと、非常にパフォーマンスが重要な場面では(そのLua処理系にJITがない限り)luarrowは適していないかもしれない、ということでもあります。
これは非常に苦しい点です。
ただし多くの用途、例えば以下の用途など、カジュアルなものに対しては十分に高速です。

  • 設定ファイルの処理
  • ゲーム開発のスクリプティング
    • 特にLuaJITを採用しているゲームエンジン(後述)
  • コマンドラインツール
  • データ変換スクリプト

💬 実際にいただいた質問・反論への回答

❓ Q1: 「メソッドチェーンでいいじゃん」

確かにメソッドチェーンも読みやすい記法です。
しかしメソッドチェーンはluarrowの代わりになることはできません。

メソッドチェーンはiteratorに特殊化されています。
一方、luarrowは全ての値に一般化されています。

より簡単に言うと、メソッドチェーンはリスト(配列)のようなLuaのクラスに対しては有効ですが、数値や文字列などの広い型に対しては使えません。

具体的な例を見てみましょう。

メソッドチェーンの制限:

-- メソッドチェーンの例(仮想的なiteratorライブラリ)
local result = iterator(list)
  :map(function(x) return x * 2 end)
  :filter(function(x) return x > 5 end)
  :reduce(function(acc, x) return acc + x end, 0)
  -- ここまではOK

-- しかしreduceの結果(数値)に対してさらに処理を続けたい場合は、メソッドチェーンは使えない。
-- result(数値)にはメソッドがないため

reduce後に数値を返した場合、メソッドチェーンはそこで終了します。
さらに処理を続けるには、別の行で書き直す必要があります。

local sum = iterator(list)
  :map(function(x) return x * 2 end)
  :filter(function(x) return x > 5 end)
  :reduce(function(acc, x) return acc + x end, 0)

-- チェインが切れる...
local average = sum / #list
print(average)

luarrowなら一般化されているので、どんな値でも継続できます:

local arrow = require('luarrow').arrow

local _ = list
  % arrow(map(function(x) return x * 2 end))
  ^ arrow(filter(function(x) return x > 5 end))
  ^ arrow(reduce(function(acc, x) return acc + x end, 0))
  -- ここでreduceは数値を返す
  ^ arrow(function(sum) return sum / #list end)  -- 数値に対してもそのまま続けられる!
  ^ arrow(print)  -- 424

luarrowは値の型に関係なくパイプラインを継続できるため、より柔軟で一般的なのです。

❓ Q2: 「ネストした関数呼び出しの方がいい」

「はじめに」での例を引用します。

print(foo(bar(baz(x), hoge)))

これは目をつむって、よいとします。
しかし以下はどうでしょうか。

local result = reduce(filter(map(list, function(x)
  return x * 2
end), function(x)
  return x > 10
end), function(acc, x)
  return acc + x
end, 0)

あなたは何度目を左右に動かしたでしょうか。

luarrowなら上から下、左から右に読めばいいのです。

local result = list
  % arrow(map(function(x) return x * 2 end))
  ^ arrow(filter(function(x) return x > 10 end))
  ^ arrow(reduce(function(acc, x) return acc + x end, 0))

❓ Q3: 「ドキュメントが明らかに宣伝目的だ」

そうではありません。
ユーザーのluarrowの原理と、メリットの理解のため、自然にそうなっています。

実際、不都合なことを避けるようなことはしていません。
例えば前述したものを再掲しますが、以下ではLuaJIT以外での不都合を公開しています。

❓ Q4: 「%メタメソッドを持つ型で問題が起きるのでは?」

聡い読者の方は気づいたかもしれませんが、luarrowの「パイプライン演算子%, ^」には、しつこくarrow関数が付属しています。
(同様に「Haskellスタイルの関数合成演算子*, %」ではfun関数が付属しています。)

これは以下のPHPのコード例のように…(非常に残念なことに)美しくはありませんが

$result = 42
  |> (fn($x) => $x - 2)
  |> (fn($x) => $x * 10)
  |> (fn($x) => $x + 1);

本クエスチョンへの回答になっています。
つまり、「arrowfun)関数によってmetatableを設定することで、他の%, ^, *を持つ型とは区別されている」ということです。

そこで未だに問題が起こるというのであれば、それは根本的に動的型付けの問題であると考えます。

LuaにはLuaCATSなどのアノテーションがあるので、そこで---@typeなどを駆使することで、ある程度の改善を試みることができるでしょう。

ただし個人的にはまだLuaCATSはまだ未成熟で、漸進的型付け6としては運用できないとも思っています

📚 豊富なドキュメント

luarrowでは、充実したドキュメントを用意しています。

📖 ドキュメント

  • API Reference - 完全なAPI仕様書
    • FunArrowの全メソッド・演算子の詳細
    • 型パラメータの説明
    • 使用例とTips
  • Examples - 実践的な使用例集
    • 基本例から高度なパターンまで
    • パフォーマンスベンチマーク
    • 他のアプローチとの比較
    • LuaCATSとの連携方法

この記事では触れきれなかった詳細な情報や、より高度な使用例について知りたい方は、ぜひドキュメントをご覧ください。

📦 インストール方法

  • luarocks
$ luarocks install luarrow
# 動作確認
$ eval $(luarocks path) && lua -e "local l = require('luarrow') ; print('Installed correctly!')"
  • Gitから直接インストールする
$ git clone https://github.com/aiya000/luarrow.lua
$ cd luarrow.lua
$ make install-to-local

🔗 リンク

🔮 今後の展望

luarrowは現在も開発中です。

📝 リスト操作モジュールの追加

追加される関数:

  • map, filter, flat_map/concat_map, flatten, find
  • foldl (reduce), foldr, foldl1, foldr1
  • sum, product, join
  • length, is_empty, head, tail, last, init
  • reverse, sort, sort_by/sort_with, unique, group_by
  • maximum, minimum

🗺️ その他の計画

今後、以下のような機能も追加していく予定です。

  • 他のユーティリティ関数やコンビネーターの追加
  • さらなるパフォーマンス最適化
  • ドキュメントの充実化

もしluarrowに追加してほしい機能や、改善のアイデアがあれば、気軽にIssueを立ててください。
実装させていただくかもしれません。

フィードバックを歓迎しています!

🎯 まとめ

luarrowは、Luaにパイプライン演算子とHaskell風関数合成を導入するライブラリです。

この記事で紹介した主な特徴をまとめます:

  • パイプライン演算子: x % arrow(f) ^ arrow(g) で左から右へのデータフロー
  • Haskellスタイルの関数合成: fun(f) * fun(g) % x で右から左へのデータフロー
  • 一般化された設計: メソッドチェインと違い、あらゆる型の値に対して使える
  • 高いパフォーマンス: LuaJIT環境では実質的にオーバーヘッドなし
  • 充実したドキュメント: API Reference、Examples、ベンチマークなど

ネストした関数呼び出しや、高階関数の可読性に悩んでいる方は、ぜひluarrowを試してみてください。

皆さんのLuaコードがより美しく、より読みやすくなることを願っています。

  1. 「"図1-4は、開発者にとってこの曲線がどのように見えるかを示したものである。最初はほぼ100%だった生産性が、リリースするたびに低下していることがわかる。4回目のリリースからは、生産性は底を打っている。"」

  2. この例や前述のluarrowのコード例画像で使われているmapfilterreduceは、どこかで実装されていると仮定します。今はluarrowには実装されていないためです。しかし現在、実装が進行中です(これには、他にもリストを引数にした関数、foldr, flatten, find, foldl1, sum, join, sortなどなどが含まれています)。 → feat(luarrow.utils.list): Add comprehensive list manipulation module with curried API

  3. 実際のところ、パイプライン演算子は|>のように見えることが望ましいですが、Luaでは演算子オーバーロードが可能な演算子が限られているため、%^を使っています。

  4. luarrowが<<>>などのわかりやすい演算子でフローを再現しなかったのは、そのためです。Lua 5.1はこれらのビット演算子をサポートしていません。

  5. LuaJITオプションでビルドされたNeovimを指します。HomebrewやLinuxbrewで入るような通常のNeovimはLuaJITを使っているので、Neovimリポジトリで自前ビルドをしているような人以外は、恩恵があります。

  6. 漸進的型付けとは、TypeScriptを代表とする、静的型付けが可能でありつつも、TypeScriptのanyなどのように動的型付けにスイッチすることができる、型付けのことです。Gradual Typingとも。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?