Кратко про FP в Julia

от автора

В отличие от императивного подхода, где выражается последовательность операций, функциональное программирование (FP) сосредотачивается на «что» и «как» должно быть вычислено, а не на «когда«. Это приводит к более чистому, модульному и легко тестируемому коду.

Juliа поддерживает анонимные функции, замыкания, и имеет систему типов, которая позволяет писать высокооптимизированный код без потери читаемости и удобства.

Основы

Чистые функции – это база. Они не имеют побочных эффектов и возвращают один и тот же результат при одинаковых входных данных:

function add(a, b)     return a + b end

Функция add является чистой, потому что она всегда возвращает один и тот же результат для одних и тех же значений a и b, и не имеет побочных эффектов

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

function apply_twice(f, x)     return f(f(x)) end  result = apply_twice(x -> x * 2, 5)  # 20

apply_twice принимает функцию f и применяет её дважды к значению x. Используем анонимную функцию (x -> x * 2), чтобы удвоить значение.

Лямбда-функции (или анон.функции) позволяют создавать компактные выражения для коротких функций без необходимости их именования. Замыкания же позволяют использовать переменные из внешнего контекста:

x = 10 multiplier = y -> x * y  # замыкание использующее переменную x из внешнего контекста  println(multiplier(5))  # 50

multiplier умножает свой аргумент на значение переменной x, определённой вне функции.

Неизменяемость данных и управление состоянием

В Julia базовые типы данных являются неизменяемыми. Т.е, создав экземпляр такого типа, нельзя изменить его содержимое. Попытка это сделать приведёт к созданию нового экземпляра, а не изменению существующего.

x = 10 x = 20  # создаётся новый объект, а не изменяется существующий

Julia предоставляет ключевое слово struct для определения неизменяемых пользовательских типов. После создания экземпляра такого типа, его поля не могут быть изменены:

struct Point   x::Int   y::Int end  p = Point(1, 2) # p.x = 3  # вызовет ошибку, так как структура Point неизменяема

Для создания изменяемых структур используется слово mutable struct. Однако в контексте FP предпочтение отдаётся неизменяемым структурам

Как можно использовать неизменяемость на практике в Julia для создания функционалки:

# неизменяемая структура данных struct ImmutableVector   data::Vector{Int} end  # функция, создающая новый ImmutableVector с добавленным элементом function add_element(ivec::ImmutableVector, element::Int)   new_data = copy(ivec.data)   push!(new_data, element)   return ImmutableVector(new_data) end  ivec = ImmutableVector([1, 2, 3]) ivec_new = add_element(ivec, 4)

Вместо изменения существующего экземпляра ImmutableVector, функция add_element создаёт новый экземпляр с изменённым состоянием.

Монады и функторы

Функтор – это тип данных, который можно отобразить с помощью функции, применённой к каждому его элементу. Если пошире, это любой тип, для которого можно определить функцию map. В Julia массивы являются примером функторов, поскольку к ним можно применить функцию map:

numbers = [1, 2, 3, 4, 5] squared_numbers = map(x -> x^2, numbers) println(squared_numbers)  # Выведет [1, 4, 9, 16, 25]

map применяет функцию к каждому элементу массива numbers, возвращая новый массив squared_numbers, где каждый элемент возведён в квадрат.

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

Хотя Julia не имеет встроенной поддержки монад в стандартной библиотеке, можно реализовать их самостоятельно:

struct Maybe{T}   value::Union{Nothing, T} end  # функция создания "успешного" значения success(x) = Maybe(x)  # функция для создания "неудачного" значения failure() = Maybe{Nothing}(nothing)  # bind для монады Maybe function bind(m::Maybe, f)   if isnothing(m.value)     return failure()   else     return f(m.value)   end end  # пример использования safe_divide = x -> x != 0 ? success(1/x) : failure() result = bind(success(2), safe_divide) println(result)  # Maybe(0.5)

Монаа Maybe может содержать либо значение, либо nothing, если произошла ошибка. bind позволяет применить функцию к значению внутри Maybe, только если это значение не nothing.

Рекурсия и оптимизация хвостовой рекурсии

Рекурсия позволяет описывать решения для сложных задач более естественным и выразительным способом, часто с меньшим количеством кода. Без должной осторожности рекурсия может привести к переполнению стека вызовов.

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

function factorial(n)     if n == 0         return 1     else         return n * factorial(n - 1)     end end  println(factorial(5))  # 120

При больших значениях n такой подход может привести к переполнению стека.

Хвостовая рекурсия – это особый случай рекурсии, при котором рекурсивный вызов является последней операцией, выполняемой функцией.

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

function factorial_tail(n, acc=1)     if n == 0         return acc     else         return factorial_tail(n - 1, n * acc)     end end  println(factorial_tail(5))  # 120

acc используется для накопления результата, и рекурсивный вызов является последней операцией функции

Julia не всегда автоматически оптимизирует хвостовую рекурсию. Для этого можно использовать макрос @tailrec из пакета TailRecursion.jl для явного указания на необходимость такой оптимизации:

using TailRecursion  @tailrec function factorial_tail_opt(n, acc=1)     if n == 0         return acc     else         return factorial_tail_opt(n - 1, n * acc)     end end  println(factorial_tail_opt(5))  # 120

Как юзать fp для аналитики

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

# к примеру, есть такой массив температур temperatures = [3.5, 4.1, 5.6, 7.2, 9.1, 12.3, 14.6, 14.9, 12.8, 9.7, 6.5, 4.3]  # функция для вычисления скользящего среднего function moving_average(data, window_size)     len = length(data)     result = []      for i in 1:(len - window_size + 1)         window = data[i:(i + window_size - 1)]         push!(result, sum(window) / window_size)     end      return result end  # вчисляем скользящее среднее с окном в 3 месяца average_temperatures = moving_average(temperatures, 3)

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

# массив словарей, представляющих данные о пациентах patients = [     Dict("name" => "Alice", "age" => 30),     Dict("name" => "Bob", "age" => 45),     Dict("name" => "Charlie", "age" => 25) ]  # функция для фильтрации пациентов по возрасту function filter_patients(data, age_threshold)     filter(patient -> patient["age"] > age_threshold, data) end  # функция для преобразования данных пациентов function transform_patients(data)     map(patient -> Dict("patient_name" => patient["name"], "patient_age" => patient["age"]), data) end  # фильтрация и трансформация filtered_patients = filter_patients(patients, 30) transformed_patients = transform_patients(filtered_patients)

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


ссылка на оригинал статьи https://habr.com/ru/articles/794819/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *