Partial application syntax and lambda parameters for JavaScript, inspired by Scala's
_& Kotlin'sit.
try it live on the online playground
- overview
- installation
- examples & features
- lambda parameters:
utensilList.find(it.isFork()) - argument placeholders:
add(1, _) - in assignments:
const areSameThing = _ === _ - other expressions:
it.getPower().level > 9000,const greet = `Hello, ${_}!`
- lambda parameters:
- usage
- differences between
_andit - caveats & limitations
- comparison to libraries
- see also
- development
- contributing
- license
param.macro provides two symbols — it and _.
it can be used in an expression passed to a function which implicitly creates
a lambda function in place accepting a single argument.
The _ symbol is inspired by Scala and is used as a placeholder to signal that
a function call is partially applied — the original code isn't actually called
yet, but will return a new function receiving the arguments you signified as
placeholders. Think of the values that aren't placeholders as being "bound", and
you'll provide the rest later.
Check out the examples section and the official introduction post if you'd like to see how these can be useful.
npm i --save-dev param.macroMake sure you also have Babel and babel-plugin-macros installed (the following use Babel v7, see usage for more):
npm i --save-dev @babel/cli @babel/core babel-plugin-macros... and configured with Babel:
module.exports = {
presets: [],
plugins: ['babel-plugin-macros']
}for usage without
babel-plugin-macros, see standalone plugin
Then just import and use:
import { _, it } from 'param.macro'it is also the default export, so you could also do:
import it from 'param.macro'The benefits of this explicit import are that linters and type systems won't have a fit over
_anditnot being defined. It's also self-documenting and more easily understandable. Anyone looking at your code will know that these symbols come fromparam.macro.
You can set custom identifiers for these just by using an aliased import.
import { it as IT, _ as PLACEHOLDER } from 'param.macro'or for the default it export:
import IT from 'param.macro'Scala, Kotlin, etc have what's called a lambda parameter — an easy shorthand
for passing unary (single-argument) functions to other functions (higher order).
It's useful in higher order functions like Array#map():
import it from 'param.macro'
const people = [
{ name: 'Jeff' },
{ name: 'Karen' },
{ name: 'Genevieve' }
]
people.map(it.name)
// -> ['Jeff', 'Karen', 'Genevieve']Transform this:
import { _ } from 'param.macro'
function sumOfThreeNumbers (x, y, z) {
return x + y + z
}
const oneAndTwoPlusOther = sumOfThreeNumbers(1, 2, _)... into this:
function sumOfThreeNumbers (x, y, z) {
return x + y + z
}
const oneAndTwoPlusOther = _arg => {
return sumOfThreeNumbers(1, 2, _arg)
}Most expressions using _ and it can also be used outside function calls and
assigned to a variable. Here are some ultra simple cases to demonstrate this:
import { _, it } from 'param.macro'
const identity = it
const isEqualToItself = it === it
const areSameThing = _ === _... becomes:
const identity = _it => _it
const isEqualToItself = _it2 => _it2 === _it2
const areSameThing = (_arg, _arg2) => _arg === _arg2We could implement a hasOwn() function to check if a property exists on an
object like this:
import { _ } from 'param.macro'
const hasOwn = _.hasOwnProperty(_)
const object = { flammable: true }
hasOwn(object, 'flammable')
// -> trueYou can also put these macros to use within binary expressions, template literals, and most other expressions.
import { it, _ } from 'param.macro'
const log = console.log(_)
log([0, 1, 0, 1].filter(!!it))
// -> [1, 1]
const heroes = [
{ name: 'bob', getPower () { return { level: 9001 } } },
{ name: 'joe', getPower () { return { level: 4500 } } }
]
log(heroes.find(it.getPower().level > 9000))
// -> { name: 'bob', getPower: [Function] }
const greet = `Hello, ${_}!`
log(greet('world'))
// -> Hello, world!It's especially fun to use with the pipeline operator since it basically removes the need to auto-curry an entire library's API (like Ramda), which can be pretty costly for performance.
This is a scenario specifically tested against to ensure compatibility:
import { _, it } from 'param.macro'
const add = _ + _
const tenPlusString =
it
|> parseInt(_, 10)
|> add(10, _)
|> String
tenPlusString('10') |> console.log
// -> 20module.exports = {
presets: [],
plugins: ['babel-plugin-macros']
}{
"presets": [],
"plugins": ["babel-plugin-macros"]
}A standalone version is also provided for those not already using babel-plugin-macros:
-
.babelrc.js (Babel v7)
module.exports = { presets: [], plugins: ['module:param.macro/plugin'] }
-
.babelrc (Babel v6)
{ "presets": [], "plugins": ["param.macro/plugin"] }
There are two separate constructs provided by param.macro:
_→ partial application symbolit→ implicit parameter symbol
There are a couple of major difference between the two:
_ will always traverse upward out of the nearest function call, while it will be
transformed in place. It's easiest to see when we look at a simple example:
import { _, it } from 'param.macro'
const array = [1, 2, 3]
array.map(_)
array.map(it)While these look like they might be the same, they'll come out acting very different:
const array = [1, 2, 3]
_arg => array.map(_arg)
array.map(_it => _it)it always refers to the same argument even when used multiple times in an
argument list. _ will always refer to the next argument.
import { _, it } from 'param.macro'
console.log(_ + _ + _)
console.log(it + it + it)... are compiled to:
(_arg, _arg2, _arg3) => console.log(_arg + _arg2 + _arg3)
console.log(_it => _it + _it + _it)
_is a common variable name ( eg. for lodash )
This is the most obvious potential pitfall when using this plugin. _ is
commonly used as the identifier for things like lodash's collection of utilities.
There are a few reasons this is totally fine.
-
The plugin allows for [custom symbols][#set-custom-tokens]
If you do happen to need
_oritas identifiers, you're able to change the imported symbols (using standard aliased imports) to anything you want. -
_is a common symbol for partial applicationThe Scala language uses the underscore as a placeholder for partially applied functions, and tons of JavaScript libraries have also used it — so it's become recognizable.
-
Monolithic builds of packages like lodash are on the way out
lodash v5 will be getting rid of the monolithic build in favor of explicitly imported or 'cherry-picked' utilities. So it will become less common to see the entirety of lodash imported, especially with ES module tree-shaking on the horizon.
On top of that, babel-plugin-lodash still works effectively when you just import what you need like this:
import { add } from 'lodash'
-
Partial application with
_is damn cool
Lodash, Underscore, Ramda, and other libraries have provided partial application
with a helper function something like _.partial(fn, _) which wraps the provided
function, and basically just takes advantage of the fact that {} !== {} to recognize
that the monolithic _, _.partial.placeholder, or Ramda's R.__ is a specific
object deemed a placeholder.
This Babel plugin gives you the same features at the syntax level. And on top of that, it adds features no runtime library can manage (like arbitrary expressions) and comes with zero runtime overhead. The macros are compiled away and turn into regular functions that don't have to check their arguments to see if a placeholder was provided.
- TC39 proposal – official syntactic proposal to the TC39 JavaScript standard
- babel-plugin-partial-application – precursor to this project, more features but less stable
- lodash/fp – functional adaptation of the great Lodash utility library
- Ramda – highly functional programming-oriented utility library
- babel-plugin-transform-scala-lambda – a similar plugin for more limited Scala-like lambda syntax
- Clone the repo:
git clone https://github.com/citycide/param.macro.git - Move into the new directory:
cd param.macro - Install dependencies:
npm install - Link the project to itself:
npm link && npm link param.macro - Build the source:
npm run build - Run tests:
npm test
this project uses itself in its source, so take note of the
npm linkstep since it's necessary to build or test
Pull requests and any issues found are always welcome.
- Fork the project, and preferably create a branch named something like
feat-make-better - Follow the build steps above but using your forked repo
- Modify the source files in the
srcdirectory as needed - Make sure all tests continue to pass, and it never hurts to have more tests
- Push & pull request! 🎉
MIT © Bo Lingen / citycide