View Source
Funx.Utils
Usage Rules
LLM Functional Programming Foundation
Key Concepts for LLMs:
Currying: Converting a multi-argument function into a chain of single-argument functions
curry_r/1
: Curries arguments from right to left, allowing data argument to be applied last (pipeline-friendly)curry/1
&curry_l/1
: Left-to-right currying (traditional functional style)- Example:
f(a, b, c)
→curry(f).(a).(b).(c)
- Edge case: Currying a unary function returns the original function (no-op)
Partial Application: Fixing some arguments of a function, creating a new function
- Result of currying: each step returns a function waiting for remaining args
- Enables configuration-first, data-last patterns
- Example:
add = curry_r(+).(5)
creates function that adds 5
Point-Free Style: Writing functions without explicitly mentioning arguments
- Compose functions without intermediate variables
- More declarative and reusable
- Example:
process = transform |> validate |> save
Function Flipping: Reversing argument order for better composition
flip/1
: Swaps arguments of binary functions (arity = 2 only)- Useful when argument order doesn't match pipeline needs
- Example:
flip(div).(2, 10)
→10 / 2
- Invalid:
flip/1
cannot be applied to unary or 3+ arity functions
Arity Independence: Works with functions of any number of arguments
- Dynamically inspects function arity via
:erlang.fun_info/2
- Returns as many nested unary functions as the original function has parameters
- No need to know function arity in advance - curry supports arbitrary arity
LLM Decision Guide: When to Use Utils
✅ Use Utils when:
- Building reusable, composable functions
- Need partial application for configuration
- Want point-free programming style
- Adapting functions for pipeline use
- User says: "configure then apply", "reuse with different parameters", "point-free"
❌ Don't use Utils when:
- Simple one-off function calls
- Performance is absolutely critical
- Argument order is already correct
- Functions are already curried
⚡ Currying Strategy Decision:
- Pipeline-friendly: Use
curry_r/1
(data flows left-to-right, config right-to-left) - Traditional FP: Use
curry/1
orcurry_l/1
(left-to-right application) - Argument reordering: Use
flip/1
then curry as needed
⚙️ Function Choice Guide (Mathematical Purpose):
- Configuration before data:
curry_r(fn config, data -> ... end).(config)
- Traditional currying:
curry(fn a, b, c -> ... end).(a).(b).(c)
- Argument order fix:
flip(fn a, b -> ... end)
- Point-free composition: Combine curried functions without variables
LLM Context Clues
User language → Utils patterns:
- "configure then apply" →
curry_r
for config-first pattern - "reuse with different settings" → curry for partial application
- "flip the arguments" →
flip/1
- "point-free style" → curry functions for composition
- "pipeline-friendly" →
curry_r/1
- "traditional currying" →
curry/1
orcurry_l/1
Quick Reference
- Use
curry_r/1
to curry functions right-to-left—ideal for Elixir's|>
pipe style. - Use
curry/1
orcurry_l/1
to curry left-to-right when needed. - Use
flip/1
to reverse arguments in binary functions. - All currying functions adapt to any arity and return nested unary functions.
Overview
Funx.Utils
provides functional utilities for reshaping multi-argument functions to support composition, partial application, and point-free style.
Use curry_r/1
by default—it aligns with Elixir’s |>
operator by shifting configuration to the right and leaving the data position first.
These tools are especially useful with predicates, monads, and other combinators where composition and reuse are key.
Composition Rules
Function | Description |
---|---|
curry_r/1 | Curries a function right-to-left (recommended for pipelines) |
curry/1 | Curries left-to-right |
curry_l/1 | Alias for curry/1 , makes intent explicit |
flip/1 | Reverses arguments of a binary function |
Each function returns a chain of unary functions that accumulate arguments until fully applied.
Usage Patterns
Right-to-Left Currying for Pipelines
f = fn prefix, name -> prefix <> name end
greet = curry_r(f).("Hello, ")
"Alice" |> greet()
# => "Hello, Alice"
Flip for Reordering Binary Functions
div = fn a, b -> a / b end
flipped = flip(div)
flipped.(2, 10) # => 5.0
Left-to-Right Currying (Less Common)
sum = curry(fn a, b, c -> a + b + c end)
sum.(1).(2).(3) # => 6
Guidelines for Composition
Follow these patterns when reshaping or generating function transformations:
- Prefer
curry_r/1
for functions that appear in pipelines. - Use
flip/1
when argument order doesn’t match surrounding context. - Use
curry/1
orcurry_l/1
when left-to-right application is more natural. - Avoid manual
fn
nesting—use the provided helpers instead. - Avoid assuming a function has only 2 or 3 arguments—curry_r/1 supports arbitrary arity and returns as many nested unary functions as needed.
- Avoid capture syntax (
&mod.fun/arity
) when partial application is needed. Capture locks arity and order, and cannot be curried or reordered.
Currying is often most useful when composed with lifted combinators like map
, bind
, filter
, and so on.
Stability Contract
Ensure that generated or transformed functions preserve:
- Purity — returned functions are referentially transparent.
- Laziness — arguments are applied only when provided.
- Equivalence — full application matches the original function:
curry_r(f).(b).(a) == f.(a, b)
Anti-Patterns
Avoid generating or emitting:
- Manual currying via nested
fn
chains:fn a -> fn b -> f.(a, b) end end
- Assuming functions have fixed arity (curry supports arbitrary arity dynamically)
- Argument reordering by hand—prefer
flip/1
- Full application immediately after currying
- Capture syntax (
&fun/2
) when transformation is required
Good Patterns
Encourage completions like:
# Partial right-to-left application for pipelines
assign_role = curry_r(fn role, user -> %{user | role: role} end)
user |> assign_role.("admin")
# Flipping to enable higher-order composition
compare = flip(fn a, b -> a > b end)
Enum.filter(list, compare.(10))
# Contextual function with partial application
transform =
curry_r(fn format, name -> format.("<" <> name <> ">") end)
"Alex" |> transform.(&String.upcase/1)
When to Use
Reach for these utilities when you want to:
- Enable point-free style
- Compose partial functions within a pipeline
- Shift configuration before data
- Adapt argument order to match surrounding combinators
- Prepare functions before lifting into a monadic or applicative context
Built-in Behavior
curry_r/1
,curry/1
, andcurry_l/1
inspect function arity via:erlang.fun_info/2
.- Returned functions accumulate arguments until fully applied.
flip/1
applies only to functions of arity 2.
LLM Code Templates
Configuration-First Pattern Template
# API client with configurable base settings
def build_api_client() do
request_fn = curry_r(fn headers, auth, url ->
HTTPoison.get(url, headers, auth: auth)
end)
# Pre-configure common settings
authenticated_request = request_fn
|> apply.([{"Content-Type", "application/json"}])
|> apply.({:bearer, "token"})
# Now just pass URLs
"/users" |> authenticated_request.()
"/posts" |> authenticated_request.()
end
Data Transformation Pipeline Template
def build_transformer() do
# Curry transformation functions for reuse
validate_with = curry_r(fn rules, data ->
if Enum.all?(rules, fn rule -> rule.(data) end) do
{:ok, data}
else
{:error, "validation failed"}
end
end)
transform_with = curry_r(fn mapper, {:ok, data} -> {:ok, mapper.(data)} end)
# Build reusable pipelines
user_rules = [&is_adult/1, &has_email/1]
user_validator = validate_with.(user_rules)
user_transformer = transform_with.(&normalize_user/1)
# Apply to data
user_data
|> user_validator.()
|> user_transformer.()
end
Function Composition Template
def build_processors() do
# Flip functions to match pipeline argument order
filter_by = flip(&Enum.filter/2)
map_with = flip(&Enum.map/2)
reduce_by = curry_r(&Enum.reduce/3)
# Create specialized processors
filter_adults = filter_by.(fn user -> user.age >= 18 end)
extract_names = map_with.(fn user -> user.name end)
count_items = reduce_by.(0, fn _, acc -> acc + 1 end)
# Compose into pipeline
users
|> filter_adults.()
|> extract_names.()
|> count_items.()
end
Predicate Factory Template
def build_predicates() do
# Create configurable predicates
field_equals = curry_r(fn value, field, item ->
Map.get(item, field) == value
end)
field_greater = curry_r(fn threshold, field, item ->
Map.get(item, field) > threshold
end)
# Generate specific predicates
is_admin = field_equals.(:admin, :role)
is_adult = field_greater.(18, :age)
is_active = field_equals.(true, :active)
# Use with filtering
users |> Enum.filter(is_admin)
users |> Enum.filter(is_adult)
end
LLM Performance Considerations
Currying overhead:
- Each curried function call has slight overhead
- Consider performance impact for hot paths
- Pre-curry functions used repeatedly
Memory considerations:
- Curried functions capture arguments in closures
- Can prevent garbage collection of captured values
- Use judiciously in long-running processes
Thread-safety for closures:
- Curried closures may capture config values
- In long-running processes, ensure captured values don't include large, stateful, or cyclic data
- Captured values should be immutable and reasonably sized
Optimization patterns:
# ✅ Good: curry once, use many times
transformer = curry_r(&String.replace/3).("old", "new")
results = Enum.map(strings, transformer)
# ❌ Less efficient: curry in loop
results = Enum.map(strings, fn s ->
curry_r(&String.replace/3).("old", "new").(s)
end)
LLM Interop Patterns
With Enum Functions
# Make Enum functions pipeline-friendly
map_with = flip(&Enum.map/2)
filter_by = flip(&Enum.filter/2)
reduce_by = curry_r(&Enum.reduce/3)
# Use in pipelines
data
|> filter_by.(predicate)
|> map_with.(transformer)
|> reduce_by.(initial_value, accumulator)
With GenServer Calls
# Create configured GenServer callers
def build_service_client(server_name) do
call_server = curry_r(&GenServer.call/2).(server_name)
cast_server = curry_r(&GenServer.cast/2).(server_name)
%{
get_user: call_server.({:get_user, user_id}),
update_user: cast_server.({:update_user, user_data}),
delete_user: cast_server.({:delete_user, user_id})
}
end
With Phoenix Contexts
# Create context function factories
def build_user_operations(repo) do
create_with_repo = curry_r(fn changeset, repo ->
Repo.insert(changeset, repo: repo)
end).(repo)
update_with_repo = curry_r(fn changeset, user, repo ->
Repo.update(changeset, repo: repo)
end).(repo)
%{
create_user: create_with_repo,
update_user: update_with_repo
}
end
LLM Testing Guidance
Test Currying Behavior
test "curry_r creates proper function chain" do
add3 = fn a, b, c -> a + b + c end
curried = Funx.Utils.curry_r(add3)
# Test partial application
partial1 = curried.(3)
partial2 = partial1.(2)
result = partial2.(1)
assert result == 6
assert add3.(1, 2, 3) == curried.(3).(2).(1)
end
test "curry_l creates left-to-right chain" do
multiply3 = fn a, b, c -> a * b * c end
curried = Funx.Utils.curry(multiply3)
result = curried.(2).(3).(4)
assert result == 24
end
Test Function Flipping
test "flip reverses binary function arguments" do
subtract = fn a, b -> a - b end
flipped = Funx.Utils.flip(subtract)
assert subtract.(10, 3) == 7
assert flipped.(3, 10) == 7
end
Test Point-Free Composition
test "curried functions compose for point-free style" do
transform = Funx.Utils.curry_r(fn suffix, prefix, text ->
prefix <> text <> suffix
end)
add_brackets = transform.("]", "[")
add_parens = transform.(")", "(")
assert add_brackets.("test") == "[test]"
assert add_parens.("test") == "(test)"
end
LLM Debugging Tips
Test Individual Steps
# Debug currying by testing each step
add3 = fn a, b, c -> a + b + c end
curried = curry_r(add3)
step1 = curried.(3)
IO.inspect(step1, label: "after first arg")
step2 = step1.(2)
IO.inspect(step2, label: "after second arg")
result = step2.(1)
IO.inspect(result, label: "final result")
Verify Equivalence
# Ensure curried version equals original
original_result = original_fn.(arg1, arg2, arg3)
curried_result = curry_r(original_fn).(arg3).(arg2).(arg1)
assert original_result == curried_result
LLM Error Message Design
Handle Arity Mismatches
def safe_curry(fun) do
case :erlang.fun_info(fun, :arity) do
{:arity, 0} -> {:error, "Cannot curry zero-arity function"}
{:arity, n} when n > 0 -> {:ok, Funx.Utils.curry_r(fun)}
_ -> {:error, "Invalid function"}
end
end
Provide Clear Function Descriptions
def build_transformer(name, transform_fn) do
curried = Funx.Utils.curry_r(transform_fn)
# Add metadata for debugging
fn config ->
fn data ->
try do
curried.(config).(data)
rescue
error ->
{:error, "#{name} transformation failed: #{inspect(error)}"}
end
end
end
end
LLM Common Mistakes to Avoid
❌ Don't use capture syntax with currying
# ❌ Wrong: capture syntax can't be curried
curry_r(&String.replace/3)
# ✅ Correct: use explicit function
curry_r(fn str, old, new -> String.replace(str, old, new) end)
❌ Don't assume argument order
# ❌ Wrong: assuming curry_r argument order
divide = curry_r(fn a, b -> a / b end)
result = divide.(10).(2) # This gives 0.2, not 5
# ✅ Correct: be explicit about order
divide = curry_r(fn divisor, dividend -> dividend / divisor end)
result = divide.(2).(10) # This gives 5
❌ Don't curry already curried functions
# ❌ Wrong: double currying
double_curried = curry_r(curry_r(fn a, b -> a + b end))
# ✅ Correct: curry once
curried = curry_r(fn a, b -> a + b end)
❌ Don't ignore arity requirements
# ❌ Wrong: flip only works on binary functions
flip(fn a, b, c -> a + b + c end) # Will error
# ✅ Correct: use flip only on binary functions
flip(fn a, b -> a + b end)
Summary
Funx.Utils
enables functional composition through currying and argument manipulation. Use it to build reusable, configurable functions that compose naturally in pipelines.
- Right-to-left currying: Use
curry_r/1
for Elixir pipeline style (data-last) - Left-to-right currying: Use
curry/1
orcurry_l/1
for traditional functional style - Argument flipping: Use
flip/1
to adapt binary functions for better composition - Point-free style: Eliminate intermediate variables through function composition
- Partial application: Pre-configure functions with some arguments, apply data later
- Arity independence: Works with functions of any number of arguments dynamically