DEV Community

Cover image for Introducing Metho: Safely adding superpowers to JS
Jon Randy πŸŽ–οΈ
Jon Randy πŸŽ–οΈ

Posted on • Updated on

Introducing Metho: Safely adding superpowers to JS

TL;DR

Metho allows you to easily and safely add methods in the form of dynamic properties to any object. Sounds boring, but if used to extend native types, it allows for the construction of JS expressions with a somewhat unique syntax:

// Add a range syntax to numbers
1[to(9)]  // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// Give numbers properties
13[isOdd]  // true
99[isEven]  // false
45.3[floor]  // 45
254[hex]  // 'fe'

// Repeat stuff
5[times(myFunction)]  // run myFunction 5 times

// Use with JSX
10[of(<div>Hello</div>)]  // 10 divs

// Go nuts!
'hello!'[titleCase][reverse][chunk(2)]  // ['!o', 'll', 'eH']
Enter fullscreen mode Exit fullscreen mode

Motivation/Inspiration

I recently read a similar post about creating a 'native' range syntax/method:

Whilst it had some interesting ideas, it used a syntax that didn't read very well and was kind of unsafe (monkey patching native objects). I had a few ideas for some other possible syntaxes, but wasn't sure if they would work - or even be possible. I did some experimenting, and as it turns out, they did work and could be implemented in a safe way. The techniques used could also be generalised into a flexible tool that could make many interesting syntax constructs possible.

What the... ? How on earth does this work?

Admittedly, the examples above don't even look like valid JavaScript - but they are! Numbers, Strings, and other types in JS are essentially just objects, and objects have prototypes, methods etc. that can be modified just like any other. Native types can be given new capabilities.

However, it's generally accepted that modifying these native types is not a good idea as there is no guarantee your changes will not conflict with other libraries, or future changes to JS itself. So, how do we go about building something that will have the capability to add functionality to native types using the proposed syntax, but in a safe manner?

Step 1: 'Safe' monkey patching

What if you could add a method to an object in such a way that it wouldn't conflict with any existing methods, or with future methods that might be added? Well, you can - using Symbols. These are a relatively new addition to JS, but are extremely useful. Essentially, a Symbol is a totally unique value - nothing else is equal to it, or can be ever equal to it. They are created like this:

const mySymbol = Symbol('My symbol description')
Enter fullscreen mode Exit fullscreen mode

That's it! You've created a totally unique value. The description given to the symbol is totally optional, but can be useful in debugging.

How does this benefit us? Well, Symbols can be used as object keys - giving us the ability to create methods with 'names' that are completely unique. This is how we can 'safely' monkey patch.

Step 2: 'Calling' a method without using parentheses

In the initial examples - you probably noticed that the parentheses you'd normally expect to be involved when calling methods are missing, but values are still being returned:

13[isEven]  // false
Enter fullscreen mode Exit fullscreen mode

How is this achieved? Using property getters.

We can use Object.defineProperty to define properties on an object that are not inert, but will return the result of a 'getter' function. So, to 'call' one of our unique methods without using parentheses we can define a property that is named using the Symbol and has a 'getter' function that is our method.

Step 3: Passing parameters

Unfortunately, by using a property getter we've just created a problem for ourselves. The syntax we are intending to allow:

1[to(8)]  // [1, 2, 3, 4, 5, 6, 7, 8]
Enter fullscreen mode Exit fullscreen mode

has a function call in the place where we previously had a Symbol. We effectively want to pass parameters into a 'getter' function - something that isn't possible.

I almost gave up at this point, but then I thought:

β€œWhat if to was a function that returned a symbol that was the 'name' of a method that had been dynamically created as a property getter on the target object by to, for our purposes? Then, this getter would immediately be called automatically because the returned symbol appears as the key in a bracket notation property-access of the object”

(Yes, I'm a hoot at parties)

Bingo! It worked. We 'simply' πŸ˜› wrap a dynamically created function (that has the parameters already passed in) with another function that stores it as the 'getter' for a new Symbol property on the target object, and then return the Symbol. The dynamically created method also deletes itself when called - to prevent the object filling up with these 'single-use' methods. The wrapper function then becomes our to 'method'.

Phew! If you understood that, then you're probably interested in the code from Metho that does it:

function addWithParams(target, method) {
  return(function(...args) {
    const s = Symbol()
    Object.defineProperty(target, s, {
      configurable: true,
      get: function() {
        delete target[s]
        return method.apply(this, args)
      }
    })
    return s
  })
}
Enter fullscreen mode Exit fullscreen mode

This obviously creates an additional overhead when calling methods that use this syntax, so if performance is an issue it may be better to sacrifice the nice syntax for a method stored as a regular property (something that is also possible with Metho). In the case of to - you would end up with:

1[to](3)  // [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

Using Metho

I wrote Metho to abstract away the mechanisms described above, and make it easy to focus on writing the method code. The 'range' example could be implemented as follows:

import * as Metho from 'metho'

const to = Metho.add(
  Number.prototype,
  function(end, {step} = {step: this<=end?1:-1}) {
    let arr = [], i, d = end>this
    for (i=+this; d?(i<=end):(i>=end); i+=step) arr.push(i)
    return arr
  }
)

console.log(1[to(3)])  // [1, 2, 3]
console.log(7[to(4)])  // [7, 6, 5, 4]
console.log(2[to(10, {step: 2})])  // [2, 4, 6, 8, 10]
Enter fullscreen mode Exit fullscreen mode

This is a quick and dirty example - and probably not the best implementation of the range function, but you get the idea.

Similarly, a simple 'hex' property for numbers could be implemented thus:

const hex = Metho.add(
  Number.prototype,
  function() { return this.toString(16) }
)

console.log(65535[hex]) // 'ffff'
Enter fullscreen mode Exit fullscreen mode

What's next?

The next logical step here is to build some libraries of useful extensions for the native JavaScript types. I'm trying to compile a list of functionality that would be great to have...

Ideas welcome! πŸš€

GitHub logo jonrandy / metho

A new method for methods

Top comments (20)

Collapse
 
guitarino profile image
Kirill Shestakov

How is 2[to(13)] better than range(2, 13)? Or 13[isEven] better than isEven(13)?

I see how it's an interesting syntax trickery to play around with, but I don't see how that produces anything other than confusion. It's definitely not something I'd want to see in production.

Collapse
 
lawrencedol profile image
Lawrence Dol

Completely agree. Interesting, but of no pragmatic value beyond raising an interesting idea.

Collapse
 
codingjlu profile image
codingjlu

Come on, the syntax looks cool. No, practicality isn't the point here.

Collapse
 
nombrekeff profile image
Keff

Interesting concept I must say. Quite elegant! I don't think I would use it in a real scenario, as it's not so clear how it works and would confuse people initially. Though it's fascinating, and might use it some time in one of my projects!

Collapse
 
lawrencedol profile image
Lawrence Dol • Edited

Agree that it is interesting; but it's not elegant. It's confusing, abstruse, inefficient, and has no upside over writing a simple function.

Collapse
 
iamandrewluca profile image
Andrei Luca

This is DOPE! I'm laughing so hard ))
These are JavaScript Extensions, like Swift and Kotlin extensions

Collapse
 
activenode profile image
David Lorenz

Nice idea!

Collapse
 
aminmansuri profile image
hidden_dude • Edited

Looks like Smalltalk to me.

Collapse
 
jonrandy profile image
Jon Randy πŸŽ–οΈ

Really? Haha... I've read about Smalltalk, but have never seen any code

Collapse
 
aminmansuri profile image
hidden_dude

This is what Smalltalk looks like:

1 to: 5 do: [ x | Transcript show: x]

That would "print" the numbers from 1 to 5.
Smalltalk is both OO and functional. It receives the method to:do: and you pass it a "block" (known as a "lambda" or "closure" in lesser languages) as [ ].

My theorem of languages states:

"Smalltalk was too advanced for it's time, and the last of the truly innovative languages. Most people don't understand it, but have spent the last 40 years trying to create languages that are more like it but utterly fail because they fail to adopt it's simple and intuitive syntax and the full breadth of it's features"

C++ added some OO-like features, but wasn't enough.
So Java added VMs and GC and some introspection that brought it closer to Smalltalk.
Ruby went further and added metaclasses and some of the other features.
Now JavaScript has suddenly become functional (and has "added" classes) but keeps getting messier and messier.

Maybe one day people will embrace the simple brilliant elegance of Smalltalk and put down these other messy and incomplete tools. But I'm not holding my breath.. more likely we'll end up with some lame imitation that is messy.

Thread Thread
 
lawrencedol profile image
Lawrence Dol

JS hasn't "become" functional, at all -- it was functional from it's inception. Even it's prototypical OO is functional. The function truly is, and has always been, the core abstraction of JS.

What I wish is that JS engines would recognize this and deeply optimize function closures to remove any penalty for using them as object instances.

Thread Thread
 
aminmansuri profile image
hidden_dude

Point taken.. but still a messy language when compared to Smalltalk.

Collapse
 
ignacionr profile image
Ignacio RodrΓ­guez

You keep creating things of beauty. This one seems to experiment on the connection between data and behaviour, exposing that there are more elegant ways than what was given to us as OOP.

Collapse
 
aminmansuri profile image
hidden_dude • Edited

and yet he's recreating Smalltalk syntax (and Smalltalk is the definition of what "real" OOP is)

He's using an OO language (JavaScript) with Functional style to implement another OO language's syntax

Collapse
 
superwibr profile image
superwibr

You thought of it before I could! I am currently building a library that would use symbols as semi-private methods. I must say, this is quite better than whatever I was thinking :)

Collapse
 
merthod profile image
Merthod

Just a question, what about TypeScript?

Collapse
 
jonrandy profile image
Jon Randy πŸŽ–οΈ • Edited

Not a fan of TypeScript at all, but according to some who've looked at this idea with relation to TS - it apparently doesn't play very well.

Collapse
 
jonrandy profile image
Jon Randy πŸŽ–οΈ

If you want to have a go at making it integrate well with TS though, please go right ahead! Contributions welcome

Collapse
 
codingjlu profile image
codingjlu

Quite interesting, but I can't at all see the proper motivation or benefits.

Collapse
 
polterguy profile image
Thomas Hansen

Hahahahaha :D