У меня есть чистая функция, которая принимает 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
Я вижу примеры функциональной композиции (цепочка точек, оператор конвейера и т. д.) с функциями с одним аргументом.
Практично ли составить вышеуказанную функцию с помощью функционального программирования? Если да, то как?
да, я планирую обернуть кортежи в типы данных позже. Мне любопытно, как функциональные программисты составляют эту большую функцию из более мелких функций.
Вы не хотите использовать выражение let (это то, что вы должны использовать в своем вопросе; по крайней мере, напишите действительный Haskell вместо этого псевдокода)?
код написан на юлии. Мне было любопытно, есть ли в Haskell способ лучше составлять функции. Выражение AFIK let аналогично вычислению промежуточных значений в julia.
Я немного смущен. Какое это имеет отношение к композиции функций? У вас просто есть функция, которая вызывает другие функции, но это не имеет ничего общего с композицией, насколько я понимаю этот термин. Что я вижу, так это острую необходимость вводить типы для очистки входных аргументов.
«У вас просто есть функция, которая вызывает другие функции», по сути, это композиция функций. Если все функции имеют только один аргумент, их проще составить с помощью оператора канала в julia. Мне было интересно, есть ли подобные методы для нескольких аргументов в функциональном программировании. прямо сейчас все функции верхнего уровня имеют две обязанности: 1- выполнять вычисления 2- собирать аргументы для внутренних функций. Если бы существовал способ составления точно так же, как эти функции с одним аргументом, тогда всем функциям нужно было бы беспокоиться только о выполнении вычислений, что упрощает их тестирование.
Нет, это просто нормальная функция. Композиция функций — это функция более высокого порядка, которая принимает две (или более) функции в качестве входных аргументов и возвращает новую функцию. Это в основном compose(f, g) = x->f(g(x)). В Julia функция композиции ∘ доступна в базе (docs.julialang.org/en/v1/base/base/#Base.:%E2%88%98) и делает именно это, а также составляет функции, которые принимают произвольное количество входных аргументов. Однако я не вижу отношения к вашему вопросу, поскольку ваша функция просто возвращает значения, а не функции.
@DNF Композиция функций отличается от оператора композиции функций. Это похоже на разницу между суммой и оператором сложения. Последний имеет два параметра. Первое — это число (без параметров), и вы просто подчеркиваете тот факт, что получили это число, используя оператор сложения, когда называете его «сумма». То же самое для функциональной композиции (или «композиции функций»). «Композиция функций» — это («просто») функция, но вы просто подчеркиваете, что получили ее с помощью оператора композиции функций.
@DavidYoung Будь то оператор композиции или результат операции, он все равно включает функции более высокого порядка, которых нет в OP. Я просто пытаюсь понять, что ищет ОП, и неясная терминология не помогает. Ссылаться на «функцию, вызывающую функцию» как на экземпляр «композиции функций», бессмысленно, поскольку все функции вызывают другие функции. Это действительно сбивает с толку то, что ищет ОП, я не просто ссорюсь.
Функция компоновки @DNF, оператор композиции (существительные) и т. д. — это всего лишь механизм для выполнения функциональной композиции функций с одной переменной. когда вы пишете f(g(x), вы, по сути, применяете преобразования, описанные для каждой функции, одно за другим к данным x. Я могу сделать то же самое compose(f, g) в императивном стиле, как a = g(x), а затем f(a). Этот код также выполняет функцию композиции (глагола) с дополнительной церемонией. При вызове функции f(g(x)) результат g(x) передается в f без какой-либо промежуточной переменной. Все они делают одно и то же.
Функция компоновки @DNF просто абстрагирует приложение функции с помощью функций более высокого порядка, что хорошо для функций с одной переменной, но не работает для многовариантных функций. Я надеялся, что какая-то магия монады поможет составить мою основную функцию с меньшими церемониями, поскольку у меня есть сотни таких функций, с которыми нужно иметь дело. Или это даже практично
@JosephS Во-первых, композиция функций - это абстракция. Если вы каким-то образом не используете функции более высокого порядка, это не композиция функций. В частности, это связано с созданием функций из функций, а не с результатом вычислений применения составной функции. Это различие имеет решающее значение. «Абстракция» — это главное. Во-вторых, вы действительно можете сделать это для функций с несколькими входными аргументами, и это поддерживается оператором композиции Julia, с которым я связался выше.
@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/…
Я думаю, что объяснил все, что мог, в этом месте, поэтому я не буду продолжать. Но я предлагаю вам сфокусировать свой вопрос немного больше, с более минимальным MWE и подчеркнуть именно одну главную вещь, которую вы хотите выполнить.


Я могу сделать небольшое начало в конце:
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, к сожалению, модератор удалил его.
Кроме того, что вы подразумеваете под разумными типами? Я могу обернуть все эти параметры в какой-то тип объекта, чтобы получить помощь от IDE? Это не большая проблема, которая требует решения на данный момент. Поскольку производственный код будет написан на языке со статической типизацией, я смогу легко деструктурировать его.
Причина, по которой ваш код трудно читать, заключается в том, что вы жонглируете большим количеством аргументов. ИМО, если ваша функция имеет более 4 входов, вы должны начать организовывать их по типам. 18 входов — это далеко за пределы разумного. Например, нельзя ли t0, ts, t_start, t_end собрать в тип Timing? А остальные в виде Material? Какую проблему вы пытаетесь решить, если это не делает код чище? Вот что я имею в виду под разумными типами: соединить логически связанные значения. Организуйте свой код так же, как вы намереваетесь в своем производственном коде.
Но сосредоточившись на вашем конкретном вопросе: можете ли вы напечатать версию псевдокода вашего «минимального концептуального примера», показывающую, что вы ищете. Основываясь на том, что мой отзыв был в основном единственным, что вы получили, весьма вероятно, что вы недостаточно четко формулируете свой вопрос. И, наконец, у вас гораздо больше шансов получить продуктивные отзывы и полезные обсуждения на discourse.julialang.org, который представляет собой форум, посвященный обмену мнениями.
Я не знаю, что вдаваться в дебаты о статических и динамических типах, лол. Просто знайте, что иногда совершенно разумно не заботиться о типах.
Я никоим образом не обсуждаю статические и динамические типы. Я просто говорю об организации кода. Как я уже сказал, в чем цель вашего вопроса, если он не очищает ваш код, делая его более читабельным, простым, тестируемым и отлаживаемым?
Стандартный и простой способ - переделать ваш пример так, чтобы его можно было записать как
# 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 и т. д. Под этой иллюзией/абстракцией мы можем «просто» составить наши функции.
Возможно, вы уже сами об этом знаете, но, возможно, стоит упомянуть в своем ответе, что ваше «переделывание функций... так, чтобы они возвращали функции с одним аргументом» по духу схоже с каррированием. Я бы сказал, что в своем ответе вы как-то делаете неполное каррирование, так как его достаточно для нужд в данном конкретном случае.
@ Себастьян, да. :) возможно, я мог бы также упомянуть монаду State - расширенную функциональную композицию, функции от (val,state) до (val,state)...
Я был бы не против :)
Это очень много аргументов. Рассматривали ли вы создание типа (или нескольких типов) для организации некоторой (под) коллекции этих параметров?