Currying is a great tool that allows you to split arguments into individual functions. The main advantage is that it allow you to specialize or partially apply functions to then be passed to higher order functions, like map() and reduce().
Today, we’ll show you how to use currying in your own programs.
Learn functional programming with hands-on practice.
Functional Programming Patterns With RamdaJS
The definition of currying is:
Currying turns multi-argument functions into unary (single argument) functions.
Curried functions take many arguments one at a time.
In other words, In other words, the function takes the first argument and returns a new function. This new function takes the second argument and returns a new function which then takes the third argument. This repeats until all arguments have been fulfilled.
Currying is helpful for functional programming as it allows a short hand syntax for this common functional procedure.
Let’s see an example:
Properly currying greet gives you:
This 3-argument function has been turned into three unary functions. Currying returns a function for each passed parameter.
I say “properly currying” because some curry functions are more flexible in their usage. Currying’s great in theory, but invoking a function for each argument gets tiring in JavaScript.
Ramda’s curry function lets you invoke curriedGreet like this:
Notice you can choose to give multiple arguments in a single shot. This implementation’s more useful while writing code.
And as demonstrated above, you can invoke this function forever without parameters and it’ll always return a function that expects the remaining parameters.
Mr. Elliot shared a curry implementation much like Ramda’s. Here’s the code, or as he aptly called it, a magic spell:
It’s incredibly concise, so let’s refactor and appreciate it together.
The version below works the same as the dense implementation above.
I’ve also sprinkled debugger statements to examine it in Chrome Developer Tools.
Open your Developer Tool sand follow along!
To get started, paste greet and curry into your console. Then enter curriedGreet = curry(greet) to begin.
Inspecting our two parameters we see originalFunction is greet and initialParams defaulted to an empty array because we didn’t supply it.
curry(greet) just returns a new function that expects 3 more parameters. Type curriedGreet in the console to see the the new function.
Now that we’ve seen the basic function, let’s get a bit more advanced and dosayHello = curriedGreet('Hello').
To see the original function and parameters, type originalFunction and initialParams in your console.
Notice we can still access those 2 parameters even though we’re in a completely new function? That’s because functions returned from parent functions enjoy their parent’s scope.
After a parent functions can leave their parameters for their child functions to use. Kind of like inheritance in the real life sense.
curry was initially given originalFunction and initialParams and then returned a “child” function.
Those 2 variables are not disposed in case the child function needs to reference them. If they don’t, then that scope gets cleaned up and the parameters are deleted.
On line 4, put a debugger to check the status of the program. This will help us understand how curry handles parameters.
Inspect nextParams and see that it’s ['Hello']…an array? But I thought we said curriedGreet(‘Hello’) , not curriedGreet(['Hello'])!
Correct: we invoked curriedGreet with 'Hello', but thanks to the REST syntax, 'Hello' becomes into ['Hello'].
This is because curry is a general function that can be supplied 1, 10, or 10,000,000 parameters, so it needs a way to reference all of them. Using the REST syntax like that captures every single parameter in one array, making curry’s job much easier.
Let’s jump to the next debugger statement.
You may have noticed that line 12 actually ran before the debugger statement on line 6. Our program defines a function called curriedFunction on line 5, uses it on line 12, and then we hit that debugger statement on line 6.
What is curriedFunction invoked with?
Look at params on line 5 and you’ll see ['Hello']. Both initialParams and nextParams were arrays, so we flattened and combined them into a single array using the handy spread operator (...).
Take a look:
Line 7 says “If params and originalFunction are the same length, call greet with our params and we’re done.”
This is how curry does its magic! Curry uses the output from .length() to know when it must ask for more parameters.
In JavaScript, a function’s .length property tells you how many arguments it expects. If there are less arguments (lower length) than expected, curry will ask for more arguments.
If our provided and expected parameters match, we’re good, just hand them off to the original function and finish the job!
Our example has less than the desired number of arguments.
We only provided ‘Hello’, so params.length is 1, and originalFunction.length is 3 because greet expects 3 parameters: greeting, first, last.
Since that if statement evaluates to false, the code will skip to line 10 and re-invoke our master curry function. It re-receives greet and this time, 'Hello', and begins all over again.
curry is essentially an infinite recursive loop of self-calling, parameter-hungry functions that won’t rest until the function has enough parameters.
Same parameters as before, except initialParams is ['Hello'] this time. Skip again to exit the cycle. Type our new variable into the console, sayHello. It’s still expecting more parameters but we’re getting closer to our exit condition.
Next we’ll try it with sayHelloToJohn = sayHello('John').
We’re inside line 4 again, and nextParams is ['John']. Jump to the next debugger on line 6 and inspect params: it’s ['Hello', 'John']!
Line 12 says “Hey curriedFunction, he gave me 'Hello' last time and ‘John’ this time." It then combines both into this array using the spread operator, [...initialParams, ...nextParams].
Now curriedFunction again compares the length of these params to originalFunction, and since 2 < 3 we move to line 10 and call curry once again! And of course, we pass along greet and our 2 params, ['Hello', 'John'].
Next we pass the final parameter:
sayHelloToJohnDoe = sayHelloToJohn('Doe')
This is added to the array with the previous parameters. By this point, we have the 3 we need and our exit condition triggers.
Many developers mix up currying and partial application — they are related but distinct concepts.
Currying transforms a function that takes multiple arguments into a sequence of unary (one-argument) functions:
f(a, b, c) → f(a)(b)(c)
Partial application fixes some arguments of a function, returning a new function with fewer parameters:
g = f(a, b) → g(c)
Currying | unary steps | Composability |
Partial | bind some args | Convenience / reuse |
Example:
function add(a, b, c) { return a + b + c; }const curried = curry(add); // curried(1)(2)(3) → 6const partial = add.bind(null, 1); // partial(2, 3) → 6
Emphasizing this distinction helps developers apply each technique correctly.
In modern codebases, you want to preserve type safety when currying.
TypeScript’s variadic tuple types make this possible:
function curry<Fn extends (...args: any[]) => any,P extends any[] = Parameters<Fn>,R = ReturnType<Fn>>(fn: Fn): CurryFn<P, R> {return function curried(...args: any[]): any {if (args.length >= fn.length) {return fn(...args);} else {return (...rest: any[]) => curried(...args, ...rest);}} as any;}
This approach allows TypeScript to infer argument types across successive calls, enhancing safety and developer experience.
The blog’s reliance on fn.length to determine the original arity is fragile:
fn.length ignores rest parameters (e.g., (...args) => {} reports length 0).
Default parameters after the first default reduce .length as well.
Arrow vs normal functions may have differing behavior in some environments.
Because of these caveats, many curry implementations accept multiple arguments per call (not strictly unary) to be more forgiving.
Currying becomes truly expressive when paired with compose or pipe to build function pipelines.
const trim = (s) => s.trim();const toLower = (s) => s.toLowerCase();const isLong = (min) => (s) => s.length >= min;const check = pipe(trim, toLower, isLong(5));// check(" HELLO ") → true
const req = curry((method, url, opts) => fetch(url, { method, ...opts }));const getJSON = req('GET', '/api/data');
const minLen = (n) => (s) => s.length >= n;const isAlpha = (s) => /^[A-Za-z]+$/.test(s);const validate = compose(minLen(3), isAlpha);
const setField = curry((name, value) => form[name] = value);const onNameChange = setField('name');
These examples show that currying isn’t just theoretical — it helps build modular, reusable, and elegant code.
Modern Kubernetes Services offer more than just exposing pods.
They provide advanced routing and networking features that help applications scale and perform reliably.
Service types:
Use ClusterIP for internal communication, NodePort for exposing apps on each node,LoadBalancer for cloud-managed load balancers, and ExternalName for aliasing external resources.
Headless Services:
Set clusterIP: None to directly expose pod DNS names — ideal for stateful apps like databases.
internalTrafficPolicy:
Restrict traffic to node-local endpoints to reduce latency and cross-node traffic.
SessionAffinity:
Enable sticky sessions for stateful web apps.
EndpointSlice:
Modern service discovery mechanism replacing legacy Endpoints, improving scalability.
These options give you fine-grained control over service behavior and performance.
Ingress was once the default for HTTP routing, but the Gateway API is now the future of Kubernetes networking.
It’s richer, more extensible, and better suited for modern, multi-team environments.
Ingress: Still great for simple host/path-based routing.
Gateway API: Provides HTTPRoute, TCPRoute, GRPCRoute, and traffic splitting.
It supports delegation, cross-namespace routing, and multi-tenant use cases.
Example HTTPRoute:
apiVersion: gateway.networking.k8s.io/v1beta1kind: HTTPRoutemetadata:name: my-app-routespec:parentRefs:- name: my-gatewayrules:- matches:- path:type: PathPrefixvalue: "/api"backendRefs:- name: my-serviceport: 80
This richer model is quickly becoming the standard for Kubernetes networking.
A simple deployment isn’t enough for production.
Use deployment strategies to ensure smooth updates and reduce risk.
RollingUpdate: Default strategy. Tune maxSurge and maxUnavailable for safer rollouts.
Blue/Green Deployments: Deploy a new version alongside the old one and switch traffic once validated.
Canary Deployments: Gradually shift traffic to a new version and monitor metrics.
Horizontal Pod Autoscaler (HPA v2): Scale based on CPU, memory, or custom metrics for adaptive performance.
These strategies improve uptime and reduce deployment risks.
Your deployment is only reliable if pods remain healthy and available.
Use Kubernetes’ built-in primitives to keep them that way:
Probes: Add liveness, readiness, and startup probes to detect failures early.
PodDisruptionBudget (PDB): Control how many pods can be taken down during maintenance.
unhealthyPodEvictionPolicy: Prevent healthy pods from being evicted before unhealthy ones.
Topology Spread Constraints: Distribute pods across zones and nodes to avoid single points of failure.
These features make your workloads more resilient and production-ready.
Deployment is the most common workload type, but it’s not the only option.
Choosing the right one ensures proper behavior:
StatefulSet: For apps needing stable network identities or persistent storage.
DaemonSet: For workloads that must run on every node (e.g., logging agents).
Job / CronJob: For one-time or scheduled batch jobs.
Use a simple decision matrix to guide selection based on your app’s needs.
Running workloads is only half the story.
Once your apps are live, you’ll need to debug and optimize them.
Ephemeral containers: Attach a temporary debugging container to a running pod with kubectl debug.
Resource requests and limits: Prevent OOM kills and throttling by setting appropriate values.
QoS classes: Understand how Kubernetes prioritizes pods under resource pressure.
Diagnostics: Use kubectl describe, logs, and events to troubleshoot issues quickly.
These tools and practices help you manage real-world production clusters confidently.
greet got his parameters, curry stopped looping, and we’ve received our greeting: Hello, John Doe.
Play around with this function some more. Try supplying multiple or no parameters in one shot, get as crazy as you want. See how many times curry has to recurse before returning your expected output.
Many thanks to Eric Elliott for introducing this to me, and even more thanks to you for appreciating curry with me. Until next time!
If you’d like more information on currying, you can visit Educative’s course Functional Programming Patterns with RamdaJS which gives you an in-depth look at currying, its advantages, and much more.
Happy learning!