tl;dr;
A Partial Application is a function where some or all of the arguments are packed inside, ready to go and it’s just waiting for a few more before the main function is invoked. They’re like functions that have default arguments, but are pure functions with a fixed amount of parameters.
Introduction
The following article and companion video playlist will cover what a partial application is and how it can be used for a more pure function option for default arguments. It’s assumed you know what pure functions are. We’ll cover:
- basic function arguments
- default arguments and how order can make them harder/easier to use
- function arity
- function currying with closures and show how the parameter order is reversed compared to default arguments
- building partial applications to show how to make using default arguments pure
- creating partial applications with no arguments
Video Playlist
In case you want an interactive example, I’ll walk through each section.
Basic Arguments
The function below pings google.com:
const pingGoogle = () => fetch('http://google.com')
. If it resolves, Google is there and your computer can talk to the internets. If it throws an error, your computer is probably having wireless trouble.
You call (or invoke) it like pingGoogle()
. If you want better logs, you’d go:
pingGoogle()
.then(() => console.log("Connected to the internet!"))
.catch( error => console.log("Not connected to the internet:", error))
Code language: JavaScript (javascript)
In software development, you’re often testing YOUR server, code that will exist in YOUR url, not googles’. Example, I wonder if my blog is up?
const pingJessesBlog = () => fetch('https://jessewarden.com')
However, while small, those are all hardcoded. While not a religion, it DOES violate DRY: Don’t Repeat Yourself. How can we make the function more re-usable? Parameters or arguments!
const ping = url => fetch(url)
Rad, now we can test Google again:
ping('http://google.com')
Or my blog:
ping('https://jessewarden.com')
… or even see if we’re connected to the internet first, THEN my blog:
ping('http://google.com')
.then(() => ping('https://jessewarden.com'))
.catch(error => console.log("Something failed:", error))
Code language: JavaScript (javascript)
Default Arguments & Pure Functions
However, we know if we call ping
, we’re going to do this google thing a lot, so to prevent ourselves from typing so much, let’s just default to “http://google.com” unless someone gives us a URL:
const ping = (url='http://google.com') => fetch(url)
Now, we only have to supply the URL parameter if we want something other than Google:
ping()
.then(() => ping('https://jessewarden.com'))
.catch(error => console.log("Something failed:", error))
Code language: JavaScript (javascript)
Notice that the function ping
is now pre-baked with an argument as a backup. “If you don’t send me one, or send me undefined
, I’ll just use ‘http://google.com’ instead.” This is what’s called a default argument. If you supply something, great, but if you don’t, still great, we got your back.
Now, ping
is not a pure function. A pure function is:
- same input, same output
- no side effects
A more pure version would be if we forced the developer to declare where they are getting the dependency fetch
:
const pingPure = (fetch, url='https://jessewarden.com') => fetch(url))
All we changed was requiring fetch
as the first parameter, the 2nd, url
is still optional and defaults to Google. Now the fetch
has side effects, like making an HTTP call, but as long as we handle the .catch
or use a try/catch with async/await syntax, it’s good enough.
ping(fetch)
.then(() => ping(fetch, 'https://jessewarden.com'))
.catch(error => console.log("Something failed:", error))
Code language: JavaScript (javascript)
Function Parameter Order
… however, that’s a pain in the ass. As you can see argument order can really make a function hard to use, and merely thinking about argument order, or testing a function out, then iterating on a new version of it with a different order can make things easier. Let’s do that by reversing the order, AND making fetch
have a default as well.
const pingPure = (url='https://jessewarden.com', fetchModule=fetch) => fetchModule(url))
Now, it’s easy to use again, but can be more pure:
ping()
.then(() => ping('https://jessewarden.com'))
.catch(error => console.log("Something failed:", error))
Code language: JavaScript (javascript)
Nice, and if you’re in older browsers, or even in Node but want fetch, you can go:
import fetch from 'cross-fetch' // polyfill for older browsers, or if you're in Node
ping(undefined, fetch)
.then(() => ping('https://jessewarden.com', fetch))
.catch(error => console.log("Something failed:", error))
Code language: JavaScript (javascript)
Note we pass undefined
manually. In JavaScript function parameters, undefined
is the same thing as “not passing anything”. The function says, “Well, I didn’t get anything, I guess I’ll use the default then.” which is google.
Pure, flexible, easy to use.
What we learned:
- default arguments make functions easier to use by requiring less arguments, and providing reasonable defaults
- the developer using them can type less
- less typing, but more importantly, less reading makes the code easier to understand
- pure functions require same input, same input, and no side effects
- pure functions more importantly, though, have to get all variables they don’t make themselves from function arguments, in this case
fetch
. - function parameter order really helps a function be more useable and easier to read
Function Arity
Let’s briefly cover function arity. You can learn more here https://jesterxl.github.io/real-world-functional-programming-book/part4/arity.html or watch a short video here https://www.youtube.com/watch?v=NoITQ4jU6jg
Function Arity is how many parameters/arguments does a function have. yo()
has 0, dude(wat)
has 1, and so on. You can query this via theFunction.length
.
console.log(yo.length) // 0
console.log(dude.length) // 1
Code language: JavaScript (javascript)
However, deafult arguments don’t count against that. Worse, if you put them first, it’ll basically say “Oh, if the first argument has a default, I guess the entire function doesn’t require any arguments, and thus has an Arity of 0”.
So our pingPure
above would have an arity of 0.
What we learned:
- Function Arity means how many parameters does a function take
- … unless you use default arguments, then arity is meaningless in JavaScript, heh
Currying
Now a fundamental question: Do default arguments affect function purity?
The answer: Yes.
While things like Strings are copied by val, like our ‘http://google.com’, the Objects/Arrays are not.
const pingPure = (url='https://jessewarden.com', fetchModule=fetch) => fetchModule(url))
Note the fetch
; where does that come from? A global or closure defined variable and it only knows that when the function is run (or defined if you use function declarations, like function pingPure
vs fat arrow functions).
So how do we make it pure, yet still allow default arguments?
Currying, also known as function currying. Currying ensures all functions take 1, and only 1, argument. To say it another way, all functions return functions until the last argument is passed; THAT is the one that actually triggers all the work.
Now functions that return functions isn’t some “new, whack” thing. For example, you can wrap things in closures that return functions:
const ping = url => fetch(url)
const pingGoogle = () => {
return ping('http://google.com')
}
Code language: JavaScript (javascript)
Now you have a function that returns a function… which returns a Prmoise. Notice how we baked the google URL into the function itself. It’s in the closure, and stored there, ready for use. That’s how currying can work in JavaScript.
You can also use it for debugging, like in the Node debug module: https://www.npmjs.com/package/debug
var debug = require('debug')('ping')
const ping = () => {
debug("Pinging Google...")
return fetch('http://google.com')
.then(result => {
debug("Done!")
})
.catch(error => {
debug("Oh, it went boom:", error)
})
}
Code language: JavaScript (javascript)
Before module systems like Node, Require.js or ES6/Webpack formalized things, people would define modules using Immediately Invoked Function Expressions, or IIFE.
(function () {
function Person(name) {
this.name = name
}
Person.prototype.sayName = function() {
console.log("this.name:", this.name)
}
return Person
})()
Code language: JavaScript (javascript)
Notice the last ()
actually invokes the function, but since the variables are local, you don’t have to worry about your Person
class accidentally destroying another, previously defined class on window
or global in Node. JQuery used to have version conflicts all the time when everyone started using it, and people would include it, and you never had an idea of which version you were using, when because of that, so IIFE fixed it.
However, none of those functions are pure; they don’t declare their dependencies in the function arguments; they’re magical closures (i.e. the debug module, or the fetch module).)
The easiest way is to use a library like Ramda or Lodash to take your existing code and curry it, but I and others now do it the manual way:
const ping = url => fetch => fetch(url)
What this means, is instead of calling it like before:
ping('http://google.com', fetch)
You have to call it a slightly weirder way:
ping('http://google.com')(fetch)
This is why Dr Axel Rauschmayer says currying is not idiomatic JavaScript. What is idiomatic JavaScript, heh?
The reason is the first parameter returns a function, the 2nd invokes the returned function. To say it another way:
const allINeedIsFetch = ping('http://google.com')
console.log(allINeedIsFetch) // function
const result = allINeedIsFetch(fetch) // Promise
Code language: JavaScript (javascript)
To do it the old fashioned way:
function ping(url) {
return function(fetch) {
return fetch(url)
}
}
Code language: JavaScript (javascript)
Yeah no, heh! Using Lodash/Ramda, you could just:
const curry = require('lodash/fp/curry')
const ping = (url, fetch) => fetch(url)
const pingCurried = curry(ping)
Code language: JavaScript (javascript)
Now, you can do either way, and both work:
pingCurried('http://google.com', fetch)
pingCurried('http://google.com')(fetch)
Code language: JavaScript (javascript)
What We Learned:
- function currying is ensuring all functions only take 1 argument
- functions will return functions until the last argument is passed, then it actually runs
- libraries like Lodash/Ramda allow you to take existing, normal comma functions and make the curried, but they still work both ways
I don’t have time to cover how this works with functions that already have default arguments, heh.
Partial Application
… so what’s a partial application?
A partial application is what a curried function returns when you didn’t call the last argument.
If we take our curried ping function:
const ping = url => fetch => fetch(url)
… and call it with both arguments:
const result = ping('http://google.com')(fetch)
console.log(result) // Promise
Code language: JavaScript (javascript)
… it runs our function and returns a Promise
.
But what happens if we call it with just 1 parameter, the url?
const wat = ping('http://google.com')
console.log(wat) // function
Code language: JavaScript (javascript)
The wat
is a function. If you log it out int he browser, it actually looks like this:
fetch => fetch(url)
Rad, right? But wait… where is this url
coming from? The closure! Same as if we did like before:
const url = 'http://google.com'
const wat = fetch => fetch(url)
Code language: JavaScript (javascript)
However, unlike the above, we don’t need a variable; the function “encloses” it, and keeps it safe, keeps it pure.
So, one could say you have a function that’s “partially applied”. The word apply comes from Category Theory where you apply functions to arguments. Basically it means, “the function takes 2 arguments, you gave us 1 of them, so take this function, and when you have the 2nd parameter ready, give it to that function I gave you, it’ll actuall do the work.”.
And THAT is how you do pure default arguments.
const pingGoogle = ping('http://google.com')
Now, you just have to supply fetch
, and she’s good to go!
pingGoogle(fetch)
Static Left, Dynamic Right
However, we always know we’re going to use fetch
, that’s kind of dumb; the only dynamic part is really the URL. As you saw above using default arguments, the dynamic stuff like URL’s typically goes to the left. The stuff you know that’s static, not changing much like fetch
which is a module most poeple in the browser use, is to the right.
It’s reversed in currying. Static left, dynamic right, like so:
const ping = fetch => url => fetch(url)
Once you do that, you can now do that thing we did above:
const pingPartial = ping(fetch)
pingPartial('http://google.com')
.then(() => pingPartial('https://jessewarden.com'))
.then(() => console.log("Pinged blog!"))
.catch(error => console.log("Failed:", error))
Code language: JavaScript (javascript)
Partial Applications With No Arguments
… but, how do you make a partial application with no arguments? It’s hard, but for now, I’ll show you the Lodash way; they have a function called partial
that makes it easy:
const partial = require('lodash/fp/partial')
const ping = (fetch, url) => fetch(url)
const pingPartial = partial(ping, [fetch])
const pingGoogle = partial(ping, [fetch, 'http://google.com'])
const pingJesse = partial(ping, [fetch, 'https://jessewarden.com'])
Code language: JavaScript (javascript)
To use:
pingGoogle()
.then(() => pingJesse())
.then(() => console.log("Pinged blog!"))
.catch(error => console.log("Failed:", error))
Code language: JavaScript (javascript)
What we learned:
- function partials are what curried functions return if you don’t give ’em all their arguments
- they allow you to use default arguments in a pure way
- if you want a partial application with no arguments, use a library like Lodash/Ramda
Conclusion
A partial application is a pure function with some or all of its arguments stored inside as a closure. If you are using default arguments a lot, it is a more pure way to accomplish that. Partial applications are created by giving only some of the required arguments to a curried function; the return value will be a partial application until all of the arguments have been given. Parameter order for default arguments tends to favor known, static parameters to the right, and unknown, dynamic data to the left. Curried functions are the opposite. These parameter orders make the function types easier to use.