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

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

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

# Minimal conceptual example
main_function(a, b, c, d, e) = begin
    x = pure_function_1(a, b, d)
    y = pure_function_2(a, c, e, x)
    z = pure_function_3(b, c, y, x)
    answer = pure_function_4(x,y,z)
    return answer
end
# real example
calculate_time_dependant_losses(
    Ap,
    u,
    Ac,
    e,
    Ic,
    Ep,
    Ecm_t,
    fck,
    RH,
    T,
    cementClass::Char,
    ρ_1000,
    σ_p_start,
    f_pk,
    t0,
    ts,
    t_start,
    t_end,
) = begin
    μ = σ_p_start / f_pk
    fcm = fck + 8
    Fr = σ_p_start * Ap
    _σ_pb = σ_pb(Fr, Ac, e, Ic)
    _ϵ_cs_t_start_t_end = ϵ_cs_ti_tj(ts, t_start, t_end, Ac, u, fck, RH, cementClass)
    _ϕ_t0_t_start_t_end = ϕ_t0_ti_tj(RH, fcm, Ac, u, T, cementClass, t0, t_start, t_end)
    _Δσ_pr_t_start_t_end =
        Δσ_pr(σ_p_start, ρ_1000, t_end, μ) - Δσ_pr(σ_p_start, ρ_1000, t_start, μ)

    denominator =
        1 +
        (1 + 0.8 * _ϕ_t0_t_start_t_end) * (1 + (Ac * e^2) / Ic) * ((Ep * Ap) / (Ecm_t * Ac))
    shrinkageLoss = (_ϵ_cs_t_start_t_end * Ep) / denominator
    relaxationLoss = (0.8 * _Δσ_pr_t_start_t_end) / denominator
    creepLoss = (Ep * _ϕ_t0_t_start_t_end * _σ_pb) / Ecm_t / denominator
    return shrinkageLoss + relaxationLoss + creepLoss
end

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

Практично ли составить вышеуказанную функцию с помощью функционального программирования? Если да, то как?

Это очень много аргументов. Рассматривали ли вы создание типа (или нескольких типов) для организации некоторой (под) коллекции этих параметров?

David Young 30.10.2022 20:09

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

Joseph S 30.10.2022 20:16

Вы не хотите использовать выражение let (это то, что вы должны использовать в своем вопросе; по крайней мере, напишите действительный Haskell вместо этого псевдокода)?

chepner 30.10.2022 21:12

код написан на юлии. Мне было любопытно, есть ли в Haskell способ лучше составлять функции. Выражение AFIK let аналогично вычислению промежуточных значений в julia.

Joseph S 30.10.2022 21:21

Я немного смущен. Какое это имеет отношение к композиции функций? У вас просто есть функция, которая вызывает другие функции, но это не имеет ничего общего с композицией, насколько я понимаю этот термин. Что я вижу, так это острую необходимость вводить типы для очистки входных аргументов.

DNF 30.10.2022 21:33

«У вас просто есть функция, которая вызывает другие функции», по сути, это композиция функций. Если все функции имеют только один аргумент, их проще составить с помощью оператора канала в julia. Мне было интересно, есть ли подобные методы для нескольких аргументов в функциональном программировании. прямо сейчас все функции верхнего уровня имеют две обязанности: 1- выполнять вычисления 2- собирать аргументы для внутренних функций. Если бы существовал способ составления точно так же, как эти функции с одним аргументом, тогда всем функциям нужно было бы беспокоиться только о выполнении вычислений, что упрощает их тестирование.

Joseph S 30.10.2022 21:54

Нет, это просто нормальная функция. Композиция функций — это функция более высокого порядка, которая принимает две (или более) функции в качестве входных аргументов и возвращает новую функцию. Это в основном compose(f, g) = x->f(g(x)). В Julia функция композиции доступна в базе (docs.julialang.org/en/v1/base/base/#Base.:%E2%88%98) и делает именно это, а также составляет функции, которые принимают произвольное количество входных аргументов. Однако я не вижу отношения к вашему вопросу, поскольку ваша функция просто возвращает значения, а не функции.

DNF 30.10.2022 23:21

@DNF Композиция функций отличается от оператора композиции функций. Это похоже на разницу между суммой и оператором сложения. Последний имеет два параметра. Первое — это число (без параметров), и вы просто подчеркиваете тот факт, что получили это число, используя оператор сложения, когда называете его «сумма». То же самое для функциональной композиции (или «композиции функций»). «Композиция функций» — это («просто») функция, но вы просто подчеркиваете, что получили ее с помощью оператора композиции функций.

David Young 31.10.2022 02:58

@DavidYoung Будь то оператор композиции или результат операции, он все равно включает функции более высокого порядка, которых нет в OP. Я просто пытаюсь понять, что ищет ОП, и неясная терминология не помогает. Ссылаться на «функцию, вызывающую функцию» как на экземпляр «композиции функций», бессмысленно, поскольку все функции вызывают другие функции. Это действительно сбивает с толку то, что ищет ОП, я не просто ссорюсь.

DNF 31.10.2022 07:06

Функция компоновки @DNF, оператор композиции (существительные) и т. д. — это всего лишь механизм для выполнения функциональной композиции функций с одной переменной. когда вы пишете f(g(x), вы, по сути, применяете преобразования, описанные для каждой функции, одно за другим к данным x. Я могу сделать то же самое compose(f, g) в императивном стиле, как a = g(x), а затем f(a). Этот код также выполняет функцию композиции (глагола) с дополнительной церемонией. При вызове функции f(g(x)) результат g(x) передается в f без какой-либо промежуточной переменной. Все они делают одно и то же.

Joseph S 31.10.2022 07:36

Функция компоновки @DNF просто абстрагирует приложение функции с помощью функций более высокого порядка, что хорошо для функций с одной переменной, но не работает для многовариантных функций. Я надеялся, что какая-то магия монады поможет составить мою основную функцию с меньшими церемониями, поскольку у меня есть сотни таких функций, с которыми нужно иметь дело. Или это даже практично

Joseph S 31.10.2022 07:46

@JosephS Во-первых, композиция функций - это абстракция. Если вы каким-то образом не используете функции более высокого порядка, это не композиция функций. В частности, это связано с созданием функций из функций, а не с результатом вычислений применения составной функции. Это различие имеет решающее значение. «Абстракция» — это главное. Во-вторых, вы действительно можете сделать это для функций с несколькими входными аргументами, и это поддерживается оператором композиции Julia, с которым я связался выше.

DNF 31.10.2022 08:43

@DNF Создание вызовов функций. Например, предположим, что у нас есть две функции f и g, например, z = f(y) и y = g(x). Их составление означает, что мы сначала вычисляем y = g(x), а затем используем y для вычисления z = f(y). Вот пример на языке C: float x, y, z; // ... у = г(х); г = f (у); Шаги можно комбинировать, если не давать имени промежуточному результату: z = f(g(x)); источник: en.wikipedia.org/wiki/…

Joseph S 31.10.2022 10:07

Я думаю, что объяснил все, что мог, в этом месте, поэтому я не буду продолжать. Но я предлагаю вам сфокусировать свой вопрос немного больше, с более минимальным MWE и подчеркнуть именно одну главную вещь, которую вы хотите выполнить.

DNF 31.10.2022 12:12
Введение в одну из самых важных концепций в React - функциональное программирование
Введение в одну из самых важных концепций в React - функциональное программирование
React разработан с использованием концепции функционального программирования, поэтому понимание функционального программирования важно для изучения...
Фото ️🔁 Radek Jedynak 🔃 on ️🔁 Unsplash 🔃
Фото ️🔁 Radek Jedynak 🔃 on ️🔁 Unsplash 🔃
Что такое Java 8 Streams API? Java 8 Stream API
3
14
405
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я могу сделать небольшое начало в конце:

sum $ map (/ denominator)
  [ _ϵ_cs_t_start_t_end * Ep
  , 0.8 * _Δσ_pr_t_start_t_end
  , (Ep * _ϕ_t0_t_start_t_end * _σ_pb) / Ecm_t
  ]

Как упоминалось в комментариях (неоднократно), оператор композиции функций действительно принимает функции с несколькими аргументами. Ссылка: https://docs.julialang.org/en/v1/base/base/#Base.:%E2%88%98

help?> ∘
"∘" can be typed by \circ<tab>

search: ∘

  f ∘ g


  Compose functions: i.e. (f ∘ g)(args...; kwargs...) means f(g(args...; kwargs...)). The ∘ symbol
  can be entered in the Julia REPL (and most editors, appropriately configured) by typing
  \circ<tab>.

  Function composition also works in prefix form: ∘(f, g) is the same as f ∘ g. The prefix form
  supports composition of multiple functions: ∘(f, g, h) = f ∘ g ∘ h and splatting ∘(fs...) for
  composing an iterable collection of functions.

Проблема заключается в объединении операций в цепочку, потому что любая функция может передать кортеж только следующей функции в составленной цепочке. Решение может состоять в том, чтобы убедиться, что ваши цепные функции «привязывают» входные кортежи к следующей функции.

Пример:

# splat to turn max into a tuple-accepting function
julia> f = (x->max(x...)) ∘ minmax;

julia> f(3,5)
    5

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

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

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

⊕(f, g) = (args...) -> f(g(args...)...)
⊕(f, g, h...) = ⊕(f, ⊕(g, h...))

Пример:

julia> myrev(x...) = reverse(x);

julia> (myrev ⊕ minmax)(5,7)
(7, 5)

julia> (minmax ⊕ myrev ⊕ minmax)(5,7)
(5, 7)

Да, оператор композиции может принимать несколько аргументов. Это не полезно в этом контексте. Мы не можем выровнять выходные кортежи со следующей функцией, потому что это только усугубляет ситуацию, чем мой императивный пример. Я видел несколько примеров, когда функциональные программисты используют lift комбинаторы, но это не так гибко. Другой способ — объединить монады в цепочку. Я просто хотел узнать, как функциональные программисты справляются с такими проблемами? Практично ли использовать монады или придерживаться императивного стиля? Именно поэтому я отметил haskell, к сожалению, модератор удалил его.

Joseph S 03.11.2022 11:07

Кроме того, что вы подразумеваете под разумными типами? Я могу обернуть все эти параметры в какой-то тип объекта, чтобы получить помощь от IDE? Это не большая проблема, которая требует решения на данный момент. Поскольку производственный код будет написан на языке со статической типизацией, я смогу легко деструктурировать его.

Joseph S 03.11.2022 11:25

Причина, по которой ваш код трудно читать, заключается в том, что вы жонглируете большим количеством аргументов. ИМО, если ваша функция имеет более 4 входов, вы должны начать организовывать их по типам. 18 входов — это далеко за пределы разумного. Например, нельзя ли t0, ts, t_start, t_end собрать в тип Timing? А остальные в виде Material? Какую проблему вы пытаетесь решить, если это не делает код чище? Вот что я имею в виду под разумными типами: соединить логически связанные значения. Организуйте свой код так же, как вы намереваетесь в своем производственном коде.

DNF 03.11.2022 11:35

Но сосредоточившись на вашем конкретном вопросе: можете ли вы напечатать версию псевдокода вашего «минимального концептуального примера», показывающую, что вы ищете. Основываясь на том, что мой отзыв был в основном единственным, что вы получили, весьма вероятно, что вы недостаточно четко формулируете свой вопрос. И, наконец, у вас гораздо больше шансов получить продуктивные отзывы и полезные обсуждения на discourse.julialang.org, который представляет собой форум, посвященный обмену мнениями.

DNF 03.11.2022 11:37

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

Joseph S 03.11.2022 11:43

Я никоим образом не обсуждаю статические и динамические типы. Я просто говорю об организации кода. Как я уже сказал, в чем цель вашего вопроса, если он не очищает ваш код, делая его более читабельным, простым, тестируемым и отлаживаемым?

DNF 03.11.2022 11:45
Ответ принят как подходящий

Стандартный и простой способ - переделать ваш пример так, чтобы его можно было записать как

# Minimal conceptual example, re-cast 
main_function(a, b, c, d, e) = begin
    x = pure_function_1'(a, b, d)()
    y = pure_function_2'(a, c, e)(x)
    z = pure_function_3'(b, c)(y)     // I presume you meant `y` here
    answer = pure_function_4(z)      // and here, z
    return answer
end

То есть мы используем функции, которые возвращают функции одного аргумента. Теперь эти функции можно легко составить, используя, например, оператор прямой композиции (f >>> g)(x) = g(f(x)) :

# Minimal conceptual example, re-cast, composed
main_function(a, b, c, d, e) = begin
    composed_calculation = 
        pure_function_1'(a, b, d) >>>
        pure_function_2'(a, c, e) >>>
        pure_function_3'(b, c, y) >>>
        pure_function_4

    answer = composed_calculation()
    return answer
end

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

# Minimal conceptual example, re-cast, composed, args packaged
main_function(a, b, c, d, e) = begin
    composed_calculation = 
                   pure_function_1'(a, b, d) >>> put('x') >>>
      get('x') >>> pure_function_2'(a, c, e) >>> put('y') >>>
      get('x') >>> pure_function_3'(b, c, y) >>> put('z') >>>
      get({'x';'y';'z'}) >>> pure_function_4

    answer = composed_calculation(empty_initial_state)
    return value(answer)
end

Передаваемое «состояние» будет состоять из двух полей: значения и расширяемой записи. Функции будут принимать это состояние, использовать значение в качестве дополнительного ввода и оставлять запись без изменений. get уберет указанное поле из записи и поместит его в поле «значение» в состоянии. put изменит расширяемую запись в состоянии:

put(field_name) = ( {value:v ; record:r} =>
  {v ; put_record_field( r, field_name, v)} )

get(field_name) = ( {value:v ; record:r} =>
  {get_record_field( r, field_name) ; r} )

pure_function_2'(a, c, e) = ( {value:v ; record:r} =>
  {pure_function_2(a, c, e, v); r} )

value(r) = get_record_field( r, value)

empty_initial_state = { novalue ; empty_record }

Все в псевдокоде.

Применение расширенных функций и, следовательно, композиция - это один из способов осмысления того, «что такое монады». Передача пары произведенного/ожидаемого аргумента и состояния известна как монада состояния. Кодировщик сосредотачивается на работе со значениями, рассматривая состояние как «скрытое» «под покровом», как мы делаем здесь с помощью средств get/put и т. д. Под этой иллюзией/абстракцией мы можем «просто» составить наши функции.

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

Sebastian 06.11.2022 18:37

@ Себастьян, да. :) возможно, я мог бы также упомянуть монаду State - расширенную функциональную композицию, функции от (val,state) до (val,state)...

Will Ness 06.11.2022 21:48

Я был бы не против :)

Sebastian 06.11.2022 22:15

Другие вопросы по теме