Треки
/
Julia
Julia
/
Салабус
/
Композиція функцій
Ко

Композиція функцій у Julia

1 вправа

Про концепцію Композиція функцій

Julia заохочує розробників розміщувати якнайбільше коду всередині функцій, які можна JIT-компілювати, а створення багатьох маленьких функцій за задумом є продуктивним.

Через це зазвичай залишається багато маленьких простих функцій, які потрібно поєднувати, щоб виконувати нетривіальні задачі.

Один очевидний підхід - вкладати виклики функцій. Наступний приклад досить штучний, але ілюструє суть.

julia> first.(titlecase.(reverse.(["my", "test", "strings"])))
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

Недолік цього підходу в тому, що читабельність швидко падає зі зростанням глибини вкладеності.

Нам потрібен простіший і гнучкіший підхід.

Композиція

Це прийом, який дуже люблять математики, і Julia копіює математичний синтаксис.

Довільну кількість функцій можна composed разом за допомогою операторів ∘ (вводяться як \circ, а потім Tab). Результат можна використовувати як одну функцію.

julia> compfunc = first ∘ titlecase ∘ reverse
first ∘ titlecase ∘ reverse

julia> compfunc.(["my", "test", "strings"])
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

# alternative syntax, giving the same result
julia> (first ∘ titlecase ∘ reverse).(["my", "test", "strings"])

Кілька моментів, на які варто звернути увагу:

  • Початкові функції стоять у тому самому порядку, що й при вкладенні, а виконуються вони справа наліво.
  • Використовувати broadcasting під час компонування не так просто, але його можна застосувати під час виклику скомпонованої функції.

Конвеєри

Альтернативний підхід можна вважати підходом програмістів, а не математиків.

Pipelines давно використовують у сценаріях оболонки Unix, а останнім часом вони стали популярними й у широко вживаних мовах програмування (кажуть, що саме F# першою запровадила їх).

Основна ідея в тому, щоб узяти якісь дані, а потім пропустити їх через низку функцій і отримати результат.

Оператор конвеєра - |> (як у F# і в останніх версіях R), хоча в Julia є також його версія для broadcasting .|>.

julia> ["my", "test", "strings"] .|> reverse .|> titlecase .|> first
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

Тепер виконання відбувається строго зліва направо, а вихідні дані кожної функції течуть у напрямку стрілки й стають вхідними даними для наступної функції.

Обмеження, обхідні шляхи та інші варіанти

Не випадково, що всі функції, якими ми ілюстрували композицію і конвеєри, приймають один аргумент.

Деякі чисто функціональні мови подають у конвеєр перший аргумент, але дозволяють долучити й інші.

Натомість Julia приймає в конвеєрі лише назви функцій (або щось рівнозначне), без жодних додаткових аргументів.

Для цього є важливі технічні причини (повʼязані з тим, що currying не є стандартною частиною мови). Тим багатьом, хто зовсім не знайомий із каррінгом, варто просто прийняти, що це обмеження - не випадкова недбалість і навряд чи зміниться в майбутніх версіях Julia.

Обхідні шляхи

Нам потрібні функції з одним аргументом, які роблять те, що потрібно. На щастя, визначати нові функції в Julia легко.

Найпростіше - скористатися anonymous function. Наприклад, якщо в нас є один вхідний рядок тексту (англ. string) і ми хочемо розбити його за символами підкреслення:

julia> "my_test_strings" |> (s -> split(s, '_'))
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

Потім цей масив можна так само пропустити через інші функції.

Узяти анонімну функцію в дужки в цьому випадку необовʼязково, але загалом це корисний спосіб зменшити неоднозначність.

Так само ми могли б створити іменовану функцію раніше в програмі й перевикористовувати її за потреби.

Closures виходять за межі цієї концепції, але кожен, хто знайомий із ними з інших мов, побачить, що вони дають гнучкіший спосіб створювати функції з одним аргументом.

julia> function makesplit(sep)
           fs(str) = split(str, sep)
           fs
       end
makesplit (generic function with 1 method)

julia> f_us = makesplit('_')
(::var"#fs#32"{Char}) (generic function with 1 method)

julia> "my_test_strings" |> f_us
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

# alternatively:
julia> "my_test_strings" |> makesplit('_')
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

Щойно makesplit() визначено, його можна використовувати з будь-яким роздільником. Зауважмо, що makesplit('_') - це виклик функції, який обчислюється в іншу функцію, а та своєю чергою отримує вхідні дані з конвеєра.

Якщо це видається заплутаним, це нормально на початку (але з практикою стає зрозуміліше).

Інші варіанти

Тривалий час точилися дискусії про те, щоб зробити конвеєри в базовій Julia універсальнішими, але різні пропозиції взаємно несумісні, і згоди так і не досягли.

Тим часом користувачі пішли звичним шляхом і створили різні встановлювані пакети під конкретні потреби. Жоден із них не працюватиме в Exercism, але якщо цікаво, погляньте на ці:

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Композиція функцій