Композиція функцій на F# і Scala

Простіше кажучи про все це
Я почав думати про написання даної статті кілька тижнів тому, після того, коли я намагався пояснити мою 7 річному чаду що таке математичні функції. Ми почали з розгляду дуже простих речей. Це прозвучить шалено і напевно незграбно, але я закінчив моє вступне пояснення розповіддю про композиції функцій. Це здавалося настільки логічним роз'яснюючи що таке функції, наводячи приклади їх використання з навколишнього світу, говорити про композиції. Мета даної статті — показати наскільки простий і потужною є композиція функцій. Почну я з розгляду поняття чистої композиції і приземленого роз'яснення, після чого ми спробуємо трохи каррі і побавимося з монадами. Сподіваюся вам сподобається.
Функція як невеликий ящик
Давайте уявимо математичні функції у вигляді невеликих скриньок(коробок), де кожен ящик здатний приймати будь-яке додатне число аргументів, виконувати якусь задачу і повертати результат. Коротше кажучи, ми могли б представити функцію додавання як показано нижче:

Зображення 1, буквено-числове представлення функції додавання

Зображення 2, символьне представлення функції додавання
Давайте розглянемо ситуацію коли нам потрібно зібрати і запустити хлібну фабрику а ля все в одному. Ця фабрика побудована на принципі запитів, де кожен такий запит буде активізувати ланцюг специфічних операцій і на кінцевому етапі буде видавати нам результат у вигляді готового хліба. На початку нам необхідно визначити ці специфічні операції, ми будемо представляти кожну операцію у вигляді функції/скриньки. Ось список операцій вищого порядку, які могли б нам знадобитися:
  • Grind, приймає пшеницю, перемелює її і повертає борошно
  • KneadDough, бере борошно на вході, заважає її з внутрішніми інгредієнтами і виробляє тісто
  • DistributeDough, приймає всі кількість тіста, і розподіляє його по формах на виході виходить формочки з тістом
  • Bake, бере формочки з тістом, запікає їх і видає порції хліба
Настав час організувати хлібну фабрику, зібравши під єдине виробничий ланцюжок як показано нижче:
w -> [Grind] -> [KneadDough] -> [DistributeDough] -> [Bake] -> b


Зображення 3, подання зібраної ланцюга
На цьому поки все, наша ланцюг готова до роботи, вона зібрана з маленьких шматочків, де кожен шматочок може бути розібраний на окремі під-шматочки, ітд. Ви можете моделювати величезну кількість речей з оточуючого нас світу, просто використовуючи поняття композиції функцій. Це насправді дуже просто. Ви можете ознайомиться з більш теоретичними аспектами тут.
Вираз композиції
Давайте розглянемо як представити виробничий ланцюжок, описану вище, використовуючи javascript:
var b = bake(distribureDough(kneadDough(grind(w))));

Спробуйте уявити як буде виглядати ланцюг з 10 — 15 функцій, і це лише одна з можливих проблем з якою ви можете зіткнутися. Так само це не зовсім композиція, т. к. в математиці, композиція функцій це по-точкове застосування однієї функції до результату інший для отримання третьої функції. Ми можемо досягти цього наступним чином:
function myChain1(w) {
return bake(distribureDough(kneadDough(grind(w))));
}
var b = myChain1(w);

Це виглядає якось безглуздо, чи не так? Давайте закличемо міць функціонального програмування і реалізуємо це в більш прийнятній формі. Ми будемо оперувати більш зрозумілими прикладами. Для початку нам потрібно визначити що є композиція у функціональному понятті.
Версія Scala
implicit class Forward[TIn, TIntermediate](f: TIn => TIntermediate) {
def >> [TOut](g: TIntermediate => TOut): TIn => TOut = source => g(f(source))
}

Версія F
Взагалі-то, F# вже має за замовчуванням оператор композиції, вам не потрібно нічого оголошувати. Але якщо вам все-таки знадобиться змінити його, ви зможете це зробити, так:
let (>>) f g x = g ( f(x) )

Компілятор F# досить розумний, що б припустити, що ви маєте справу з функціями, так що, тип вище функції
(>>)
буде виглядати як:
f:('a -> 'b) -> g:('b -> 'c) -> x:'a -> 'c

Сцепляем всі разом
Рішення для попередньої задачі буде виглядати на Scala:
object BreadFactory {

case class Wheat()
case class Flour()
case class Dough()
case class Bread()

def grind: (Wheat => Flour) = w => {println("make the flour"); Flour()}
def kneadDough: (Flour => Dough) = f => {println("make the dough"); Dough()}
def distributeDough: (Dough => Seq[Dough]) = d => {println("distribute the dough"); Seq[Dough]()}
def bake: (Seq[Dough] => Seq[Bread]) = sd => {println("bake the bread"); Seq[Bread]()}

def main(args: Array[String]): Unit = {
(grind >> kneadDough >> distributeDough >> bake) (Wheat())
}

implicit class Forward[TIn, TIntermediate](f: TIn => TIntermediate) {
def >> [TOut](g: TIntermediate => TOut): TIn => TOut = source => g(f(source))
}
}

Версія на F# буде більш лаконічною:
type Wheat = {wheat:string}
type Flour = {flour:string}
type Dough = {dough:string}
type Bread = {bread:string}

let grind (w:Wheat) = printfn "make the flour"; {flour = ""}
let kneadDough (f:Flour) = printfn "make the dough"; {dough = ""}
let distributeDough (d:Dough) = printfn "distribute the dough"; seq { yield d}
let bake (sd:seq<Dough>) = printfn "bake the bread"; seq { yield {bread = ""}}

(grind >> kneadDough >> distributeDough >> bake) ({wheat = ""})

Вивід на консоль буде:
make the flour
make the dough
distribute the dough
bake the bread

Карринг
Якщо ви не знайомі з поняттям карринга, ви можете знайти більше інформації тут. У цій частині ми поєднаємо два потужних механізму родом зі світу функціонального програмування — карринг і композицію. Давайте розглянемо ситуацію коли вам потрібно працювати з функціями, які мають більше одного параметра і велика частина цих параметрів відома до виконання самої функції. Наприклад функція
bake
з попередньої частини може мати такі параметри як температура і тривалість запікання, які в свою чергу добре відомі заздалегідь.
Scala:
def bake: (Int => Int => Seq[Dough] => Seq[Bread]) =
temperature => duration => sd => {
println(s"bake the bread, duration: $duration, temperature: $temperature")
Seq[Bread]()
}

F#:
let bake temperature duration (sd:seq<Dough>) =
printfn "bake the bread, duration: %d, temperature: %d" temperature duration
seq { yield {bread = ""}}

Карринг це наш друг, давайте визначимо один рецепт для випікання хліба.
Scala:
def bakeRecipe1 = bake(350)(45)

def main(args: Array[String]): Unit = {
(grind >> kneadDough >> distributeDough >> bakeRecipe1) (Wheat())
}

F#:
let bakeRecipe1: seq<Dough> -> seq<Bread> = bake 350 45
(grind >> kneadDough >> distributeDough >> bakeRecipe1) ({wheat = ""})

Висновок в обох випадках буде наступним:
make the flour
make the dough
distribute the dough
bake the bread, duration: 45, temperature: 350

Монадические ланцюжка
чи Можете ви уявити собі ситуацію коли в середині вашої ланцюжка щось йде не так? Ну наприклад, ситуацію, коли провід подачі дріжджів або води засмічується і виробництво тесту порушується, або ситуацію при якій піч ламається і ми отримуємо полузапеченную масу тіста. Чистий композиція функцій може бути цікава для завдань толерантних до збоїв або без-збійних аналогам. Але що ж нам робити у вищеописаних випадках? Відповідь очевидна — використовувати монади, хм. Ви можете знайти купу фундаментальних речей по темі монад на сторінці вікіпедії. Давайте подивимося як монади можуть бути корисні в нашій ситуації, для початку нам потрібно визначити (F#) або використовувати (на Scala) спеціальний тип, званий
Either
. Визначення на F# може виглядати як розмічене об'єднання представлене нижче:
type Either<'a 'b> =
| Left of 'a
| Right of 'b

Тепер ми готові до зчеплення всіх елементів, для цього нам знадобиться створити еквівалент монадической операції bind, яка приймає монадическое значення (M) і функцію (f) здатну трансформувати це значення (
f: (x -> M y)
).
F#:
let chainFunOrFail twoTrackInput switchFunction =
match with twoTrackInput
| Left s -> switchFunction s
| Right f -> Right f

let (>>=) = chainFunOrFail

Scala:
implicit class MonadicForward[TLeft, TRight](twoTrackInput: Either[TLeft,TRight]) {
def >>= [TIntermediate](switchFunction: TLeft => Either[TIntermediate, TRight]) =
twoTrackInput match {
case Left (s) => switchFunction(s)
case Right (f) => Right(f)
}
}

Останнє що ми повинні зробити це невелика адаптація представленої вище ланцюга в новому, більш
Either
-дружньому форматі.
F#:
let grind (w:Wheat): Either<Flour, string> =
printfn "make the flour"; Left {flour = ""}
let kneadDough (f:Flour) =
printfn "make the dough"; Left {dough = ""}
let distributeDough (d:Dough) =
printfn "distribute the dough"; Left(seq { yield d})
let bake temperature duration (sd:seq<Dough>) =
printfn "bake the bread, duration: %d, temperature: %d" duration temperature
Left (seq { yield {bread = ""}})
let bakeRecipe1: seq<Dough> -> Або<seq<Bread>, string> = bake 350 45

({wheat = ""} |> grind) >>= kneadDough >>= distributeDough >>= bakeRecipe1

Scala:
def grind: (Wheat => Either[Flour, String]) = w => {
println("make the flour"); Left(Flour())
}
def kneadDough: (Flour => Either[Dough, String]) = f => {
println("make the dough"); Left(Dough())
}
def distributeDough: (Dough => Either[Seq[Dough], String]) = d => {
println("distribute the dough"); Left(Seq[Dough]())
}
def bake: (Int => Int => Seq[Dough] => Either[Seq[Bread], String]) =
temperature => duration => sd => {
println(s"bake the bread, duration: $duration, temperature: $temperature")
Left(Seq[Bread]())
}
def bakeRecipe1 = bake(350)(45)

def main(args: Array[String]): Unit = {
grind(Wheat()) >>= kneadDough >>= distributeDough >>= bakeRecipe1
}

Висновок буде виглядати так:
make the flour
make the dough
distribute the dough
bake the bread, duration: 45, temperature: 350

Якщо один з елементів вашої ланцюга поверне
Right
з відповідним індикатором помилки, то наступні елементи ланцюга будуть просто пропускає і робочий процес пропустить їх все і буде просто поширювати викинуте виняток від попереднього ланки до наступного. Ви можете самостійно спробувати пограти зі сценаріями в яких присутні помилки.
Заключна частина
Як ви могли помітити, є деяка чарівна зв'язок між теорією категорій(витоки монад) та композицією функцій. Завдання даної статті зводиться до того що б показати як управлятися на практиці з представленими механізмами і як організувати ваш код у більш функціональному вигляді. Ви можете поринути в більш фундаментальні аспекти з представленого матеріалу самостійно. Сподіваюся ця стаття буде корисною для тих з вас, хто шукає як відмовитися від імперативного програмування і зрозуміти манеру функціонального мислення, або ж хто просто хоче відкрити для себе практичні аспекти монад і функціональної композиції.
Посилання
  • Англійська версія даної статьи.
  • Все в одному, модуль на F# доступний здесь
  • Scala версія може бути завантажена здесь

чи Застосовуєте ви на практиці монади або композицію функцій

/>
/>


<input type=«checkbox» id=«vv73096»
class=«checkbox js-field-data»
name=«variant[]»
value=«73096» />
та й часто
<input type=«checkbox» id=«vv73098»
class=«checkbox js-field-data»
name=«variant[]»
value=«73098» />
Так, але рідко
<input type=«checkbox» id=«vv73100»
class=«checkbox js-field-data»
name=«variant[]»
value=«73100» />
Тільки збираюся
<input type=«checkbox» id=«vv73102»
class=«checkbox js-field-data»
name=«variant[]»
value=«73102» />
Ніколи в житті не використовував і не треба

Проголосувало 18 осіб. Утрималося 13 осіб.


Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.