пятница, 26 февраля 2010 г.

[Scala] Function currying

Вот эта тема меня поначалу сильно озадачила :) Что это такое, в принципе было понятно, не было понятно - а зачем оно мне надо? Итак, каррирование a.k.a function currying - что это? Если у нас есть некая функция с несколькими аргументами, то для получения ее значения нам необходимо передать ей значения всех аргументов. При применении каррирования мы можем передать ей лишь некоторые из них, а в качестве результата получим функцию, принимающую оставшиеся аргументы. И если уже в эту функцию мы передадим остальные аргументы, то получим собственно результат. Названа техника в честь ее изобретателя, Хаскелла Карри. В некоторых случаях это позволяет уменьшить дублирование кода и упростить его переиспользование.

В качестве практического примера позаимствую хороший пример с вычислением налогов, желающие могут ознакомиться с оригиналом на аглицком здесь.

Итак, у нас есть несколько штатов, в которых есть города. В каждом городе существует своя политика налогообложения, где сумма облагается налогом по одной ставке и плюс к этому, определенная ее часть облагается дополнительным налогом по другой ставке:

Abbr. State City Tax computation
TFTerrafirman/a2% of the entire value
TCTerracottan/a6% on the first $1500
CFConfusionGotham5% on the entire value plus 4.5% on the first $1000
CFConfusionOcean City5% on the entire value plus 3% on the first $1000
CFConfusionHometown5% on the entire value plus 1.5% on the first $1000

Сначала определим обобщенную функцию вычисления налогов:

def pct(rate: Double, amt: Long) = (rate * amt / 100.0D + 0.5D).toLong //helper function for rounded percentage calculation
def tax(rateA: Double, limit: Long, rateB: Double, amt: Long) = pct (rateA, amt) + pct (rateB, limit min amt)

Теперь определим функции для вычисления налогов в каждом городе без применения каррирования:

def taxTF (amt: Long) = tax(2.0D, 0, 0.0D, amt)
def taxTC (amt: Long) = tax(0.0D, 150000, 6.0D, amt)
def taxGothamCF (amt: Long) = tax(5.0D, 100000, 4.5D, amt)
def taxOceanCityCF (amt: Long) = tax(5.0D, 100000, 3.0D, amt)
def taxHometownCF (amt: Long) = tax(5.0D, 100000, 1.5D, amt)

Дублирование видно сразу - передаются одинаковые аргументы сразу в нескольких местах. К тому же этот код не очень хорошо читаем, сразу и не понятно, что к чему относится. А теперь добавим карри. Общая функция вычисления налогов станет выглядеть так:

def tax = (rateA: Double) => (limit: Long) =>
(rateB: Double) => (amt: Long) =>
pct (rateA, amt) + pct (rateB, limit min amt)

Первые 2 функции вычисления налогов примут вид:

def taxTF = tax( 2.0D)(0)(0.0D)
def taxTC = tax( 0.0D)(150000)(6.0D)

Здесь мы уже отделили мух от котлет - константы передаются в общую функцию, которая вернет нам функцию с одним аргументом. Вроде бы не особо на первый взгляд и необходимая вещь, однако это позволяет писать более структурированный код, где функции будут более специализированы. С остальными тремя все еще интереснее - там первые 2 аргумента дублируются, т.к. для штата процентные ставки и суммы одинаковы. Опрделим функцию налогообложения на уровне штата:

def taxCF = tax(5.0D)(100000)

Теперь черз нее можно выразить города:

def taxGothamCF = taxCF(4.5D)
def taxOceanCityCF = taxCF(3.5D)
def taxHomeTownCF = taxCF(1.0D)

Для города Gotham налог будет таким:

scala> taxGothamCF(9999)
res0: Long = 950

Дублирования нет, каждая функция делает только то, что от нее требуется. Использовать можно например так - определим функцию отчетности:

def report(taxf: (Long => Long), amts: List[Long]) = {
println("Tax on " + amts + " is " + taxf((amts :\ 0L)(_ + _)))
}
val items = List[Long](20000, 40000, 60000, 80000)

scala> report(taxHomeTownCF, items)
Tax on List(20000, 40000, 60000, 80000) is 11000

Что это даёт? В краткосрочной перспективе вобщем-то особенно ничего, но если нужно обеспечить гибкую функциональность, когда требования меняются часто, и нужно быстро и легко вносить изменения в код - то этот приём будет однозначно полезен. Кстати, налогообложение - как раз такая предметная область. Ставки и формулы там меняются весьма часто.

Комментариев нет: