Практические ежедневные уловки для улучшения навыков функционального программирования в Julia

Будучи таким выразительным языком, при программировании Джулия легко начать думать:

Нет ли еще более совестливого и элегантного способа решения этой проблемы?

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

Частичное применение

Частичное применение функции или каррирование - это то, что популяризировал Haskell. Это означает, что, не предоставляя все аргументы, вы можете вернуть новую функцию с остальными аргументами.

Это может показаться запутанным, поэтому позвольте мне привести несколько примеров. Обычно вы проводите такое сравнение:

julia> 3 < 4
true

Что идентично этому, потому что почти все в Julia - это функция:

julia> <(3, 4)
true

Что произойдет, если вы не приведете все аргументы?

julia> <(4)
(::Base.Fix2{typeof(<),Int64}) (generic function with 1 method)

Вместо этого вы получаете вызываемый объект. Мы можем сохранить это и использовать позже:

julia> f = <(4);

julia> f(3)
true

Чем это полезно? Это делает действительно элегантной работу с такими функциями, как map, filter и reduce.

Найдите все элементы, которые меньше значения

Найдите элементы в списке или диапазоне, которые меньше заданного значения . julia> filter(<(5), 1:10) 4-element Array{Int64,1}: 1 2 3 4

Вы также можете использовать его для поиска больших значений:

julia> filter(>(5), 1:10)
5-element Array{Int64,1}:
  6
  7
  8
  9
 10

Найти индекс элемента

Мы можем найти индекс каждого вхождения числа 4, например.

julia> findall(==(4), [4, 8, 4, 2, 1, 5])
2-element Array{Int64,1}:
 1
 3

Или мы можем просто найти первое вхождение:

julia> findfirst(==(4), [4, 8, 4, 2, 1, 5])
1

Это, конечно, одинаково хорошо работает со строками:

julia> findlast(==("foo"), ["bar", "foo", "qux", "foo"])
4

Отфильтровать определенные типы файлов

Допустим, вы хотите получить список всех .png файлов в текущем каталоге. Как ты это делаешь?

Мы можем использовать функцию endswith.

julia> endswith("somefile.png", ".png")
true

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

pngs = filter(endswith(".png"), readdir())

Отрицание функции предиката

К сожалению, я обнаружил этот трюк довольно поздно. Но оказывается, что вы можете поместить ! перед функцией, чтобы создать новую функцию, которая инвертирует ее вывод. На самом деле это не встроено в язык Julia, а просто функция, определенная как:

!(f::Function) = (x...)->!f(x...)

Опять же, может быть не совсем понятно, о чем я говорю, поэтому давайте рассмотрим несколько примеров.

Удаление пустых строк

Предположим, вы читаете все строки в файле, заданном filename, и хотите вырезать пустые строки, вы можете сделать это следующим образом:

filter(line -> !isempty(line), readlines(filename))

Но это более элегантный подход с частичным применением !:

filter(!isempty, readlines(filename))

Вот пример его использования в REPL с фиктивными данными:

julia> filter(!isempty, ["foo", "", "bar", ""])
2-element Array{String,1}:
 "foo"
 "bar"

Трансляция и карта

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

julia> map(sqrt, [9, 16, 25])
3-element Array{Float64,1}:
 3.0
 4.0
 5.0

julia> broadcast(sqrt, [9, 16, 25])
3-element Array{Float64,1}:
 3.0
 4.0
 5.0

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

Преобразование списка строк в числа

Чтобы преобразовать, скажем, строку в число, вы используете функцию parse следующим образом:

julia> parse(Int, "42")
42

Наивный способ применить это к нескольким текстовым строкам - написать:

julia> map(s -> parse(Int, s), ["7", "42", "1331"])
3-element Array{Int64,1}:
    7
   42
 1331

Мы можем упростить это с помощью функции broadcast:

julia> broadcast(parse, Int, ["7", "42", "1331"])
3-element Array{Int64,1}:
    7
   42
 1331

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

julia> parse.(Int, ["7", "42", "1331"])
3-element Array{Int64,1}:
    7
   42
 1331

Вы даже можете связать это:

julia> sqrt.(parse.(Int, ["9", "16", "25"]))
3-element Array{Float64,1}:
 3.0
 4.0
 5.0

Преобразование футляра для змеи в футляр для верблюда

В программировании у нас часто есть идентификаторы, написанные как hello_how_are_you, которые мы можем захотеть скрыть в верблюжьем регистре, написанном как HelloHowAreYou. Оказывается, вы легко можете сделать это с помощью одной строчки кода в Julia.

julia> greeting = "hello_how_are_you"
"hello_how_are_you"

julia> join(uppercasefirst.(split(greeting, '_')))
"HelloHowAreYou"

Избегайте глубокого вложения с помощью оператора трубы

Распространенная жалоба поклонников ООП на более функционально ориентированный язык, такой как Джулия, заключается в том, что трудно читать глубоко вложенные вызовы функций. Однако мы можем избежать глубокой вложенности, используя конвейер |>. Просто чтобы дать простое представление о том, что он делает. Вот пример эквивалентных выражений:

julia> string(sqrt(16))
"4.0"
julia> 16 |> sqrt |> string
"4.0"

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

julia> [16, 4, 9] .|> sqrt .|> string
3-element Vector{String}:
 "4.0"
 "2.0"
 "3.0"

С этим мы можем упростить наш змеиный футляр до верблюжьего примера.

julia> split(greeting, '_') .|> uppercasefirst |> join
"HelloHowAreYou"