Skip to content

Instantly share code, notes, and snippets.

@brainysmurf
Last active April 3, 2024 18:17
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save brainysmurf/35c901bae6e33a52e3abcea720a6b515 to your computer and use it in GitHub Desktop.
Save brainysmurf/35c901bae6e33a52e3abcea720a6b515 to your computer and use it in GitHub Desktop.
Things one can get used to for the V8 Google Apps Scripts engine

Things one can get used to for the V8 Google Apps Scripts engine

A bit of a monologue about various syntax changes and adjustments in learning and using the tool.

Logging is slooooooow

I know. Everyone knows.

That's why I wrote a library that writes to a spreadsheet instead. Ever since I started using it I'm far more productive. I don't know how people are still putting with that god-awful lagging logger.

The font of knowledge

If you want to see the details of changes to the language, in all of its geeky details, head on over to here.

Note that modules are not included, so all that import goodness doesn't apply to the V8 runtime.

String literals

I won't go through normal, everyday usage, but will instead highlight what might be less common but quite handy when needed.

const noun = 'World';
const hello = `Hello ${noun}`;

I want to do generalize something like this to work in any case:

function output (string, noun) {
    Logger.log( /* `Hello ${noun}` */ )
}
function entrypoint () {
    const string = 'Hello ${noun}';  // just a regular string
    output(string, 'World');
}

In other words, use a normal string, pass it some other part of code that can take the ${var} pattern and apply it there. Why do I want this? Not only because it is cool, but because it allows me to de-couple code. I can define the string template in one place, but apply the template in another.

The function I use for this ability is here, which I totally didn't write myself:

function interpolate (targetString, params) {
    const names = Object.keys(params);
    const vals = Object.values(params);
    return new Function(...names, `return \`${targetString}\`;`)(...vals);
}

It is used like so:

const output = interpolate('${greeting} ${noun}', {greeting: 'Hello', noun: 'World'});
Logger.log(output);  // "Hello World"

Understanding the mechanics of how interpolate works is beyond the scope of this gist, and beyond what you really need to use it (although it's fun once you've groked it).

In any case, now you can define a template in one place and then apply it in another.

Arrow functions are nice, but learn what happens with this

Arrow functions are fun and dandy, but there are some serious side effects around, and if you're not aware of them can make debugging very frustrating.

The main gotcha has to do with the behaviour of the this keyword inside one of these babies. For long-form function myFunction() {} function, inside the body the this is referring to itself, which is different from what this is outside of the function.

For an arrow function, the this value is the same both inside and outside of it.

Upshot, if you aren't using this anywhere, you don't need to know much about it, but then you won't be able to use classes. (You do want to use classes, see below.)

Weird syntax thingies with arrow functions

Weird syntax thingy #1: No parameters

You have two choices when an arrow function does not have any parameters:

  • Empty parentheses
  • Use the underscore _ variable
// empty parentheses:
() => {}
// underscore variable
_ => {}

I prefer the latter, because Javascript has enough parentheses as it is, and it usually hurts my eyes whenever I see it.

And it should be noted that using a single _ is no special meaning in JavaScript: It is actually a legal variable name. In many languages, the _ is used to refer to a placeholder variable that is never used. So in this case, we are actually defining an arrow function with one parameter; it is just never used.

Weird syntax thingy #2:

Arrow functions can optionally have a {} block, in which case you don't have a return statement. But if you return an object, we have a confusing syntax:

_ => {obj: 'obj'}

That looks a lot like a body {} rather than an object literal. So, you're supposed to use parentheses (told you there are a lot of parentheses) instead:

_ => ({obj: 'obj'})

Sigh.

Use const or let instead of var

There is nothing on its own wrong with var, but I think it's worth "overcompensating" when transitioning to V8 and to exclusively use const or let instead for a while, to get the hang of it.

The massive difference between const/let and var is the scope. The former obeys its scoping rules according to the nearest curly braces block, and the latter obeys its scoping rules to the enclosing function.

Any variable made with var has an additional oddity in that it is "hoisted" -- and odd it is. If you haven't learned what hoisted is referring to but have been scripting happily along without knowing much about it, then that is a case in point to use const/letinstead.

When to use const

Whenever you make a variable that will not be reassigned to some value. It is important to understand that if the value itself is an object where assignments make sense (such as objects) you can still use assignment statements on those, just not on the variable immediately after the const.

When to use let

Whenever you make a variable that could be reassigned to some value. It doesn't have to be reassigned, but if there is no possibility of it, use const instead.

const with try/catch

There is an interesting pattern that can be used which I quite like. Here's the problem:

function getResponseFromUrl(url) {
    try {
       const response = UrlFetchApp.fetch(url); 
    } catch (e) {
        Logger.log("Oh no, error");
    }
    return response;  // fails
}

What's the problem? Well, const response is defined inside the try {} area, and is used again in the return statement outside of it. You can't do that, because const variables obey rules according to the immediate curly braces.

You could do this:

function getResponseFromUrl(url) {
    let response;
    try {
       response = UrlFetchApp.fetch(url); 
    } catch (e) {
        Logger.log("Oh no, error");
    }
    return response;  // okay
}

That's just fine. But I much prefer this method:

function getResponseFromUrl(url) {
    const response = (_ => {
       return UrlFetchApp.fetch(url); 
    } catch (e) {
        Logger.log("Oh no, error");
    }();
    
    return response;  // okay
}

What the heck is going on there?

First of all one thing we have is a self-invoking function. You can do that:

(function log (text) {
    Logger.log(text);
})("Hello World");

That calls the function log immediately after being created, with the value "Hello World"as text. The arrow function equivalent is:

(text => {
    Logger.log(text)
})('Hello World');

Which means we can get around the earlier const problem by doing this:

const variable = (_ {
    return 'value';
})();
Logger.log(variable);

The (_ is confusing at first, but it's actually the _ representing that there is no parameter, with a ( enclosing the function until ), there it then calls itself with ().

Kinda cool.

Destructuring

I think of destructuring as bulk re-assignment. The syntax is hard to get used to, but it results in values being assigned. It's a bit weird, but here goes:

const bart = 'Bart Simpson';
const [firstName, lastName] = bart.split(' ');
firstName; // = 'Bart';
lastName;  // = 'Simpson';

That's what it is for array assignments, but you can also do for object assignments. But when it comes to using objects, I think it more as a re-mapping of variables to values.

const {log: __log__} = Logger;
__log__('Hello World');  // same as Logger.log('Hello World');

What's happening here? It's assigning the variable log to Logger.log by evaluating it as an object reference. So clear right? I actually think that's kinda awful. Why not just do this?

const __log__ = Logger.log;

I like using __log__ for this, although you could just as easily use log, but _ can be part of variable names and it's easy to find them again after you've finished debugging.

Destructuring is also useful for objects. The syntax follows the same pattern: You can make new variables and assign it values at the same time.

const obj = {greeting: 'Hello', name: 'World'};
const {greeting: hello, name: world} = obj;
Logger.log(hello + world);  // "HelloWorld"

But why? Isn't using destructuring with arrays easier to read?

const obj = {greeting: 'Hello', name: 'World'};
const [hello, world] = [ obj.greeting, obj.name ];
Logger.log(hello + world);  // "HelloWorld"

I think so anyway.

Default values

If an object doesn't have the key, you can assign default values too. This is best to understand in the context of a function definition as settings:

function DoSomethingUseful(settings) {
    const {verbose=false: verbose, log=true: log} = settings
    if (verbose && log) {
        Logger.log('Hello World');
    }
}

const obj = {verbose: true};
DoSomethingUseful(obj);

Shortcuts

With destructuring, if the key and the variable name are identical, you don't have to type it twice. The above could be rewritten like so:

function DoSomethingUseful(settings) {
    const {verbose=false, log=true} = settings
    // verbose and log variables are defined!
    
    if (verbose && log) {
        Logger.log('Hello World');
    }
}

const obj = {verbose: true};
DoSomethingUseful(obj);

If you think this is cool, check out named parameters, but we're better off spending some some with "spreading" things.

The "spread" ellipsis ...

The ... in code has actually more than one use but shares the same syntax. One thing it does is converts a variable that has a name, say args into a list that is populated with assigned values, which I think of as "remaining":

const [one, two, ...remaining] = [1, 2, 3, 4, 5];
Logger.log(remaining);  // [3, 4, 5]

Another sort of "remaining" usage, but this time for objects:

const {one, two, ...remaining} = {one: 1, two: 2, three: 3, four: 4};
Logger.log(remaining);  // {three: 3, four: 4}

When applying values, that's when I think of it as "spreading."

function add (a, b) {
    return a + b;
}
const args = [1, 2];
add(...args);

Named parameters for functions

Full disclosure: I am a Pythonista first and foremost, which means I have very strongly influenced by Python and its ecosystem. This section is a good example of that. I think it is very grand to be able to give names to the parameters instead of by position. So much more readable. So much better.

So this is cool, but unsophisticated, and we should make it better:

function UnsophisticatedFunction(greeting="Hello", name="World") {
    Logger.log(greeting + name);
}

UnsophisticatedFunction('Hello', 'World');  
// ^--- outputs "Hello World"
UnsophisticatedFunction(greeting='Hello', name='World');
// ^--- outputs "Hello World"

UnsophisticatedFunction(name='World', greeting='Hello');
// ^--- outputs "World Hello" BOOOO

That last one is just dumb. But we can use destructuring to our advantage!

function SophisticatedFunction({greeting="Hello", name="World"}) {
    Logger.log(greeting + name);
}

SophisticatedFunction({greeting='Hello', name='World'});
// ^--- outputs "Hello World"

SophisticatedFunction({name='World', greeting='Hello'});
// ^--- outputs "Hello World"

It works much better that way eh? Easier to read eh? What happens if we invoke with no parameters?

SophisticatedFunction();
// ^--- "TypeError: Cannot destructure property `greeting` of 'undefined' or 'null'"

That's happening because you're passing nothing as a parameter and it's trying to destructuring on nothing. No sweat, let's change the function declaration to avoid this:

function EvenMoreSophisticatedFunction({greeting="Hello", name="World"}={}) {
    Logger.log(greeting + name);
}

The difference is in the ={} part of the parameter list. If nothing is passed, it makes it an empty object {} by default, which makes destructuring a-ok.

You can mix positional parameters and named parameters:

function Mixed(one='Hello', two='World', {greeting="Hello", name="World"}={}) {
    Logger.log(one, two);
}

But that to me defeats the purpose of named parameters. Exception: If the name of the function gives away what the parameter is doing:

function doSomethingById(id, {greeting="Hello", noun="World"}={}) {
    Logger.log(id);
}

How about requiring certain named parameters?

You may wish a function that is using named parameters to require that a certain property be required to be passed. For example, in our hello world example, ensure that something is passed for greeting and noun. This is simple to do without named parameters:

function HelloWorld (greeting, noun) {
    Logger.log(greeting + noun);
}
HelloWorld('Hello')  // fails, which is what we want. yay

There is a way, and it borrows the idea of "interfaces" (a concept in other languages) to implement. It's kinda involved, though, which is why I wrote a library for it.

But what about keyword arguments (kwargs)?

What if you want to accept some parameters with default values, but the function can also accept any remaining values, which is then an object?

You can! Do it like this:

function MostSophisticated({greeting="Hello", noun="World", ...kwargs}) {
    Logger.log(kwargs);  // object
}

No kwargs will not be an array, it will be an object with properties. Yes, because the ... here just means "remaining."

Nested named parameters?

Just don't. I'm not going to explain why it doesn't work very well, but it's very ugly and not worth it.

Classes

Get to know how to classes in JavaScript. Not only are they useful but they are powerful, and it's likely that others will start using them throughout their own code too.

A class is like a function that creates objects that have functions, oh and state

Classes allow us to create things that are very expressive and useful. To see how useful they are, let's first just look at how we can play around with functions with them. Let's construct a nonsense "Hello World" class.

class App {
    constructor (value) {
        this.value = value;
    }
    
    log () {
        Logger.log(this.value);
    }
    
    get getter () {
        return this.value;
    }
    
    set setter (value) {
        this.value = value;
    }
    
    static createHello () {
        return new App('Hello World');
    }
}

You can think of a class as a kind of blueprint that declares how an object behaves, and there's a way to create "instances" of these objects built on the blueprint. Or you can think of a class as a function that has a bunch of functions contained inside, and holds state.

In the above example the only state that it holds is value, making it not very useful. State is initialized by the constructor function, which is invoked when you call the App variable the same way you would a function:

const instance = App('a value, any value');  
// ^--- executes this.value = value;

So that class is called like a function, which returns a thing with some functions defined, and state. We can interact with it according to the blueprint:

instance.log();
// ^--- executes Logger.log(this.value)

The blueprint says that "log is a function with no parameters, which can be invoked with .get."

The next two items on the blueprints uses the getter/setter pattern, where get and set preceded the name of the function. The concept of a getter and setter is that you don't actually use them as a function, though.

instance.setter = 'Bonjour';  
// ^--- executes this.value = 'Bonjour';

const value = instance.getter  // value now equals "Bonjour"

Instead of using them as functions, you use assignment statements (or as part of an expression.)

The final piece of blueprint says that we have a function on the class itself. It says that we have a function called createHello which we can invoke by using dot notation on the class variable App.

const instance = App.createHello(); 
// ^--- executes return new App("Hello World");

In this way, we have created an instance of our little app which will output "Hello World" when we use instance.log()

A function that is on the class itself might be called a "class method" in other languages; the others would be "instance methods." Having this distinction is useful. Instance methods change or retrieve state, or do calculations depending on the state. Whereas class methods are useful when you do something that doesn't depend on state.

What I really like is making instances with the class methods, in what might be called a "convenience method." In the example above, we use the class method to make an instance of the class that contains "Hello World" in the state.

You can use this convenience method pattern to avoid having to use the new keyword, which you have to do with classes. Sorta cool:

class App {
    static new () {
        return new App();
    }
}

const app = App.new();

There are actually a few of these class methods in common use for built-in objects. Do these look familiar?

Object.create({});
// ^--- makes an duplicate object

Array.from('Hello World');  
// ^--- makes an array of characters: ['H', 'e' ...]

Class methods that are also getters

I really like this as a design pattern:

class Utils {
    power (base, exponent) {
        return base ** exponent;
    }
}

class App {
    constructor () {
        this.value = 1;
    }
    
    static get utils () {
        return new Utils();
    }
}

const app = new App();
const four = App.utils.power(2, 2);  // returns 4

We'll take advantage of this pattern to solve a confusing change from Rhino to V8 below.

The Global Context

There is a major change in the global context. It's really confusing, but the change was a necessary one. In Rhino, you could use variables between files that were defined in the global context, and it was all good. I suppose it was done that way in order to make it easier to get started. In V8, that's all broken.

Why? It has to do with the files that are on the project and how they are parsed. The technical details are boring. What's really interesting is figuring out how to compensate without having to know all the niggly details.

In the end, though, we'll have a design pattern we can use to our advantage.

Let's keep it intuitive and learn by rule-making, and so here goes:

Any code that is executed before the endpoint is called should not refer to a function / variable in another file, unless it gets executed after the endpoint has been called

What the heck are you going on about. Let's break it down:

"After the endpoint is called"

What's the endpoint? In the online editor, that's the function that you click play on. Or it's the onEdit function that gets executed as a result of an edit in the spreadsheet. Or it's the function that is exposed via the library mechanism. Just whatever gets called is the endpoint.

So you click the run button with Endpoint selected:

// before the endpoint is called
function Endpoint() {
    // after the endpoint is called
}
// code here is also executed before endpoint is called

What about the case with more than one file? Let's see

// Code.gs:
const before = 'before endpoint';
function Endpoint () {
    const after = 'after endpoint';
}
const afterButBefore = 'before endpoint too';

// Business.gs
const stillBefore = 'still before endpoint';
function NotTheEndpoint () {
    const never = 'never gets here';
}
const alsoStillBefore = 'yes even here';

So, between those files, we can't really refer to variables or functions between the two (we'll see an exception in a minute).

Now we know what "the endpoint" is talking about, but also why it's the pivotal thing to think about for the next bit.

Why say "should not" instead of "can not"?

Because, depending on rules not worth knowing, it might work in some cases. But it has to do with the order in which the files are parsed. So it depends, but let's just nevermind all that. So, I'm being really literal in my rule-making. (Can you tell I'm a programmer?)

What's the illusive exception you referred to?

Glad you asked. It has to do with classes!

Check out the following code:

// App.gs
class App {
    constructor () {}
    
    get module () {
        return new Module();
        // ^--- does this work?
    }
}

// Module.gs
class Module {
    log (text) {
        Logger.log(text);
    }
}

// Code.gs
function Endpoint () {
    const app = App();
    app.module.log('Hello World');    
}

This gets us to the "unless" part of the rule above. By all appearances, you might be thinking that the new Module() won't work because it's in the "before the endpoint is called" area. Alas, it is not. That code gets executed well after the endpoint is called, in fact, and in this way you can organize your project accordingly.

This pattern, which is a form of composition, allows for the creation of files that does some aspec of the application logic, and also allow you the developer to keep various functions organized in files as you'd like them to be.

@zesertebe
Copy link

and..... private methods? :C

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment