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"])
Кілька моментів, на які варто звернути увагу:
Альтернативний підхід можна вважати підходом програмістів, а не математиків.
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, але якщо цікаво, погляньте на ці: