皆さんは、純粋関数がお好きでしょうか?
そして、関数を合成した記述を目にしたことはありますでしょうか?
本記事では入門レベルの関数合成とカリー化の考え方ついて取り扱います。
関数合成とカリー化は、コードの再利用性と可読性を向上させる強力な手法です。適切に使用すれば、保守しやすく拡張可能なコードを書くことができます。
前半部分は自分の理解を深めるため python で記述します。
後半部分では、Haskell を使って補足の説明をします。
⚠️※注意
一般的な Python を含むその他言語でのプロダクションコードで関数合成の仕組みを使うことは、必ずしも最適とは言えません。むしろ少数派だと思います。
大規模コードベースやチームでの開発では、可読性とデバッグ性を優先して「その場所に合うコード」に落とし込むべきであることは間違いないです。
✅ 題材
本記事は以下 2 つの関数について考えます。
from functools import partial, reduce
def curry(func):
def curried(*args, **kwargs):
if len(args) + len(kwargs) >= func.__code__.co_argcount:
return func(*args, **kwargs)
return partial(func, *args, **kwargs)
return curried
def compose(*functions):
def inner(arg):
return reduce(lambda result, func: func(result), functions, arg)
return inner
-
curry
デコレータ
- 任意の関数を「カリー化」し、引数が不足している場合には部分適用(
functools.partial
)を返す
- 引数を一つずつ順に渡しながら最終的に元の関数を呼び出せるようにする
-
compose
関数合成ユーティリティ
- 複数の単項関数(1 つの引数を取り 1 つの値を返す関数)を左から順につなぎ、
最初の入力を受けて一連の関数を順番に適用し、その最終結果を返す新しい関数を生成する
── ここでは、小さな処理を分割して定義し、必要に応じて部分適用や順序の組み替えを効率的に行うことを目指します。
同じような考え方は以下のライブラリで実装されています。
次のセクションで具体例を用いて説明をします。
小道具
数字演算する実装を例にとります。
@curry
def multiply(x, y):
return x * y
@curry
def add(x, y):
return x + y
@curry
def power(base, exp):
return base ** exp
@curry
def divide(x, y):
return x / y
次のセクションで curry
と compose()
がどのように機能するかを見ていきます。
最小の実装
そして、この関数合成の一番初めの例が以下です。
def create_math_pipeline():
"""演算パイプライン作成"""
double = multiply(2)
add_ten = add(10)
square = power(2)
half = divide(2)
return compose(
math.sqrt, # 平方根
add_ten, # +10
square, # ^2
double, # *2
half # /2
)
math_pipeline = create_math_pipeline()
print(f"演算パイプライン(100): {math_pipeline(100)}")
# 演算パイプライン(100): 9.5367431640625e-07
create_math_pipeline の処理順は次のようになります。
- 定義
math_pipeline(100)
half(2, 100)
→ divide(2, 100)
= y
double(2, y)
→ multiply(2, y)
= y
square(2, y)
→ power(2, y)
= y
add_ten(10, y)
→ add(10, y)
= y
math.sqrt(y)
→ 計算結果を返す
create_math_pipeline()
は固定値で決まったパラメータを使用し、決められた順序の四則演算をしました。
curry
が引数を伝達し、compose
が関数を伝番していることが分かったと思います。
ここで、もう少し柔軟な計算をやりたいと感じることだと思います。
柔軟に変換した例
「任意パラメータ・任意順で演算する」ことを目指します。
class PipelineFactory:
"""
動的な順序・パラメータを設定できる数値パイプラインを構築するファクトリ
"""
def __init__(self):
# デフォルトのパラメータ
self.multiply_factor = 2
self.add_value = 10
self.power_exp = 2
self.divide_value = 2
# デフォルトの実行順序
self.step_order = ["sqrt", "add", "power", "multiply", "divide"]
def set_multiply_factor(self, value):
self.multiply_factor = value
return self
def set_add_value(self, value):
self.add_value = value
return self
def set_power_exp(self, value):
self.power_exp = value
return self
def set_divide_value(self, value):
self.divide_value = value
return self
def set_order(self, order_list):
"""
実行順序をリストで設定
例: ["add", "multiply", "sqrt"]
"""
allowed = {"sqrt", "add", "power", "multiply", "divide"}
for name in order_list:
if name not in allowed:
raise ValueError(f"Unknown step: {name}")
self.step_order = order_list
return self
def build(self):
"""
現在の設定に基づいてパイプライン関数を構築し、
引数 1 つを受け取る関数を返す
"""
# 名前 → 実際の処理関数(パラメータ適用済み)
makers = {
"multiply": multiply(self.multiply_factor),
"add": add(self.add_value),
"power": power(self.power_exp),
"divide": lambda x: divide(x, self.divide_value),
"sqrt": math.sqrt,
}
# 指定順に関数を並べて compose へ渡す
funcs = [makers[name] for name in self.step_order]
return compose(*funcs)
ひとまずこれで、「任意パラメータ・任意順で演算する」という要件を満たしています。
次のように使います。
factory = PipelineFactory()
# デフォルトの順序で
pipeline1 = factory.build()
print(pipeline1(4)) # → ((((√4) +10)^2) *2) /2
# 順番を差し替えてみる
pipeline2 = factory.set_order(["add","multiply","sqrt","power"]).build()
# まず +10, 次 *2, √, 最後に ^2
print(pipeline2(4))
# 値の設定 → 順序設定 → パイプライン生成 をチェーン
pipeline3 = (
factory
.set_multiply_factor(3) # multiply のファクタを 3 に
.set_add_value( 7) # add の加算値を 7 に
.set_power_exp( 4) # power の指数を 4 に
.set_divide_value(5) # divide の除数を 5 に
.set_order([ # 実行順序をカスタム
"add",
"multiply",
"sqrt",
"power",
"divide",
])
.build()
)
print(pipeline3(2))
このようにメソッドチェーンを使います。
pipeline3
の部分では、compose
がいい感じに関数をまとめてくれているとは思いませんか?
私の感覚ですが、見た目としてはこの状態が一番きれいです。
ここで、疑問があります。
「演算の途中で条件分岐をしたい場合はどうすんの?」ということです。
結論から申し上げると、compose()
は使えなくなります。
compose()
は単純で美しい合成を実現するためのものになります。純粋関数の線形合成が基本の型となります。
なので、演算の途中で条件分岐をするなどの動的な処理選択は不向きとなります。
動的パイプラインを使う例
例えば、中間結果が任意の条件を満たした時点でパイプラインを終了する場合は、次のようになります。
class PipelineFactory:
"""
動的な順序・パラメータ設定に加え、
中間結果の条件で早期終了できるパイプラインファクトリ
"""
def __init__(self):
self.multiply_factor = 2
self.add_value = 10
self.power_exp = 2
self.divide_value = 2
self.step_order = ["sqrt", "add", "power", "multiply", "divide"]
# 停止条件(Trueを返したらその時点で終了)
self.stop_condition = None
def set_multiply_factor(self, value):
self.multiply_factor = value
return self
def set_add_value(self, value):
self.add_value = value
return self
def set_power_exp(self, value):
self.power_exp = value
return self
def set_divide_value(self, value):
self.divide_value = value
return self
def set_order(self, order_list):
"""実行順序を設定"""
allowed = {"sqrt", "add", "power", "multiply", "divide"}
for name in order_list:
if name not in allowed:
raise ValueError(f"Unknown step: {name}")
self.step_order = order_list
return self
def set_stop_condition(self, predicate):
"""
中間結果が predicate(result) == True となった時点で処理を停止する
predicate: 結果を受け取って bool を返す関数
"""
self.stop_condition = predicate
return self
def build(self):
"""
現在の設定に基づいて、入力値を1つ受け取り、
各ステップを順番に適用し、
停止条件を満たしたら即座に結果を返すパイプラインを構築する
"""
# 名前→処理関数(パラメータ適用済み)のマッピング
makers = {
"multiply": multiply(self.multiply_factor),
"add": add(self.add_value),
"power": power(self.power_exp),
"divide": lambda x: divide(x, self.divide_value),
"sqrt": math.sqrt,
}
def pipeline(x):
result = x
for name in self.step_order:
func = makers[name]
result = func(result)
if self.stop_condition and self.stop_condition(result):
return result
return result
return pipeline
ここで compose
は動的処理との相性が悪いため、完全に消えました。
これを使い、中間結果が 50 を超えた時点で停止するコードを作成してみます。
factory = PipelineFactory()
pipeline = (
factory
.set_multiply_factor(3)
.set_add_value(7)
.set_power_exp(4)
.set_divide_value(5)
.set_order(["add","multiply","sqrt","power","divide"])
.set_stop_condition(lambda v: v > 50)
.build()
)
result = pipeline(2)
print(result) # 中間結果が 50 を超えた時点で停止
まだ読みやすいコードを維持できていますが、class PipelineFactory
が膨らんできそうな予感がしますね。
次のセクションで「美しさを保つ」ための一案を出します。
PipelineFactory
の小さい実装
先ほどの class PipelineFactory
をより小さくした例が以下です。
class ConditionalPipelineFactory:
def __init__(self):
self.operations = {
'multiply': multiply,
'add': add,
'power': power,
'divide': divide,
'sqrt': math.sqrt,
'abs': abs,
'round': round
}
def build_conditional_pipeline(self, rules):
"""
条件に基づいて異なる操作を実行
rules: [{'condition': lambda x: x > 0, 'operations': [('add', 10)]}, ...]
"""
def pipeline(x):
result = x
for rule in rules:
condition = rule['condition']
operations = rule['operations']
if condition(result):
for op_name, *params in operations:
if op_name in self.operations:
func = self.operations[op_name]
if params:
if op_name in ['multiply', 'add', 'power']:
partial_func = func(*params)
result = partial_func(result)
else:
result = func(result, *params)
else:
result = func(result)
return result
return pipeline
汎用性と可読性のトレードオフですがここまでで取り扱った内容だと、この辺りに落ち着くかなと思います。
以下が使い方です。
factory = ConditionalPipelineFactory()
pipeline = factory.build_conditional_pipeline([
{
'condition': lambda x: x > 50,
'operations': [('multiply', 2), ('add', 10)]
},
{
'condition': lambda x: x <= 50,
'operations': [('add', 5), ('multiply', 3)]
}
])
Haskell での実装
さて、ここからが本題です。
Haskell はカリー化と関数合成は言語の基本機能として組み込まれています。また、型システムによりコンパイル時に関数の合成が正しいかチェックしてくれるので動作検証の段階でミスが減るといった恩恵があります。
上述の class ConditionalPipelineFactory
を Haskell に置き換え、より実践的な実装を目指します。
完全なコードは一番下に記載しています。
module ConditionalPipelineFactory where
module ConditionalPipelineFactory
を作っていきます。
小道具
上述した例と同じように数字演算するための関数を作成してもいいです。
multiply :: Double -> Double -> Double
multiply x y = x * y
add :: Double -> Double -> Double
add x y = x + y
power :: Double -> Double -> Double
power base exp = base ** exp
divide :: Double -> Double -> Double
divide x y = x / y
このように定義してもよいですが、multiply が Double 型で x * y をすることは周知の事実なためわざわざ書かなくてもよいです。以降は自明なものは書きません。
13 種類の演算操作のを定義します。
data Operation =
Multiply Double
| Add Double
| Power Double
| Divide Double
| Sqrt
| Abs
| Round Int -- | 桁数指定
| Square
| Double
| Half
| Log
| Exp
deriving (Show, Eq)
定義順序が前後しますが、safeDivide
, safeSqrt
, safeLog
は Maybe 型を使用してエラーの可能性を型レベルで表現するとより安全になります。
-- | ゼロ除算、負数の平方根、非正数の対数などの危険な操作を安全に処理
-- | 除算(ゼロ除算チェック付き)
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide x y = Just (x / y)
-- | 平方根(負数チェック付き)
safeSqrt :: Double -> Maybe Double
safeSqrt x
| x < 0 = Nothing
| otherwise = Just (sqrt x)
-- | 対数(正数チェック付き)
safeLog :: Double -> Maybe Double
safeLog x
| x <= 0 = Nothing
| otherwise = Just (log x)
演算操作ルール型を定義
ルール名と説明を追加してデバッグ性を向上させる意図があります。
-- | 条件付きルールを表すデータ型
data Rule = Rule
{ ruleName :: String
, condition :: Double -> Bool
, operations :: [Operation]
, description :: String
}
デバッグ用に操作ルール表示用のインスタンスを定義します。
-- | Rule の Show インスタンス
instance Show Rule where
show (Rule name _ ops desc) =
"Rule { name = \"" ++ name ++
"\", operations = " ++ show ops ++
", description = \"" ++ desc ++ "\" }"
エラーハンドリング型
エラーハンドリング用に戻り値の共通型を作ります。
目的は次の通りです。
- 代数的データ型でエラーと成功を明確に区別
- エラー発生時も元の値を保持し、処理の継続が可能
data PipelineResult
= Success Double
| Error String Double -- エラーメッセージと元の値
deriving (Show, Eq)
演算操作関数を定義
エラーハンドリング付きの単一の演算操作を適用する関数群を定義します。
applyOperation :: Operation -> Double -> PipelineResult
applyOperation (Multiply y) x = Success (x * y)
applyOperation (Add y) x = Success (x + y)
applyOperation (Power exp) base =
if base < 0 && (fromIntegral (floor exp) /= exp)
then Error "Cannot raise negative number to non-integer power" base
else Success (base ** exp)
applyOperation (Divide y) x =
if y == 0
then Error "Division by zero" x
else Success (x / y)
applyOperation (SafeDivide y) x =
case safeDivide x y of
Just result -> Success result
Nothing -> Error "Safe division by zero" x
applyOperation Sqrt x =
if x < 0
then Error "Square root of negative number" x
else Success (sqrt x)
applyOperation SafeSqrt x =
case safeSqrt x of
Just result -> Success result
Nothing -> Error "Safe square root of negative number" x
applyOperation Abs x = Success (abs x)
applyOperation (Round digits) x =
let factor = 10 ^ digits
in Success (fromIntegral (round (x * factor)) / factor)
applyOperation Log x =
if x <= 0
then Error "Logarithm of non-positive number" x
else Success (log x)
applyOperation SafeLog x =
case safeLog x of
Just result -> Success result
Nothing -> Error "Safe logarithm of non-positive number" x
applyOperation Negate x = Success (-x)
applyOperation Identity x = Success x
applyOperation
が純粋関数となっており、末端の演算処理に対応しています。
そして、以下で関数の合成を行います。
applyOperations :: [Operation] -> Double -> PipelineResult
applyOperations [] x = Success x
applyOperations (op:ops) x =
case applyOperation op x of
Success newX -> applyOperations ops newX
Error msg _ -> Error msg x
applyOperations
は上述 python ソースの compose
に対応しています。
これで操作のリストを安全に適用する関数ができました。
ロジック層の定義
演算操作関数を定義のセクションで作成した純粋関数をパイプラインに単一のルールを適用する型シグネチャを作ります。
applyRule :: Rule -> Double -> PipelineResult
applyRule (Rule _ cond ops _) x
| cond x = applyOperations ops x -- 条件に合致→操作を実行
| otherwise = Success x -- 条件に合致しない→元の値をそのまま返す
具体例
-- ルールの例
positiveRule = Rule "Positive" (> 0) [Add 10, Multiply 2] "正数処理"
-- 使用例
applyRule positiveRule 5.0 -- Success 30.0 (5 + 10 = 15, 15 * 2 = 30)
applyRule positiveRule (-3.0) -- Success (-3.0) (条件不一致、元の値返却)
この関数は条件付き処理の核となる部分で、入力値が特定の条件を満たす場合のみ操作を適用し、そうでなければ元の値をそのまま返します。
複数のルールを順番に適用する再帰関数
buildConditionalPipeline :: [Rule] -> Double -> PipelineResult
buildConditionalPipeline [] x = Success x -- ルールがなくなったら成功として元の値を返す
-- | 再帰を実行(ルールを順次適用)
buildConditionalPipeline (rule:rules) x =
case applyRule rule x of
Success newX -> buildConditionalPipeline rules newX -- 成功→次のルールへ
Error msg _ -> Error msg x -- エラー→即座に停止
buildConditionalPipeline
ではルールベースのパイプラインがどのように実行されるかを示すための型です。これは型チェックやドキュメントとして機能します。なので実行関数として直接で使うものではありません。
※このようなドキュメント用の実装をどこまで書くかは利用側で決めてください。
以下のように動作することを定義しています。
rules = [rule1, rule2, rule3]
input = 10.0
-- 1. rule1を10.0に適用 → Success 20.0
-- 2. rule2を20.0に適用 → Success 25.0
-- 3. rule3を25.0に適用 → Success 50.0
-- 結果: Success 50.0
-- エラーの場合:
-- 1. rule1を10.0に適用 → Success 20.0
-- 2. rule2を20.0に適用 → Error "Division by zero" 20.0
-- 結果: Error "Division by zero" 10.0 (元の入力値で返す)
ルールを作成
任意のルールを作成します。ここでは例として 3 つのルールを作ります。
advancedRules :: [Rule]
advancedRules =
[ rule "PerfectSquares(完全平方数)" (\x -> x > 0 && sqrt x == fromIntegral (floor (sqrt x)))
[SafeSqrt, Multiply 2]
"Perfect squares: take square root and multiply by 2"
, rule "PowersOfTwo(2の累乗)" (\x -> x > 0 && (logBase 2 x == fromIntegral (floor (logBase 2 x))))
[SafeLog, Add 1]
"Powers of 2: take natural log and add 1"
, rule "EvenIntegers(偶数の整数)" (\x -> x == fromIntegral (floor x) && floor x `mod` 2 == 0)
[Divide 2, Add 5]
"Even integers: divide by 2 and add 5"
]
パイプライン実行関数
runPipeline :: [Rule] -> Double -> PipelineResult
runPipeline = buildConditionalPipeline
ついでにテスト実行用の関数も作成します。
testAdvancedPipeline :: Double -> PipelineResult
testAdvancedPipeline = runPipeline advancedRules
ここまでで一旦完成です。
動かしてみます。
main :: IO ()
main = do
putStrLn "=== Basic Conditional Pipeline Test ==="
let testValues = [5, -4, 150, -10, 0, 0.5, 16, 8, 12]
putStrLn "\n=== Advanced Pipeline Test ==="
putStrLn "\nAdvanced Rules:"
mapM_ print advancedRules
putStrLn "\nAdvanced Pipeline Results:"
mapM_ (\x -> putStrLn $ show x ++ " -> " ++ show (testAdvancedPipeline x)) testValues
-- | 出力
-- === Basic Conditional Pipeline Test ===
-- === Advanced Pipeline Test ===
-- Advanced Rules:
-- Rule { name = "PerfectSquares", operations = [SafeSqrt,Multiply 2.0], description = "Perfect squares: take square root and multiply by 2" }
-- Rule { name = "PowersOfTwo", operations = [SafeLog,Add 1.0], description = "Powers of 2: take natural log and add 1" }
-- Rule { name = "EvenIntegers", operations = [Divide 2.0,Add 5.0], description = "Even integers: divide by 2 and add 5" }
-- Advanced Pipeline Results:
-- 5.0 -> Success 5.0
-- -4.0 -> Success 3.0
-- 150.0 -> Success 80.0
-- -10.0 -> Success 0.0
-- 0.0 -> Success 5.0
-- 0.5 -> Success 0.3068528194400547
-- 16.0 -> Success 3.0794415416798357
-- 8.0 -> Success 3.0794415416798357
-- 12.0 -> Success 11.0
ちゃんと機能していそうですね。
まとめ
本記事では、Python と Haskell を用いてカリー化と関数合成の考え方を段階的に学習しました。
学習した内容を簡単にまとめます。
基本概念**
- カリー化: 複数の引数を持つ関数を、一つずつ引数を取る関数の連鎖に変換
- 関数合成: 小さな関数を組み合わせて複雑な処理を構築する手法
- 部分適用: 引数の一部だけを与えて新しい関数を生成
言語による特徴の違い**
側面 | Python | Haskell |
---|
カリー化 | デコレータで実装 | 言語の基本機能 |
関数合成 | compose 関数で実現 | . 演算子で直接表現 |
型安全性 | 実行時エラー | コンパイル時チェック |
エラーハンドリング | 例外処理 | Maybe /Either 型 |
可読性 | 明示的だが冗長 | 簡潔だが学習コストあり |
実際のプロダクトでの活用
- データ処理: ETL パイプライン、機械学習前処理
最後に全体コードを記載しておきます。
全体コード
# python
from functools import partial, reduce
def curry(func):
def curried(*args, **kwargs):
if len(args) + len(kwargs) >= func.__code__.co_argcount:
return func(*args, **kwargs)
return partial(func, *args, **kwargs)
return curried
def compose(*functions):
def inner(arg):
return reduce(lambda result, func: func(result), functions, arg)
return inner
@curry
def multiply(x, y):
return x * y
@curry
def add(x, y):
return x + y
@curry
def power(base, exp):
return base ** exp
@curry
def divide(x, y):
return x / y
class PipelineFactory:
"""
動的な順序・パラメータ設定に加え、
中間結果の条件で早期終了できるパイプラインファクトリ
"""
def __init__(self):
self.multiply_factor = 2
self.add_value = 10
self.power_exp = 2
self.divide_value = 2
self.step_order = ["sqrt", "add", "power", "multiply", "divide"]
# 停止条件(Trueを返したらその時点で終了)
self.stop_condition = None
def set_multiply_factor(self, value):
self.multiply_factor = value
return self
def set_add_value(self, value):
self.add_value = value
return self
def set_power_exp(self, value):
self.power_exp = value
return self
def set_divide_value(self, value):
self.divide_value = value
return self
def set_order(self, order_list):
"""実行順序を設定"""
allowed = {"sqrt", "add", "power", "multiply", "divide"}
for name in order_list:
if name not in allowed:
raise ValueError(f"Unknown step: {name}")
self.step_order = order_list
return self
def set_stop_condition(self, predicate):
"""
中間結果が predicate(result) == True となった時点で処理を停止する
predicate: 結果を受け取って bool を返す関数
"""
self.stop_condition = predicate
return self
def build(self):
"""
現在の設定に基づいて、入力値を1つ受け取り、
各ステップを順番に適用し、
停止条件を満たしたら即座に結果を返すパイプラインを構築する
"""
# 名前→処理関数(パラメータ適用済み)のマッピング
makers = {
"multiply": multiply(self.multiply_factor),
"add": add(self.add_value),
"power": power(self.power_exp),
"divide": lambda x: divide(x, self.divide_value),
"sqrt": math.sqrt,
}
def pipeline(x):
result = x
for name in self.step_order:
func = makers[name]
result = func(result)
if self.stop_condition and self.stop_condition(result):
return result
return result
return pipeline
class ConditionalPipelineFactory:
def __init__(self):
self.operations = {
'multiply': multiply,
'add': add,
'power': power,
'divide': divide,
'sqrt': math.sqrt,
'abs': abs,
'round': round
}
def build_conditional_pipeline(self, rules):
"""
条件に基づいて異なる操作を実行
rules: [{'condition': lambda x: x > 0, 'operations': [('add', 10)]}, ...]
"""
def pipeline(x):
result = x
for rule in rules:
condition = rule['condition']
operations = rule['operations']
if condition(result):
for op_name, *params in operations:
if op_name in self.operations:
func = self.operations[op_name]
if params:
if op_name in ['multiply', 'add', 'power']:
partial_func = func(*params)
result = partial_func(result)
else:
result = func(result, *params)
else:
result = func(result)
return result
return pipeline
-- haskell
{-# LANGUAGE OverloadedStrings #-}
module ConditionalPipeline where
-- 安全な算術操作を提供するモジュール
import Control.Monad (when)
-- | 除算(ゼロ除算チェック付き)
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide x y = Just (x / y)
-- | 平方根(負数チェック付き)
safeSqrt :: Double -> Maybe Double
safeSqrt x
| x < 0 = Nothing
| otherwise = Just (sqrt x)
-- | 対数(正数チェック付き)
safeLog :: Double -> Maybe Double
safeLog x
| x <= 0 = Nothing
| otherwise = Just (log x)
-- | 操作を表すデータ型
data Operation
= Multiply Double
| Add Double
| Power Double
| Divide Double
| SafeDivide Double
| Sqrt
| SafeSqrt
| Abs
| Round Int
| Log
| SafeLog
| Negate
| Identity
deriving (Show, Eq)
data Rule = Rule
{ ruleName :: String
, condition :: Double -> Bool
, operations :: [Operation]
, description :: String
}
-- | Rule の Show インスタンス
instance Show Rule where
show (Rule name _ ops desc) =
"Rule { name = \"" ++ name ++
"\", operations = " ++ show ops ++
", description = \"" ++ desc ++ "\" }"
-- | パイプライン実行結果
data PipelineResult
= Success Double
| Error String Double -- エラーメッセージと元の値
deriving (Show, Eq)
-- | 単一の操作を適用する関数(エラーハンドリング付き)
applyOperation :: Operation -> Double -> PipelineResult
applyOperation (Multiply y) x = Success (x * y)
applyOperation (Add y) x = Success (x + y)
applyOperation (Power exp) base =
if base < 0 && (fromIntegral (floor exp) /= exp)
then Error "Cannot raise negative number to non-integer power" base
else Success (base ** exp)
applyOperation (Divide y) x =
if y == 0
then Error "Division by zero" x
else Success (x / y)
applyOperation (SafeDivide y) x =
case safeDivide x y of
Just result -> Success result
Nothing -> Error "Safe division by zero" x
applyOperation Sqrt x =
if x < 0
then Error "Square root of negative number" x
else Success (sqrt x)
applyOperation SafeSqrt x =
case safeSqrt x of
Just result -> Success result
Nothing -> Error "Safe square root of negative number" x
applyOperation Abs x = Success (abs x)
applyOperation (Round digits) x =
let factor = 10 ^ digits
in Success (fromIntegral (round (x * factor)) / factor)
applyOperation Log x =
if x <= 0
then Error "Logarithm of non-positive number" x
else Success (log x)
applyOperation SafeLog x =
case safeLog x of
Just result -> Success result
Nothing -> Error "Safe logarithm of non-positive number" x
applyOperation Negate x = Success (-x)
applyOperation Identity x = Success x
-- | 操作のリストを安全に適用する関数
applyOperations :: [Operation] -> Double -> PipelineResult
applyOperations [] x = Success x
applyOperations (op:ops) x =
case applyOperation op x of
Success newX -> applyOperations ops newX
Error msg _ -> Error msg x
-- | 単一のルールを適用する関数
applyRule :: Rule -> Double -> PipelineResult
applyRule (Rule _ cond ops _) x
| cond x = applyOperations ops x
| otherwise = Success x
-- | 条件付きパイプラインを構築する関数
buildConditionalPipeline :: [Rule] -> Double -> PipelineResult
buildConditionalPipeline [] x = Success x
buildConditionalPipeline (rule:rules) x =
case applyRule rule x of
Success newX -> buildConditionalPipeline rules newX
Error msg _ -> Error msg x
-- | ルール構築関数
rule :: String -> (Double -> Bool) -> [Operation] -> String -> Rule
rule name cond ops desc = Rule name cond ops desc
-- | ルール例
advancedRules :: [Rule]
advancedRules =
[ rule "PerfectSquares" (\x -> x > 0 && sqrt x == fromIntegral (floor (sqrt x)))
[SafeSqrt, Multiply 2]
"Perfect squares: take square root and multiply by 2"
, rule "PowersOfTwo" (\x -> x > 0 && (logBase 2 x == fromIntegral (floor (logBase 2 x))))
[SafeLog, Add 1]
"Powers of 2: take natural log and add 1"
, rule "EvenIntegers" (\x -> x == fromIntegral (floor x) && floor x `mod` 2 == 0)
[Divide 2, Add 5]
"Even integers: divide by 2 and add 5"
]
-- | パイプライン実行関数
runPipeline :: [Rule] -> Double -> PipelineResult
runPipeline = buildConditionalPipeline
testAdvancedPipeline :: Double -> PipelineResult
testAdvancedPipeline = runPipeline advancedRules
-- | メイン関数(テスト用)
main :: IO ()
main = do
putStrLn "=== Basic Conditional Pipeline Test ==="
let testValues = [5, -4, 150, -10, 0, 0.5, 16, 8, 12]
putStrLn "\n=== Advanced Pipeline Test ==="
putStrLn "\nAdvanced Rules:"
mapM_ print advancedRules
putStrLn "\nAdvanced Pipeline Results:"
mapM_ (\x -> putStrLn $ show x ++ " -> " ++ show (testAdvancedPipeline x)) testValues