Git Product home page Git Product logo

param.macro's Introduction

param.macro · Version License JavaScript Standard Style

Partial application syntax and lambda parameters for JavaScript, inspired by Scala's _ & Kotlin's it. Read more about this macro in the intro blog post "Partial Application & Lambda Parameter Syntax for JavaScript".

try it live on the online playground


overview

param.macro provides two main 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.

installation

yarn add --dev param.macro

Make sure you also have Babel and babel-plugin-macros installed (the following use Babel v7, see usage for more):

yarn add --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 _ and it not being defined. It's also self-documenting and more easily understandable. Anyone looking at your code will know that these symbols come from param.macro.

set custom tokens

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'

examples

lambda parameters

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']

argument placeholders

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)
}

_ and it in assignments

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 === _arg2

We could implement a hasOwn() function to check if a property exists on an object like this:

import { it, _ } from 'param.macro'

const hasOwn = it.hasOwnProperty(_)
const object = { flammable: true }

hasOwn(object, 'flammable')
// -> true

other expressions

You 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
// -> 20

lift modifier

In addition to _ and it, there is a third symbol exported by param.macro called lift. In most scenarios it is simply removed from the output but is very useful in combination with _ placeholders.

Because it creates only unary functions in place and _ always traverses out of its nearest parent function call, lift serves as an operator that fills out the middle ground: using placeholders to create inline functions of any arity.

With _ alone, the following example will not do what you probably want:

import { _ } from 'param.macro'

const array = [1, 2, 3, 4, 5]
const sum = array.reduce(_ + _)

Because it produces this:

const array = [1, 2, 3, 4, 5]
const sum = (_arg, _arg2) => {
  return array.reduce(_arg + _arg2)
}

To actually pass in an implicit binary function with _ you can use the lift operator:

import { _, lift } from 'param.macro'

const array = [1, 2, 3, 4, 5]
const sum = array.reduce(lift(_ + _))
console.log(sum)
// -> 15

It may be helpful to note that _ is still following its own rules here: it traversed upward out of its parent function call! It just so happens that call is removed afterward leaving your new function exactly where you want it.

usage

.babelrc.js (Babel v7)

module.exports = {
  presets: [],
  plugins: ['babel-plugin-macros']
}

.babelrc (Babel v6)

{
  "presets": [],
  "plugins": ["babel-plugin-macros"]
}

standalone plugin

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"]
    }

differences between _ and it

There are two main & distinct constructs provided by param.macro:

  • _ → partial application symbol
  • it → implicit parameter symbol

There are a couple of major differences between the two:

scoping

_ 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)

An exception to these scoping differences is at the top-level, like the right-hand side of an assignment. it and _ behave similarly here since there's no further upward to go, so they'll both happen to target the same place.

For example the following two map implementations do the same thing:

import { _, it } from 'param.macro'

const map1 = _.map(_)
const map2 = it.map(_)

However, if nested deeper inside a function call the object placeholder _ in map1 above would traverse further upward than an it would, and create a separate function first, before the argument placeholder _ inside the method call itself. This creates an unary method call instead of the implicit binary function we probably wanted, lift or not.

The it implementation in map2 above does still create the implicit binary function, even if nested deeper. And following the normal placeholder rules, any _ inside the method call will traverse up to the method call and stop to create a function there, as we wanted.

argument reuse

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)

caveats & limitations

_ 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.

  1. The plugin allows for custom symbols

    If you do happen to need _ or it as identifiers, you're able to change the imported symbols (using standard aliased imports) to anything you want.

  2. _ is a common symbol for partial application

    The 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.

  3. 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'
  4. Partial application with _ is damn cool

comparison to libraries

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.

see also

development

  1. Clone the repo: git clone https://github.com/citycide/param.macro.git
  2. Move into the new directory: cd param.macro
  3. Install dependencies: yarn or npm install
  4. Build the source: yarn build or npm run build
  5. Run tests: yarn test or npm test

this project uses itself in its source, so you can use param.macro while you develop param.macro (... yo dawg)

contributing

Pull requests and any issues found are always welcome.

  1. Fork the project, and preferably create a branch named something like feat-make-better
  2. Follow the build steps above but using your forked repo
  3. Modify the source files in the src directory as needed
  4. Make sure all tests continue to pass, and it never hurts to have more tests
  5. Push & pull request! 🎉

license

MIT © Bo Lingen / haltcase

param.macro's People

Contributors

5310 avatar andarist avatar haltcase avatar romankrru avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

param.macro's Issues

How to create simple implicit functions

Hello,

Thanks for your macros, they're great.
I was wondering if it is possible, and if it is how, can I make implicit binary functions. Something in between _ and it

[1,2,3,4].reduce( _ + _)

The expected output would be

[1,2,3,4].reduce( (arg1, arg2) => arg1 + arg2 )

But instead I get:

;(_arg2, _arg3) => {
  return [1, 2, 3, 4].reduce(_arg2 + _arg3)
}

Regards

Placeholders: stop upward traversal at `ObjectProperty`

I'd like input on this one in terms of what's expected. I'm proposing that when placeholders are used in the value of an ObjectProperty, upward traversal will stop at that object property, causing the expression to be transformed essentially in place rather than wrapping the CallExpression further up.

input:

doSomething({
  key: _ + _
})

current:

;(_arg, _arg2) => {
  return doSomething({
    key: _arg + _arg2
  })
}

proposed:

doSomething({
  key: (_arg, _arg2) => {
    return _arg + _arg2
  }
})

The current transformation seems like it allows for too much complexity in my opinion, like in this example:

getNameByIdAsync(id)
  .then(doSomething({
    name: _,
    /*...*/
  }))

Lift curries implicit binary function

I encountered an instance where I think lift() isn't working as expected. Instead of lifting a binary function, it curries it!

import { _, it, lift } from 'param.macro'

const swap = (a, b) => _(b, a)

const map1 = _.map(_)  
const map2 = swap(lift(_.map(_))) // lift curries ✘

console.log(map1([0, 1, 2], lift(_ * 10))) // [0, 10, 20] ✔
console.log(map2(lift(_ * 10), [0, 1, 2])) // [function]  ✘

[Playground link]

I am expecting map2 to look like this:

const map2 = swap((_arg2, _arg3) => {
  return _arg2.map(_arg3)
})

But it returns as this:

const map2 = swap(_arg4 => {
  return _arg5 => {
    return _arg4.map(_arg5)
  }
})

Is this intended?

On bootstrapping (using param.macro in its own source)

NOTE: this issue has no bearing on users of this package, and is strictly an internal development concern.

lay of the land

param.macro has used itself in its source since its initial release. npm packages can't depend on themselves so it does this by leveraging npm link && npm link param.macro to bootstrap itself. Recently there was a build step upgrade put forward by @Andarist to make this more pleasant with a CLI tool called npm-self-link

the problem

While it's cool to be able to use param.macro on itself and dogfood the product, it doesn't come without issues.

  • dependence on locally built sources

    Because we depend on the locally built sources, development can become a train wreck when you make changes that break stuff. Now, your local build won't build your new sources correctly or will fail entirely. One solution is to scrap all changes to the dist built files and rebuild, but even this can fail in certain edge cases.

  • built files have to be included in source control

    The dist directory contains the build output and has to be included in the repo. If it isn't, there's nothing to bootstrap from in a fresh clone. This means we have to ensure that the remote dist files are up to date & working whenever changes are made.

the way way forward

As I see it, there are 3 4 options to work with or around this.

  1. keep the status quo

    Don't change anything and deal with the current state, which has led even me to frustration while trying to make fixes or add features.

  2. maintain a known functional & compatible source for the distributed files

    This is similar to the current method but rather than using a local build to bootstrap, we'd need to use a remote build that's able to compile the local sources. This still leads to the maintenance burden of keeping this up to date, and param.macro's source likely just isn't large enough to justify this burden.

  3. ditch bootstrapping entirely

    I'm pretty sure this is the option I'm going with. It's been a fun experiment but I don't think it's worth the cost — we'll spend as much or more time making sure bootstrapping works as we will working on the actual library. And the first time any potential contributor tries to work on the library and runs into a broken local bootstrap it's probably too late (it's already hit me a couple times).

  4. just add a dev dependency derp

    npm doesn't allow an explicit npm i [-D] param.macro while in param.macro, but it doesn't prevent installing that same dependency if you just manually add it and run an install afterward. Yarn allows both.

discuss

Feel free to discuss. Soon I'll probably move on going with option 3 and ditch bootstrapping. Unfortunately this makes the recent build step improvements moot, so I do have to apologize to @Andarist who contributed that and kickstarted npm-self-link. I'm sure I'll find other uses for the package at some point, though.

Edit: Given the revelation behind option 4, it'd be pretty simple to just go with that.

Edit 2: Option 4 is implemented.

Allow & evaluate future syntax in the playground (ie. pipeline)

I want the playground to handle stage-x proposals so we can, for example, test out interop with the pipeline operator |>. For most current proposals this is already doable if I add their plugins to the babel-standalone configs, but pipeline operator is missing from babel-standalone.

I opened up a pull request to add it - once that gets cut to a release I'll update the playground.

Seem not work with styled-components

This works

const getColor = it.color
const ActionName = styled.div`
  text-align: center;
  font-weight: bold;
  font-size: 16px;
  color: ${getColor};
  border: 1px solid ${getColor};
  padding: 5px;
`;
// same as
const ActionName = styled.div`
  text-align: center;
  font-weight: bold;
  font-size: 16px;
  color: ${({ color }) => color};
  border: 1px solid ${({ color }) => color};
  padding: 5px;
`;

And this don't:

const ActionName = styled.div`
  text-align: center;
  font-weight: bold;
  font-size: 16px;
  color: ${it.color};
  border: 1px solid ${it.color};
  padding: 5px;
`;

Any possible reason?

Upgrade to babel-plugin-macros

The project was renamed from babel-macros to babel-plugin-macros to better support babel v7. No other breaking changes.

Which pipe plugin are you using ?

Hello,

I'm trying your code and I see that it does not works with any pipe operator. I'm using babel-plugin-pipe-operator-curry

and with the following code:

const add = _ + _
const tenPlusString =
  it
  | parseInt(_, 10)
  | add(10, _)
  | String

I get the following compiled code which is wrong:

const add = (_arg, _arg2) => {
  return _arg + _arg2;
};

const tenPlusString = (_it, _arg3, _arg4) => {
  return String(add(10, _arg4)(parseInt(_arg3, 10)(_it)));
};

Could you please specify with which plugin are you compatible ?
Regards

`it` with assignment

Is something like this not possible?

foo.forEach(it.bar = true)

The output of that is:

foo.forEach(
  (_it2.bar = _it2 => {
    return true
  })
)

Which produces an error.

I was expecting it to compile into this:

foo.forEach(_it2 => {
  return _it2.bar = true
})

Seems not working with pipeline-operator + template literal

function getTachyonsCSS(shorthand) {
	return shorthand |> snakeCase |> tachyons[_] |> toPairs |> flatten |> it.join(': ') |> `${_};`;
}

this throws
PartialError: param.macro: Placeholders must be used as function arguments or the right side of a variable declaration
while this works fine:

const getTachyons = tachyons[_];
const addColon = it.join(': ');
const addSemi = `${_};`;
function getTachyonsCSS(shorthand) {
	return shorthand |> snakeCase |> getTachyons |> toPairs |> flatten |> addColon |> addSemi;
}

How to deal with it?

Include tail paths inside the wrapper function

input:

import { _ } from 'param.macro' 
const fn = String(_).toUpperCase() === 2

current:

const fn = (_arg => {
  return String(_arg)
}).toUpperCase() === 2

expected:

const fn = _arg => {
  return String(_arg).toUpperCase() === 2
}

Rest/spread placeholder

prior art

These are both roughly equivalent to:

const log = (..._arg) => console.log('hi', ..._arg)

param.macro

input:

import { _ } from 'param.macro'
const log = console.log(..._)

current output:

const log = (_arg) => {
  return console.log(..._arg);
};

This ends up having to be called with an array (or other spreadable object) to work, ie. log([1, 2, 3]). Otherwise it throws an error.

proposed output:

const log = (..._arg) => {
  return console.log(..._arg);
};

This would be a breaking change at this point so it'd have to drop in v2.0.0 - but probably should happen.

TypeError: Cannot read property 'data' of undefined

Quickly tried out param.macro in a create-react-app application we're working on, but i can't get the plugin to work so far. I like the idea a lot though!

.babelrc

{
  "presets": [],
  "plugins": ["babel-plugin-macros"]
}

Trying it with a Promise, gives an Uncaught (in promise) TypeError: Cannot read property 'data' of undefined error.

import { it } from 'param.macro'
// ...

async initialize() {
  const vertexShader = await axios.get(vertexSource).then(it.data)

  // this works
  // const vertexShader = await axios.get(vertexSource).then(e => e.data)
}

Maybe i'm doing something wrong here. Also tried out mapping over a simple array
[{a:11},{a:22}].map(it.a), which gives the same TypeError.

-Edit- console.log(it) returns undefined as well i see now, so maybe it's caused by the create-react-app way of compiling code.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.