Saturday, September 30, 2017
Friday, September 29, 2017
Building a Slack Bot Using Node.js
Slack is quickly becoming the new industry standard for teams to communicate with. In fact, it is so popular that when I typed slack into Google, as I expected, the first result was the definition of the word from the dictionary. This was followed immediately by Slack's website!
This is almost unheard of for most common words in the English dictionary. Usually, Google's definition is followed by several links to the top dictionary websites.
What is Slack?
At its most basic, Slack is a messaging system. It allows for direct messages to team members and the creating of channels (private or public) that allow easy real-time team communication and collaboration. For more information on Slack, you can view Slack's Features.
At this point, you might be wondering where Node.js comes in. As I mentioned, at its most basic, Slack is a messaging system; however, it can be infinitely extended and customized. Slack provides an incredibly flexible system to customize your team's integration, including:
- creating custom welcome messages
- creating custom emojis
- installing third-party applications
- creating your own applications
- creating custom Slack Bots
In this article, I am going to demonstrate how to create a Slack Bot with Node.js that can be added to your team's Slack configuration.
Slack Bots Defined
A Slack Bot's job is to receive events sent from Slack and handle them. There are a plethora of events that will be sent to your Bot, and this is where Node.js will come in. We must decide not only which events to handle, but how to handle each individual event.
For example, some common events that a Bot would handle are:
member_joined_channel
member_left_channel
message
In this article, I will create a Node.js application and a Slack Bot that can be added to your team project to perform specific actions based on the events it receives.
To begin, I need to create a Bot on Slack. Two types of bots can be created:
- a custom bot
- creating an application and adding a bot user
This article will create a custom bot because an application bot user would be more appropriate if you were planning to write and publish an application on Slack. Given that I wish this bot to be private to my team, a custom bot will suffice.
Creating a Custom Slack Bot
A custom bot can be created here: http://ift.tt/2fVYiPf. If you are already logged in to your Slack account, on the left select the Add Configuration button; otherwise, log in to your Slack account before proceeding. If you do not have a Slack account, you can sign up for free.
This will take you to a new page that requires you to provide a username for your bot. Enter your username now, ensuring you follow Slack's naming guidelines. Once you have selected an awesome bot name, press Add bot configuration.
After you have successfully created your bot, Slack redirects you to a page that allows for further customization of your bot. I'll leave that part to your creative self. The only thing needed from this page is the API Token that starts with xoxb-
. I would either copy this token to a safe place for later use or simply leave this page open until we need the token for the Node.js application.
Configurations
Before moving on to code, two more Slack configurations are required:
- Create or choose an existing channel that your bot will interact with. While I'm testing my new bot, I chose to create a new channel. Be sure to remember the channel name as you will need it within your application shortly.
- Add/Invite your bot to the channel so it can interact with it.
Now that I've got my Slack Bot configured, it's time to move on to the Node.js application. If you already have Node.js installed, you can move on to the next step. If you do not have Node.js installed, I suggest you begin by visiting the Node.js Download page and selecting the installer for your system.
For my Slack Bot, I am going to create a new Node.js application by running through the npm init
process. With a command prompt that is set to where you wish your application to be installed, you can run the following commands:
mkdir slackbot cd slackbot npm init
If you are unfamiliar with npm init
, this launches a utility to help you configure your new project. The first thing it asks is the name. It defaulted mine to slackbot
, which I'm comfortable with. If you would like to change your application name, now is the chance; otherwise, press Enter to proceed to the next configuration step. The next options are version and description. I've left both as the default and simply continued by pressing Enter for both of these options.
Entry Points
The next thing that is asked for is the entry point. This defaults to index.js
; however, many people like to use app.js
. I do not wish to enter this debate, and given my application will not require an intensive project structure, I am going to leave mine as the default of index.js
.
After you've recovered from a debate that is probably as strong as tabs vs. spaces, the configuration continues, asking several more questions:
- test command
- git repository
- keywords
- author
- license
For the purposes of this article, I've left all options as their default. Finally, once all options have been configured, a confirmation of the package.json
file is displayed prior to creating it. Press Enter to complete the configuration.
Enter the SDK
To make interacting with Slack easier, I'm also going to install the Slack Developer Kit package as follows:
npm install @slack/client --save
Are you finally ready for some code? I sure am. To begin, I'm going to use the example code from the Slack Developer Kit's website that posts a Slack message using the Real-Time Messaging API (RTM) with a few tweaks.
Given that the entry point I chose was index.js
, it's time to create this file. The example from the Slack Developer Kit's website is roughly 20 lines of code. I'm going to break it down several lines at a time, only to allow for explanations of what these lines are doing. But please note that all these lines should be contained in your index.js
file.
The code begins by including two modules from the Slack Developer Kit:
var RtmClient = require('@slack/client').RtmClient; var CLIENT_EVENTS = require('@slack/client').CLIENT_EVENTS;
The RtmClient
, once instantiated, will be our bot object that references the RTM API. The CLIENT_EVENTS
are the events that the bot will be listening for.
Once these modules are included, it's time to instantiate and start the bot:
var rtm = new RtmClient('xoxb-*************************************'); rtm.start();
Be sure to replace the API Token that is obfuscated above with your token obtained during the Slack Bot creation.
Calling the start
function on my RtmClient
will initialize the bot's session. This will attempt to authenticate my bot. When my bot has successfully connected to Slack, events will be sent allowing my application to proceed. These events will be shown momentarily.
With the client instantiated, a channel
variable is created to be populated momentarily inside one of the CLIENT_EVENTS
events.
let channel;
The channel
variable will be used to perform specific actions, such as sending a message to the channel the bot is connected to.
When the RTM Session is started (rtm.start();
) and given a valid API Token for the bot, an RTM.AUTHENTICATED
message will be sent. The next several lines listen for this event:
rtm.on(CLIENT_EVENTS.RTM.AUTHENTICATED, (rtmStartData) => { for (const c of rtmStartData.channels) { if (c.is_member && c.name ==='jamiestestchannel') { channel = c.id } } console.log(`Logged in as ${rtmStartData.self.name} of team ${rtmStartData.team.name}`); });
When the RTM.AUTHENTICATED
event is received, the preceding code performs a for
loop through the list of Slack team channels. In my case, I'm specifically looking for jamiestestchannel and ensuring that my bot is a member of that channel. When that condition is met, the channel ID is stored in the channel
variable.
Debugging
To aid in debugging, a console message is logged that displays a message indicating that the bot has successfully authenticated by displaying its name (${rtmStartData.self.name}
) and the team name (${rtmStartData.team.name}
) it belongs to.
After the bot has authenticated, another event is triggered (RTM.RTM_CONNECTION_OPENED
) that signifies the bot is fully connected and can begin interacting with Slack. The next lines of code create the event listener; upon success, a Hello! message is sent to the channel (in my case, jamiestestchannel).
rtm.on(CLIENT_EVENTS.RTM.RTM_CONNECTION_OPENED, function () { rtm.sendMessage("Hello!", channel); });
At this point, I can now run my Node application and watch my bot automatically post a new message to my channel:
node index.js
The results of running this command (when successful) are twofold:
- I receive my debug message indicating that my bot has successfully logged in. This originated from the
RTM.AUTHENTICATED
being triggered after starting the RTM Client. - I receive a Hello! message in my Slack channel. This occurred when the
RTM.RTM_CONNECTION_OPENED
event message was received and handled by the application.
Before proceeding and further enhancing my application, now is a good time to recap what I have done to get this far:
- Created a custom Slack Bot.
- Created a custom Slack Channel and invited my bot to it.
- Created a new Node.js application called slackbot.
- Installed the Slack Developer Kit package to my application.
- Created my
index.js
file that creates anRtmClient
using my API Token from my custom bot. - Created an event listener for
RTM.AUTHENTICATED
that finds the Slack Channel my bot is a member of. - Created an event listener for
RTM.RTM_CONNECTION_OPENED
that sends a Hello! message to my Slack Channel. - Called the RTM Start Session method to begin the authentication process that is handled by my event listeners.
Building the Bot
Now it's time for the real fun to begin. Slack offers (I didn't count) at least 50 different events that are available for my custom bot to listen and optionally handle. As you can see from the list of Slack Events, some events are custom to the RTM API (which we are using), while other events are custom to the Events API. At the time of writing this article, it is my understanding that the Node.js SDK only supports RTM.
To finish my bot, I will handle the message
event; of course, this is probably one of the most complicated events as it supports a large number of sub-types that I will explore in a moment.
Here is an example of what the most basic message
event looks like from Slack:
{ "type": "message", "channel": "C2147483705", "user": "U2147483697", "text": "Hello world", "ts": "1355517523.000005" }
In this basic object, the three most important things I care about are:
- The
channel
. I will want to ensure this message belongs to the channel my bot is a part of. - The
user
. This will allow me to interact directly with the user or perform a specific action based on who the user is. - The
text
. This is probably the most important piece as it contains the contents of the message. My bot will want to only respond to certain types of messages.
Some messages are more complicated. They can contain a lot of sub-properties such as:
edited
: A child object that describes which user edited the message and when it occurred.subtype
: A string that defines one of the many different kinds, such as channel_join, channel_leave, etc.is_starred
: A boolean indicating if this message has been starred.pinned_to
: An array of channels where this message has been pinned.reactions
: An array of reaction objects that define what the reaction was (e.g. facepalm), how many times it occurred, and an array of users who reacted this way to the message.
I'm going to extend my previously created index.js
to listen for message
events. To reduce redundancy of code, the following examples will contain just the portion of code related to the message
event enhancements.
The first thing that must be done is to include a new module for the RTM_EVENTS
that I will be listening to. I've placed this below my two previous module includes:
var RTM_EVENTS = require('@slack/client').RTM_EVENTS;
The code for handling the message
event I will be placing at the bottom of my file. To test that the message
event is working correctly, I've created a new event listener that logs the message
object to the console as follows:
rtm.on(RTM_EVENTS.MESSAGE, function(message) { console.log(message); });
I can now re-run my Node application (node index.js
). When I type a message into my channel, the following is logged to my console:
{ type: 'message', channel: 'C6TBHCSA3', user: 'U17JRET09', text: 'hi', ts: '1503519368.000364', source_team: 'T15TBNKNW', team: 'T15TBNKNW' }
So far, so good. My bot is successfully receiving messages. The next incremental step to make is to ensure the message belongs to the channel my bot is in:
rtm.on(RTM_EVENTS.MESSAGE, function(message) { if (message.channel === channel) console.log(message); });
Now when I run my application, I only see my debug message if the message
event was for the channel
that my bot is a part of.
I'm now going to extend the application to send a custom message to the channel demonstrating how a user can be tagged in a message:
rtm.on(RTM_EVENTS.MESSAGE, function(message) { if (message.channel === channel) rtm.sendMessage("Stop, everybody listen, <@" + message.user + "> has something important to say!", message.channel); });
Now, when anyone types a message in the channel, my bot sends its own message that looks something like: "Stop, everybody listen, @endyourif has something important to say!"
Ok, not extremely useful. Instead, I'm going to finish my bot by enhancing the message
event listener to respond to specific commands. This will be accomplished by doing the following:
- Split the
text
portion of amessage
into an array based on a blank space. - Check if the first index matches my bot's username.
- If it does, I will look at the second index (if one exists) and treat that as a command that my bot should perform.
To make it easy to detect if my bot was mentioned, I need to create a new variable that will store my bot user ID. Below is an updated section of code where I previously set the channel
variable. It now also stores my bot's user ID in a variable called bot
.
let channel; let bot; rtm.on(CLIENT_EVENTS.RTM.AUTHENTICATED, (rtmStartData) => { for (const c of rtmStartData.channels) { if (c.is_member && c.name ==='jamiestestchannel') { channel = c.id } } console.log(`Logged in as ${rtmStartData.self.name} of team ${rtmStartData.team.name}`); bot = '<@' + rtmStartData.self.id + '>'; });
With my bot
variable set, I've finished my bot by fleshing out the previously created message
event listener as follows:
rtm.on(RTM_EVENTS.MESSAGE, function(message) { if (message.channel === channel) { if (message.text !== null) { var pieces = message.text.split(' '); if (pieces.length > 1) { if (pieces[0] === bot) { var response = '<@' + message.user + '>'; switch (pieces[1].toLowerCase()) { case "jump": response += '"Kris Kross will make you jump jump"'; break; case "help": response += ', currently I support the following commands: jump'; break; default: response += ', sorry I do not understand the command "' + pieces[1] + '". For a list of supported commands, type: ' + bot + ' help'; break; } rtm.sendMessage(response, message.channel); } } } } });
The following code splits the text
property of the message
object in an array based on a space. I next ensure that I have at least two elements in the array, ideally my bot and the command to perform.
When the first element in the array matches my bot, I perform a switch
statement on the second element in the array: the command. The current commands supported are jump and help. When a message is sent to the channel that looks like "@jamiestest jump", my bot will respond with a special message to the originating user.
If the command is not recognized, it will fall into my default case statement for my switch
and respond with a generic command that will look like this: "@endyourif, sorry I do not understand the command "hi". For a list of supported commands, type: @jamiestest help".
Conclusion
At this point, my bot is complete! If you are interested in further enhancing your bot, here's a list of ideas:
- Handle a new team member joining by listening to the
team_join
event. When a new team member joins, it would be a great idea to send them a variety of onboarding information and/or documentation welcoming them to your team. - Enhance the list of supported commands that I've started.
- Make the commands interactive by searching a database, Google, YouTube, etc.
- Create a bot user on an application and create your own custom slash commands.
Thursday, September 28, 2017
Learn Computer Science With JavaScript: Part 3, Loops
Introduction
Suppose you have been given the task to write a program that displays the numbers 1–100. One way you could accomplish this is to write 100 console.log statements. But I’m sure you wouldn’t because you would have become fed up by the 9th or 10th line.
The only part that changes in each statement is the number, so there should be a way to write only one statement. And there is with loops. Loops let us perform a set of steps in a code block repeatedly.
Contents
- While loops
- Do-while loops
- For loops
- Arrays
- For-in loops
- For-of loops
- Review
- Resources
While Loops
While loops will execute a set of statements repeatedly while some condition is true. When the condition is false, the program will exit the loop. This kind of loop tests the condition before performing an iteration. An iteration is an execution of the loop’s body. The following example will not display anything because our condition is false.
let hungry = false; while (hungry) { console.log("eat"); }
This is the general form of a while loop:
while (condition) { statement; statement; etc. }
One thing to be careful of when using while loops is creating loops that never end. This happens because the condition never becomes false. If it happens to you, your program will crash. Example:
let hungry = true; while (hungry) { console.log("eat"); }
Task
How many times will the body of this loop be executed:
let i = 0; while (i < 10) { console.log("Hello, World"); i += 1; }
Do-While Loops
A do-while loop will execute the body of statements first, and then check the condition. This kind of loop is useful when you know you want to run the code at least once. The following example will display “eat” once, even though the condition is false.
let hungry = false; do { console.log("eat"); } while (hungry);
This is the general form for a do while-loop:
do { statement; statement; etc. } while (condition);
Task
Write a do-while loop that will display the numbers 1–10.
For Loops
A for-loop will repeat execution of a code block for a specific number of times. The following example displays the numbers 1–10:
for (let i = 1; i <= 10; i++) { console.log(i); }
This is the general form of a for-loop:
for (initial; condition; step) { statement; statement; etc. }
Initial is an expression that sets the value of our variable. Condition is an expression that must be true for the statements to execute. And step is an expression that increments the value of our variable.
One programming pattern is to use a for loop to update the value of a variable with itself and a new value. This example sums the numbers 1–10:
let x = 0; for (let i = 1; i <= 10; i++) { x += i; } console.log(x) //55
The +=
is an assignment operator that adds a value back to a variable. This is a list of all the assignment operators:
Operator |
Example |
Equivalent |
---|---|---|
+= | x += 2 |
x = x + 2 |
-= | x -= 2 |
x = x - 2 |
*= | x *= 2 |
x = x * 2 |
/= | x /= 2 |
x = x / 2 |
%= | x %= 2 |
x = x % 2 |
Task
Write a for loop that calculates the factorial of a number. The factor of a number n is the product of all the integers from 1 to n. For example, 4! (4 factorial) is 1 x 2 x 3 x 4 which equals 24.
Arrays
An array is an object that holds a list of items, called elements, which are accessed by their index. The index is the position of the element in the array. The first element is at the 0 index. The following are some common array operations.
Create an empty array:
let arr = [ ];
Initialize an array with values:
let arr = [1, 2, "Hello", "World"];
Get an element from an array:
let arr = [1, 2, "Hello", "World"]; arr[0] //1 arr[2] //"Hello"
Update an element in an array:
let arr = [1, 2, "Hello", "World"]; arr[2] = 3; //[1, 2, 3, "World"]
Loop over an array:
let arr = [1, 2, "Hello", "World"]; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); }
A two-dimensional array is an array whose elements are arrays. Example:
let arr = [ [1, 2], ["Hello", "World"] ]; console.log(arr[ 0 ][ 1 ]); //2
This is how you would loop over the array and display each element:
for (let i = 0; i < arr.length; i++) { for (let j = 0; j < arr[i].length; j++) { console.log(arr[ i ][ j ]); } }
Task
What element is displayed when i = 1 and j = 0 in the above for loop?
For-In Loop
This kind of loop lets us loop through the keys in an object. An object is a data structure that has keys mapped to values. Here are some common operations that can be performed on an object.
Create an empty object:
let obj = { };
Initialize an object with values:
let obj = { foo: "Hello", bar: "World" };
Get a property from an object:
let obj = { foo: "Hello", bar: "World" }; obj.foo; //"Hello" obj["foo"]; //"Hello"
Update a property in an object:
let obj = { foo: "Hello", bar: "World" }; obj.foo = "hi" obj["foo"] = "hi"
Loop over the keys of an object:
for (let key in obj) { console.log(key); }
Task
What does the above for loop display given obj = {foo: "Hello", bar: "World"}?
For-Of Loop
This kind of loop lets us loop over the values of iterable objects. Examples of iterable objects are arrays and strings.
Loop over an array:
let arr = ["foo", "bar", "baz"]; for (let elem of arr) { console.log(elem); } // foo bar baz
Loop over a string:
let str = "Hello"; for (let char of str) { console.log(char); } //'H' 'e' 'l' 'l' 'o'
Task
Using any of the loops, write a program that will display this staircase pattern:
# # # # #
Review
Loops let us reduce duplication in our code. While loops let us repeat an action until a condition is false. A do-while loop will execute at least once. For loops let us repeat an action until will we reach the end of a count. The for-in loop is designed so we can access the keys in an object. The for-of loop is designed so we can get the value of an iterable object.
Next, in part 4, we will learn about functions.
Resources
Wednesday, September 27, 2017
Learn Computer Science With JavaScript: Part 4, Functions
Introduction
Suppose you have a file that is 82 lines long and consists only of a series of statements. (I hope this is isn’t true, but anything is possible.) How would you understand what the program does? How would you modify it or use it? It would be kind of hard to do anything with this code because there is no structure to it.
To solve this problem, you could use functions. A function is a group of statements that perform a specific task. Functions allow us to break up a program into smaller programs, making our code more readable, reusable, and testable.
Contents
- Void functions
- Value returning function
- Scope
- Parameters
- Modules
Void Functions
This kind of function lists steps for the program to perform. Consider we are writing a program to log a user into a website. The program might consist of the following tasks:
- Get the username
- Get the password
- Check if the username and password exist
- Redirect the user to their dashboard
Each of these steps might be contained inside a login function. This is an example function:
function greet() { console.log("Hello, World"); }
This is the general form of a function:
function functionName() { statement; statement; etc. }
To execute the function (also known as calling the function, or invoking the function), you write a statement that calls it.
greet();
The ()
is where we pass input to the function. When we are defining the function, the input is called a parameter. When we call the function, the input will be the actual value and is called the argument. Example:
function greet(name) { console.log("Hello, " + name); } greet("Alberta"); //Hello Alberta
With JavaScript ES6, you can define functions using arrow syntax. Here is our greet function defined using arrow syntax:
let greet = () => console.log("Hello, World");
A function with one parameter:
let greet = name => console.log("Hello, " + name);
A function with more than one parameter:
let greet = (fname, lname) => console.log("Hello, " + fname + " " + name);
A function with multiple statements:
let greet = (fname, lname) => { let name = fname + " " + name; console.log("Hello, " + name); }
Because an arrow function is an anonymous function, we give our function a name by assigning it to a variable. Arrow functions can be useful when your function body only has one statement.
Value Returning Function
This kind of function returns a value. The function must end with a return statement. This example returns the sum of two numbers.
function add(x, y) { return x + y; }
This is the general form defining a value returning function:
function functionName() { statement; statement; etc. return expression; }
The value of expression is what gets output by the function. This kind of function is useful when it is stored in a variable.
let variableName = functionName();
Scope
A variable’s scope is the part of the program where a variable can be accessed. A variable can be local or global. A local variable’s scope is inside the function it was created in. No code outside of the function can access its local variables.
Also, when you use let
or const
to declare a variable, they have block scope. A block is a set of statements that belong together as a group. A block could be as simple as wrapping our code in curly braces:
{ let a = 2; }
The variable a
is local to the block it is in. A block can also be a loop or an if statement. Example:
let a = 1; if (5 >4){ let a = 2; } console.log(a); //1
Because our console statement is in the same scope as our first variable a
, it displays that value, which is 1. It does not have access to the variables inside the if block. Now, consider this example:
let a = 1; if (5 >4) { let a = 2; console.log(a); //2 }
Now 2 will be displayed because the scope of variables that our console statement has access to is within the if block. A function’s parameters are also local variables and can only be accessed by code inside the function. Global variables, on the other hand, can be accessed by all the statements in a program’s file. Example:
let a = 1; function foo () { a = 2; } console.log(a); //1 foo(); console.log(a); //2
In this example, a
is a global variable, and we have access to it inside the foo function. The first console statement will display 1. After calling foo
, the value of a
is set to 2, making the second console statement display 2.
Global variables should be used very little, ideally not at all. Because global variables can be accessed by any part of a program, they run the risk of being changed in unpredictable ways. In a large program with thousands of lines of code, it makes the program harder to understand because you can’t easily see how the variable is being used. It is better to create and use local variables.
However, if you need to use a variable in multiple places in your program, it is OK to use a global constant. Declaring a variable with the const
keyword prevents it from being changed, making it safer to use. You only need to worry about updating the value of the constant in the place it was declared.
Parameters
Recall that a parameter is a variable a function uses to accept data. The parameter is assigned the value of a function’s arguments when the function is called. As of ES6, parameters may also be given default values with the format parameterName=value
. In this case, you can call a function without arguments, and it will use default values. Example:
function greet (name="world") { console.log("Hello, " + name); } greet(); //Hello World
The spread/rest operator is new to ES6 and can be used to either expand an array or object into individual values or gather the parameters of a function into an array. This is an example of using a rest parameter:
function foo(...args) { console.log(args); } foo( 1, 2, 3, 4, 5); //[1, 2, 3, 4, 5]
Modules
Suppose now you have a file that has 1,082 lines. (I have seen this, and you should run if you encounter such a thing.) The file is organized into functions, but it is difficult to see how they relate to each other.
To group together related behavior, we should put our code in modules. A module in ES6 is a file that contains related functions and variables. Modules let us hide private properties and expose public properties that we want to use in other files. The filename would be the name of the module. Modules also have their own scope. To use variables outside of the module’s scope, they have to be exported. Variables that aren’t exported will be private and can only be accessed within the module.
Individual properties can be exported like this:
export function foo() { console.log(“Hello World”); } export let bar = 82; export let baz = [1,2,3];
Alternatively, all properties can be exported with one export statement:
function foo() { console.log("Hello World"); } let bar = 82; let baz = [1,2,3]; export { foo, bar, baz };
To use a module’s variables, you import it into the file. You can specify what you want to import from the module like this:
import { foo, bar, baz } from "foo";
You can also rename your import:
import { foo as Foo } from "foo"; Foo();
Or you can import all of the properties of the module:
import * as myModule from "foo"; myModule.foo();
Review
Functions allow us to divide our programs into smaller programs that we can easily manage. This practice is known as modularizing. There are two kinds of functions: void functions and value returning functions. A void function executes the statements inside of it. A value returning function gives us back a value.
Scope is the part of the program where a variable can be accessed. Variables declared inside a function, including the function’s parameters, are local. Blocks also have scope, and local variables can be created inside of them.
Variables not enclosed in a block or module will be global. If you need a global variable, it is acceptable to have a global constant. Otherwise, try to contain your code to modules because modules have their own scope. But even better, modules give your code structure and organization.
Resources
Tuesday, September 26, 2017
Learn Computer Science With JavaScript: Part 2, Conditionals
Introduction
In part one of this series, our programs were only written as a sequence of statements. This structure severely limits what we can do. Say you are designing a program that needs to log in users. You may want to direct a user to one page if they give the correct credentials and send them to another if they aren’t registered.
To do this, you need to use a decision structure like an if statement. This will perform an action only under certain conditions. If the condition does not exist, the action is not performed. In this tutorial, you'll learn all about conditionals.
Contents
- If statements
- Relational operators
- If-else statement
- Switch statements
- Logical operators
- Review
- Resources
If Statements
A single if statement will perform an action if a condition is true. If the condition is false, the program will execute the next statement that is outside of the if block. In the following example, if the expression isRaining()
is true, then we will putOnCoat()
and putOnRainboots()
then goOutside()
. If isRaining()
is false, the program will only execute goOutside()
.
if (isRaining) { putOnCoat(); putOnRainboots(); } goOutside();
This is the general form for writing an if statement:
if (condition) { statement; statement; etc. }
The condition is an expression that has the value true or false. An expression that is true or false is called a boolean expression. Boolean expressions are made with relational operators.
Relational Operators
A relational operator compares two values and determines if the relationship between them is true or false. They can be used to create boolean expressions for our conditions. Here is a list of relational operators with examples:
Operator | Meaning | Example | Meaning |
---|---|---|---|
== | equality | x == y | Is x equal to y? |
=== |
strict equality | x === y | Is x equal to y in value and type? |
!= |
inequality |
x != y |
Is x not equal to y? |
!== |
strict inequality |
x !== y |
Is x not equal to y in value and type? |
> | greater than |
x > y |
Is x greater than y? |
< | less than |
x < y |
Is x less than y? |
>= | greater than or equal |
x >= y |
Is x greater than or equal to y? |
<= | less than or equal |
x <= y |
Is x less than or equal to y? |
It is important to note the difference between the equality operator ==
and the strict equality operator ===
. For example, the expression 2 == "2"
is true. But the expression 2 === "2"
is false. In the second example, the two values are different data types, and that is why the expression is false. It is best practice to use ===
or !==
.
The following example will display the message, "You get an A".
let grade = 93; if (grade >= 90) { console.log("You get an A"); }
Task
What is the value of the expression 5 > 3? 6 != "6"?
If-Else Statements
An if-else statement will execute one block of statements if its condition is true, or another block if its condition is false. The following example will display the message “valid username” because the condition is true.
let username = "alberta"; if (username === "alberta") { console.log("valid username"); } else { console.log("Incorrect username. Try again."); }
This is the general form of an if-else statement:
if (condition) { statement; statement; etc. } else { statement; statement; etc. }
Task
What will be the output of this program:
let isLoggedIn = false; if (isLoggedIn) { console.log("Welcome"); } else { console.log("You are not logged in"); }
It is also possible to check for more than one condition. Example:
let num = 3; if (num === 1) { console.log("I"); } else if (num === 2) { console.log("II"); } else if (num === 3) { console.log("III"); } else if (num === 4) { console.log("IV"); } else if (num === 5) { console.log("V"); } else { console.log("Invalid input"); }
This is the general form for writing multiple if-else-if statements:
if (condition1) { statement; statement; etc. } else if (condition2) { statement; statement; etc. } else { statement; statement; etc. }
Switch Statements
A switch statement is also used to conditionally execute some part of your program. The following example implements our roman numeral converter as a switch statement:
let num = 3; switch (num) { case 1: console.log("I"); break; case 2: console.log("II"); break; case 3: console.log("III"); break; case 4: console.log("IV"); break; case 5: console.log("V"); break; default: console.log("Invalid input"); }
This is the general form of a switch statement:
switch (expression) { case value1: statement; statement; etc. break; case value2: statement; statement; etc. break; default: statement; statement; etc. }
Each case represents a value our expression can take. Only the block of code for the case that is true will execute. We include a break statement at the end of the code block so that the program exits the switch statement and doesn’t execute any other cases. The default case executes when none of the other cases are true.
Task
Write a switch statement that displays the day of the week given a number. For example, 1 = Sunday, 2 = Monday, etc.
Logical Operators
The and operator &&
and the or operator ||
allow us to connect two boolean expressions. The not operator !
negates an expression. To illustrate how logical operators work, we will look at a truth table. A truth table contains all the combinations of values used with the operators. I use P to represent the left-hand expression and Q for the right-hand expression.
&&
truth table:
P | Q | P && Q |
---|---|---|
true | true | true |
true |
false |
false |
false |
true |
false |
false |
false | false |
We read the table going across each row. The first row tells us that when P is true and Q is true, P && Q is true. The following example tests whether 82 is between 60 and 100 inclusive.
- let x = 82;
- x >= 60 && x <= 100
- P: x >= 60 is true
- Q: x <= 100 is true
- P && Q: true && true
||
truth table:
P | Q | P || Q |
---|---|---|
true |
true |
true |
true |
false |
true |
false |
false | true |
false |
false |
false |
This example tests if 82 is outside the range 60–100:
- let x = 82;
- x < 60 || x > 100
- P: x < 60 is false
- Q: x > 100 is false
- P || Q: false || false
!
truth table:
P | !P |
---|---|
true |
false |
false |
true |
Example:
- x = 82
- P: x > 0 is true
- !P: false
Task
Fill in the table with the missing values.
P | Q | !P | !Q | !P && !Q |
!P || !Q |
---|---|---|---|---|---|
true |
true |
||||
true |
false |
||||
false |
true |
||||
false |
false |
Something useful to know about logical operators is that if the expression on the left side of the &&
operator is false, the expression on the right will not be checked because the entire statement is false. And if the expression on the left-hand side of an ||
operator is true, the expression on the right will not be checked because the entire statement is true.
Review
A program can execute blocks of code conditionally using boolean expressions. A boolean expression is written using relational operators. Logical operators allow us to combine boolean expressions.
A single if statement gives the program one alternative path to take if a condition is met. If-else statements provide a second course of action if the condition is false. And if-else-if statements allow us to test multiple conditions. Switch statements can be used as an alternative to an if-else-if statement when you have multiple conditions to test.
Next, in part 3, we will discuss loops.
Resources
Monday, September 25, 2017
6 Things That Make Yarn the Best JavaScript Package Manager
Yarn is an open-source npm client that was developed at Facebook and improves on many aspects of the standard npm client. In this tutorial, I'll focus on the top six features that make Yarn awesome:
- Speed
- Robust Installs
- License Checks
- Compatibility with npm and Bower
- Multiple Registries
- Emojis
1. Speed
One of Yarn's claims to fame is its speed compared to the standard npm client. But how fast is it? In a recent benchmark, Yarn was two to three times faster than npm. The benchmark timed the installation of React, Angular 2, and Ember. This is a pretty good test for a package manager as each of these frameworks pulls a bunch of dependencies and represents a major share of the dependencies of a real-world web application.
Let's add another data point and check for ourselves by installing a create-react-app using both yarn and npm. Here is the installation using yarn:
$ yarn global add create-react-app --prefix /usr/local yarn global v0.27.5 warning package.json: No license field warning No license field [1/4] Resolving packages... [2/4] Fetching packages... [3/4] Linking dependencies... [4/4] Building fresh packages... success Installed "create-react-app@1.4.0" with binaries: - create-react-app warning No license field Done in 2.59s.
Here is the installation using npm:
$ npm install -g create-react-app /usr/local/bin/create-react-app -> /usr/local/lib/node_modules/create-react-app/index.js + create-react-app@1.4.0 added 80 packages in 9.422s
Yep. This definitely corroborates other reports about a significant speed advantage to yarn. Yarn installed in 2.59 seconds, while npm took 9.422 seconds. Yarn was 3.63X faster!
2. Robust Installs
Yarn also boasts more robust installs than npm. What makes an install flaky? If subsequent installs fail or produce a different outcome then an install is flaky. There are two main causes:
- Transient network issues might cause fetching packages from the registry to fail.
- New releases of packages might result in incompatible and breaking changes.
Yarn addresses both concerns.
Offline Cache
Yarn uses a global offline cache to store packages you've installed once, so new installations use the cached version and avoid flakiness due to intermittent network failures. You can find where your yarn cache is by typing:
$ yarn cache dir /Users/gigi.sayfan/Library/Caches/Yarn/v1
Here are the first five packages in my offline cache:
$ ls `yarn cache dir` | head -5 npm-@kadira npm-@types npm-Base64-0.2.1-ba3a4230708e186705065e66babdd4c35cf60028 npm-JSONStream-0.8.4-91657dfe6ff857483066132b4618b62e8f4887bd npm-abab-1.0.3-b81de5f7274ec4e756d797cd834f303642724e5d
Yarn can go even further and have a full offline mirror that will work across upgrades of the yarn itself.
The yarn.lock File
The yarn.lock file is updated whenever you add or upgrade a version. It essentially pins down the exact version of each package that may be specified in package.json using partial versioning (e.g. just major and minor) and its dependencies.
Here is the beginning of a typical yarn.lock file. You can see the version as specified in package.json like "abbrev@1" and the pinned version "1.1.0".
cat yarn.lock | head -18 # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 abab@^1.0.3: version "1.0.3" resolved "http://ift.tt/2xq9xcu /abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" abbrev@1: version "1.1.0" resolved "http://ift.tt/2fKVBjd /abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" accepts@~1.3.3: version "1.3.4" resolved "http://ift.tt/2xr9xZL -/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" dependencies: mime-types "~2.1.16" negotiator "0.6.1"
But Why?
Yarn also gives you the yarn why
command to explain why a particular package is installed in your project:
$ yarn why worker-farm yarn why v0.27.5 [1/4] Why do we have the module "worker-farm"...? [2/4] Initialising dependency graph... [3/4] Finding dependency... [4/4] Calculating file sizes... info This module exists because "react-scripts#jest#jest-cli" depends on it. info Disk size without dependencies: "132kB" info Disk size with unique dependencies: "212kB" info Disk size with transitive dependencies: "244kB" info Number of shared dependencies: 2 Done in 1.38s.
3. License Checks
Some projects need to abide by certain licensing requirements or just produce a report for internal or external purposes. Yarn makes it really easy with the yarn licenses ls
command. It produces a compact report that includes the fully qualified package name, its URL, and the license. Here is an example:
$ yarn licenses ls | head -20 yarn licenses v0.27.5 ├─ abab@1.0.3 │ ├─ License: ISC │ └─ URL: git+http://ift.tt/2fLMaAc ├─ abbrev@1.1.0 │ ├─ License: ISC │ └─ URL: http://ift.tt/2xqVOC1 ├─ accepts@1.3.4 │ ├─ License: MIT │ └─ URL: http://ift.tt/2fLE9em ├─ acorn-dynamic-import@2.0.2 │ ├─ License: MIT │ └─ URL: http://ift.tt/2xsfcPk ├─ acorn-globals@3.1.0 │ ├─ License: MIT │ └─ URL: http://ift.tt/2fM6EZz ├─ acorn-jsx@3.0.1 │ ├─ License: MIT │ └─ URL: http://ift.tt/1zX0fOk
Yarn can even generate a disclaimer for you with yarn licenses generate-disclaimer
. The result is some text with a disclaimer message and text for each package in your application. Here is a sample from the disclaimer generated for my test project:
----- The following software may be included in this product: utils-merge. A copy of the source code may be downloaded from git://github.com/jaredhanson/utils-merge.git. This software contains the following license and notice below: (The MIT License) Copyright (c) 2013 Jared Hanson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----- The following software may be included in this product: uuid. A copy of the source code may be downloaded from http://ift.tt/2xqnebm. This software contains the following license and notice below: Copyright (c) 2010-2012 Robert Kieffer MIT License - http://ift.tt/SHXZm8 -----
4. Compatibility With npm and Bower
Yarn is fully compatible with npm as it is just a different client that works with npm registries. Very early it supported Bower, but then shortly after a decision was made to drop Bower support.
The main reason was that the Bower support didn't work very well and emptied the bower_components directory or didn't fetch any bower packages on a fresh project. But another reason is that the Yarn team didn't want to encourage fragmentation in the package management arena and instead preferred for everyone to switch to npm.
If you are invested in Bower and don't want to migrate right now, you can still use Yarn, but add the following snippet to your package.json file:
"scripts": { "postinstall": "bower install" }
5. Multiple Registries
Yarn can work with multiple registry types. By default, if you just add a package, it will use its npm registry (which is not the standard npm registry). But it can also add packages from files, remote tarballs, or remote git repositories.
To see the current configured npm registry:
$ yarn config get registry http://ift.tt/2eBLKgI
To set a different registry type: yarn config set registry <registry url>
To add packages from different locations, use the following add commands:
# Configured npm registry yarn add <pkg-name> # Local package yarn add file:/<path to local package directory # Remote tarball yarn add https://<path to compressed tarball>.tgz # Remote git repo yarn add <git remote-url>
6. Emojis FTW!
Some people like emojis, and some people don't. Yarn originally displayed emojis automatically, but only on Mac OS X. It caught fire from both camps: the emoji haters were upset that their console on Mac OS X was cluttered with emojis, and the emoji lovers were upset that they didn't have emojis on Windows and Linux.
Now, emojis are not displayed on macOS by default, and you can enable emojis with the --emoji
flag:
$ yarn install --emoji yarn install v0.27.5 [1/4] 🔍 Resolving packages... [2/4] 🚚 Fetching packages... [3/4] 🔗 Linking dependencies...
Conclusion
Yarn is the best JavaScript package manager. It is compatible with npm, but much, much faster. It addresses serious problems for large-scale projects with flaky installation, it supports multiple types of registries, and it has emojis to boot.
JavaScript, though not without its learning curves, has plenty of libraries and frameworks to keep you busy, as you can see. If you’re looking for additional resources to study or to use in your work, check out what we have available in the Envato marketplace.
The JavaScript community is overall very positive, and there is a lot of momentum behind Yarn. It has already addressed some issues such as redundant Bower support and emojis by default. Migrating to Yarn from npm is very easy. I highly recommend that you give a try.
Learn Computer Science With JavaScript: Part 1, The Basics
Introduction
JavaScript is a language that we can use to write programs that run in a browser or on a server using Node. Because of Node, you can use JavaScript to build full web applications like Twitter or games like Agar.io.
This is the first lesson in a four-part series where I will teach you the programming fundamentals you will need so you can learn to build your own apps. In part 1, I will introduce you to the syntax of JavaScript and ES6. ES6 stands for ECMAScript 6, which is a version of JavaScript.
Contents
- Installation and setup
- Designing a program
- Syntax
- Variables
- Data types
- Review
- Resources
Installation and Setup
First, we will set up our development environment so that we can run our code on our own computer. Alternatively, you can test code examples on an online editor like repl.it. I prefer you get started writing and running code on your computer so you can feel like a real programmer. Plus, I want you using Node so you can put it on your resume and impress your employer.
First, you will need a text editor to write your code in. I recommend Sublime Text. Next, download and install Node to your computer. You can get the software on the Node.js website. Confirm the installation worked by typing the command node -v
from your terminal. If everything is fine, you will see the version number of your Node installation.
One of the things you can do with Node is run JavaScript code from within your terminal. This takes place in what is called a REPL. To try it out, enter the command node
in your terminal.
Next, let's print the message “Hello, World”. Type the following into the terminal:
> console.log("Hello, World");
To exit the REPL, press Control-C twice. Using the REPL comes in handy when you want to test simple statements like the example above. This can prove more convenient than saving code to a file—especially if you’re writing throwaway code.
To execute a program you have written in a file, in your terminal run the command node filename
, where filename is replaced with the name of your JavaScript file. You do not have to type the js
extension of the filename to run the script. And you must be in the root directory where the file lives.
Let’s try an example. Create a file named hello.js
. Inside, we will put the following code:
console.log("Hello, World");
Run the code from the terminal:
$ node hello
If all is well, you will see the text "Hello, World" output to the terminal. From now on, you can test the code examples from this tutorial either by using the Node REPL or by saving to a file.
Designing a Program
Before you write any code, you should take the time to understand the problem. What data do you need? What is the outcome? What tests does your program need to pass?
When you understand the requirements of the program, then you can write the steps to solve it. The steps are your algorithm. Your algorithm is not code. It is plain English (replace English with your native language) instructions for solving the problem. For example, if you want to write an algorithm for cooking top ramen, it might look like this:
- Remove top from cup.
- Empty seasoning pack into cup.
- Fill cup with water.
- Microwave on high for 2 minutes.
- Cool for 1 minute.
Yes, I was hungry when I thought of this. And no, this is not something you would actually be given as a programming problem. Here is an example of a problem that is more practical. It is an algorithm to calculate the average of a list of numbers.
- Calculate the sum all of the numbers.
- Get the total number of numbers.
- Divide the sum by the total.
- Return the result.
Understanding the problem and coming up with an algorithm are the most important steps in programming. When you feel confident in your algorithm, then you should write some test cases. The tests will show how your code should behave. Once you have your tests, then you write code and tweak it until your tests pass. Depending on how complex your problem is, each one of the steps in your algorithm may need to be broken down further.
Task
Write an algorithm to calculate the factorial of a number. The factorial of a number *n* is the product of all integers from 1 to *n*. For example, 4! (4 factorial) is 1 x 2 x 3 x 4 = 24.
Syntax
A program is similar to the language we speak with. The only difference is a programming language is meant to communicate with the computer (and other programmers who have to use it). The rules for constructing the program are its syntax. Programs consist of statements. A statement can be thought of as a sentence. In JavaScript, you must put a semicolon at the end of a statement. Example:
a = 2 + 2;
Statements consist of expressions. Expressions are like the subject/predicate parts of a sentence. In a programming language, expressions have a value. Expressions consist of keywords like var
and for
which are the built-in vocabulary of the language; data like a number or string; and operators like +
and =
. Example:
2+2
Here is a list of arithmetic operators:
+
- Addition-
- Subtraction*
- Exponentiation*
- Multiplication/
- Division%
- Remainder++
- Increment--
- Decrement
The remainder operator returns the remainder after dividing two numbers. For example, 4 % 2
returns 0, and 5 % 3
returns 2. The remainder operator is commonly used to find out if a value is even or odd. Even values will have a remainder 0.
Task
Find the value of the following expressions. Write down your answers first, and then check them in your REPL.
- 9 % 3
- 3 % 9
- 3 % 6
- 3 % 4
- 3 % 3
- 3 % 2
Variables
A variable is a name that represents a value in the computer’s memory. Any value we would like to store or to use over and over should be placed in a variable. One way of creating variables is with the var
keyword. But the preferred way is to use the let
or const
keywords. Here are some examples of using let to create variables:
Declaring a variable:
let a;
Declaring and initializing a variable:
let a = 1;
Reassigning a variable:
a = 2;
Constants are variables that cannot change. They can only be assigned once. Constants that have objects or arrays as values can still be modified because they are assigned by reference. The variables do not hold a value; instead, they point to the location of the object. Example:
const a = {foo:1,bar:2}; a.baz = 3; console.log( a ); // displays { foo: 1, bar: 2, baz: 3 }
However, this will give you an error:
const a = {foo:1,bar:2}; a = {}; console.log( a );
Data Types
Data types have rules for how they can be operated on. For example, if we add two numbers, we get their sum. But if we add a number with a string, we get a string. Here is a list of the different data types:
- Undefined: a variable that has not been assigned a value
- Null: no value
- Boolean: an entity that has the value true or false
- String: a sequence of characters
- Symbol: a unique, unchanging key
- Number: integer and decimal values
- Object: a collection of properties
A string is a data type that consists of characters. A string will be surrounded with single quotes or double quotes. Strings also have methods you can use to perform actions on them. The following are some examples of actions you can perform on strings.
Determine if a string begins with a substring:
"Hello, World".startsWith("hello"); //true
Determine if a string ends with a substring:
"Hello, World".endsWith("World"); //true
Determine if a substring is located anywhere in a string:
"Hello, World".includes("World"); //true
Repeat a string a specified number of times:
"Hello".repeat(3); //HelloHelloHello
We can turn a string into an array with the spread operator: ...
let str = [..."hello"]; console.log(str); //[ 'h', 'e', 'l', 'l', 'o' ]
Template literals are a special kind of string that use backticks: ` `
. We can use them to insert variables within a string like this:
let name = "World"; let greeting = `Hello, ${name}`; console.log(greeting); //Hello, World
We can create multiline strings like this:
` <div> <h1>Hello, World</h1> </div> `
Review
We have seen how to set up our development environment using Node. The first step to programming is writing the steps to solve the problem. This is called the algorithm. The actual code will consist of many statements. Statements are the program’s instructions and are made up of expressions. Expressions are useful in our program if we assign them to variables. Variables are created with the let
or const
keyword.
In part 2, I will explain conditionals.
Resources
Sunday, September 24, 2017
Saturday, September 23, 2017
Friday, September 22, 2017
Teach for Us: Design & Illustration Gurus Wanted!
Do you want to share your design knowledge with an eager community of millions? Are you looking for a way to supplement your income? Would you like to establish yourself as an expert in your field? Well, this may be the opportunity for you.
What We're Looking For
Envato Tuts+ has many areas of content, and while we love a variety of avenues, we're specifically looking for instructors to create written content. The topics we're specifically looking for are:
- Photo manipulation
- Product mockups
- Logo design
- Text effects
- Photoshop Actions
- Adobe Photoshop
- Procreate
- Affinity Designer
- Sketch app
Your area of expertise not listed? We never turn a good application down!
You must also be comfortable with the English language. We can proof your content, but we can't rewrite everything for you. To put it simply, if you struggle when deciding whether to write its or it's, this might not be the gig for you.
We're looking for content from quick tips to in-depth tutorials. Most of all, we want instructors who can explain themselves clearly and accurately and produce quality end results. Whether you're an expert in Adobe Illustrator, Adobe InDesign, Inkscape, Adobe Photoshop, Sketch or any other design package, we want to hear from you!
What Do You Get Out of It?
There are many benefits in becoming an Envato Tuts+ Instructor:
- Getting paid for a subject you're passionate about is always rewarding. Depending on the content type and subject, you could be billing thousands per month! We pay $250 USD for a standard length tutorial.
- Get your name out into the community. This is especially good if you're just starting your freelance career.
- Establish yourself as an expert in your given field by writing regularly for a respected educational network.
- If you provide assets to Envato Market or Envato Elements, then use this opportunity to help promote your items!
Pitch a Tutorial!
While I can tell you which areas we're specifically looking into, it all comes down to which areas are your strongest and what you feel most confident in teaching. If you are interested in becoming an Envato Tuts+ Instructor, why don't you pitch us an idea? We're looking forward to hearing from you.
Create Interactive Charts Using Plotly.js, Part 5: Pie and Gauge Charts
If you have been following this series from the beginning, you might have noticed that Plotly.js uses the same scatter
type for creating both line charts and bubble charts. The only difference is that we had to set the mode
to lines
while creating line charts and markers
when creating bubble charts.
Similarly, Plotly.js allows you to create pie, donut and gauge charts by using the same value for the type
attribute and changing the value of other attributes depending on the chart you want to create.
Creating Pie Charts in Plotly.js
You can create pie charts in Plotly.js by setting the type
attribute to pie
. There are also other attributes like opacity
, visible
and name
that are common to other chart types as well. The name
attribute is used to provide a name for the current pie trace. This name is then shown in the legend for identification. You can show or hide the pie trace in the legend of a chart by setting the showlegend
attribute to true
or false
respectively. You can set a label name for the different sectors of a pie chart by using the labels
attribute.
In the case of pie charts, the marker object is used to control the appearance of different sectors of the chart. The color
attribute nested inside marker
can be used to set the color of each sector of the pie chart. The color for different sectors can be specified as an array value to the color
attribute.
You can also set the color and width of all the lines enclosing each sector using the color
and width
attributes nested inside the line object. You also have the option to sort all the sectors of the pie chart from largest to smallest using the boolean sort
attribute. Similarly, the direction of the sectors can be changed to clockwise
or counterclockwise
with the help of the direction
attribute.
The following code creates a basic pie chart that lists the forested area of the top five countries in the world.
var pieDiv = document.getElementById("pie-chart"); var traceA = { type: "pie", values: [8149300, 4916438, 4776980, 3100950, 2083210], labels: ['Russia', 'Canada', 'Brazil', 'United States', 'China'] }; var data = [traceA]; var layout = { title: "Area Under Forest for Different Countries" }; Plotly.plot(pieDiv, data, layout);
As you can see, we are no longer using the x
and y
attributes to specify the points that we want to plot. This is now done with the help of values
and labels
. The percentages are determined automatically based on the input values.
By default, the first slice of the pie starts at 12 o'clock. You can change the starting angle of the chart using the rotation
attribute, which accepts a value between -360 and 360. The default 12 o'clock value is equal to the angle 0.
If you want a slice in your chart to stand out, you can use the pull
attribute, which can accept either a number or an array of numbers with values between 0 and 1. The pull
attribute is used to pull the specified sectors out of the pie. The pull distance is equal to a fraction of the larger radius of the pie or donut.
It is very easy to convert a pie chart into a donut chart by specifying a value for the hole
attribute. It will cut the given fraction of the radius out from the pie to make a donut chart.
You can control the color of individual sectors in a pie chart using the colors
attribute nested inside the marker object. The width and color of the line that encloses each sector can also be changed with the help of the width
and color
attributes nested inside the line object. The default width of the enclosing line is 0. This means that no line will be drawn around the sectors by default.
There is also a hovertext
attribute, which can be used to provide some extra textual information for each individual sector. This information will be visible to viewers when they hover over a sector. One condition for the text to appear is that the hoverinfo
attribute should contain a text flag. You can set the color of text lying inside or outside of the pie sectors using the family
, size
and color
attributes nested inside the insidetextfont
and outsidetextfont
objects respectively.
The following code uses the data from our previous pie chart to create a donut chart that uses the additional attributes we just learned about.
var pieDiv = document.getElementById("pie-chart"); var traceA = { type: "pie", values: [8149300, 4916438, 4776980, 3100950, 2083210], labels: ['Russia', 'Canada', 'Brazil', 'United States', 'China'], hole: 0.25, pull: [0.1, 0, 0, 0, 0], direction: 'clockwise', marker: { colors: ['#CDDC39', '#673AB7', '#F44336', '#00BCD4', '#607D8B'], line: { color: 'black', width: 3 } }, textfont: { family: 'Lato', color: 'white', size: 18 }, hoverlabel: { bgcolor: 'black', bordercolor: 'black', font: { family: 'Lato', color: 'white', size: 18 } } }; var data = [traceA]; var layout = { title: "Area Under Forest for Different Countries" }; Plotly.plot(pieDiv, data, layout);
Creating Gauge Charts in Plotly.js
The basic structure of a gauge chart is similar to a donut chart. This means that we can use some cleverly selected values and create simple gauge charts by still keeping the type
attribute set to pie
. Basically, we will be hiding some sections of the full pie to make it look like a gauge chart.
First, we need to choose some values for the values
attribute. To keep things simple, I will be using the top half of the pie as my gauge chart. This means that the values should be divided equally between the part that I want to be visible and the part of the pie chart that I want to hide. The visible section of the chart can further be divided into smaller parts. Here is an example of choosing the values for our gauge chart.
values: [100 / 5, 100 / 5, 100 / 5, 100 / 5, 100 / 5, 100]
The number 100 in the above line is arbitrary. As you can see, the first five slices together add up to 100, which is also the value set for the hidden area of the pie chart. This divides the whole pie equally between the hidden and visible part.
Here is the complete code that creates our basic gauge chart. You should note that I have set the color attribute of the sector that should be hidden to white. Similarly, the text
and labels
values for the corresponding sector have also been set to empty strings. The rotation
attribute has been set to 90 so that the chart is not drawn from its default 12 o'clock position.
var gaugeDiv = document.getElementById("gauge-chart"); var traceA = { type: "pie", showlegend: false, hole: 0.4, rotation: 90, values: [100 / 5, 100 / 5, 100 / 5, 100 / 5, 100 / 5, 100], text: ["Very Low", "Low", "Average", "Good", "Excellent", ""], direction: "clockwise", textinfo: "text", textposition: "inside", marker: { colors: ["rgba(255, 0, 0, 0.6)", "rgba(255, 165, 0, 0.6)", "rgba(255, 255, 0, 0.6)", "rgba(144, 238, 144, 0.6)", "rgba(154, 205, 50, 0.6)", "white"] }, labels: ["0-10", "10-50", "50-200", "200-500", "500-2000", ""], hoverinfo: "label" };
The next part of the code deals with the needle of the gauge chart. The value that you set for the degrees
variable will determine the angle at which the needle is drawn. The radius
variable determines the length of the needle. The attributes x0
and y0
are used to set the starting point of our line. Similarly, the attributes x1
and y1
are used to set the ending point of our line.
You can create more complex shapes for your needle with the help of SVG paths. All you have to do is set the type
attribute to path
and specify the actual path using the path
attribute. You can read more about it in the layout shapes section of the reference.
var degrees = 115, radius = .6; var radians = degrees * Math.PI / 180; var x = -1 * radius * Math.cos(radians); var y = radius * Math.sin(radians); var layout = { shapes:[{ type: 'line', x0: 0, y0: 0, x1: x, y1: 0.5, line: { color: 'black', width: 8 } }], title: 'Number of Printers Sold in a Week', xaxis: {visible: false, range: [-1, 1]}, yaxis: {visible: false, range: [-1, 1]} }; var data = [traceA]; Plotly.plot(gaugeDiv, data, layout, {staticPlot: true});
All the code of this section creates the following gauge chart. Right now, the chart is not very fancy, but it can act as a good starting point.
Final Thoughts
In this tutorial, you learned how to create pie and donut charts using the pie
trace type in Plotly.js. You also learned how to carefully set the values of a few attributes to convert those pie charts into simple gauge charts. You can read more about pie charts and their different attributes on the reference page.
This was the last tutorial of our interactive Plotly.js charts series. The first introductory tutorial provided you an overview of the library. The second, third and fourth tutorials showed you how to create line charts, bar charts, and bubble charts respectively. I hope you enjoyed this tutorial as well as the whole series. If you have any questions, feel free to let me know in the comments.
Thursday, September 21, 2017
An Introduction to ETS Tables in Elixir
When crafting an Elixir program, you often need to share a state. For example, in one of my previous articles I showed how to code a server to perform various calculations and keep the result in memory (and later we've seen how to make this server bullet-proof with the help of supervisors). There is a problem, however: if you have a single process that takes care of the state and many other processes that access it, the performance may be seriously affected. This is simply because the process can serve only one request at a time.
However, there are ways to overcome this problem, and today we are going to talk about one of them. Meet Erlang Term Storage tables or simply ETS tables, a fast in-memory storage that can host tuples of arbitrary data. As the name implies, these tables were initially introduced in Erlang but, as with any other Erlang module, we can easily use them in Elixir as well.
In this article you will:
- Learn how to create ETS tables and options available upon creation.
- Learn how to perform read, write, delete and some other operations.
- See ETS tables in action.
- Learn about disk-based ETS tables and how they differ from in-memory tables.
- See how to convert ETS and DETS back and forth.
All code examples work with both Elixir 1.4 and 1.5, which was recently released.
Introduction to ETS Tables
As I mentioned earlier, ETS tables are in-memory storage that contain tuples of data (called rows). Multiple processes may access the table by its id or a name represented as an atom and perform read, write, delete and other operations. ETS tables are created by a separate process, so if this process is terminated, the table is destroyed. However, there is no automatic garbage collection mechanism, so the table may hang out in the memory for quite some time.
Data in the ETS table are represented by a tuple {:key, value1, value2, valuen}
. You can easily look up the data by its key or insert a new row, but by default there can't be two rows with the same key. Key-based operations are very fast, but if for some reason you need to produce a list from an ETS table and, say, perform complex manipulations of the data, that's possible too.
What's more, there are disk-based ETS tables available that store their contents in a file. Of course, they operate slower, but this way you get a simple file storage without any hassle. On top of that, in-memory ETS can be easily converted to disk-based and vice versa.
So, I think it's time to start our journey and see how the ETS tables are created!
Creating an ETS Table
To create an ETS table, employ the new/2
function. As long as we are using an Erlang module, its name should be written as an atom:
cool_table = :ets.new(:cool_table, [])
Note that until recently you could only create up to 1,400 tables per BEAM instance, but this is not the case anymore—you are only limited to the amount of available memory.
The first argument passed to the new
function is the table's name (alias), whereas the second one contains a list of options. The cool_table
variable now contains a number that identifies the table in the system:
IO.inspect cool_table # => 12306
You may now use this variable to perform subsequent operations to the table (read and write data, for example).
Available Options
Let's talk about the options that you may specify when creating a table. The first (and somewhat strange) thing to note is that by default you cannot use the table's alias in any way, and basically it has no effect. But still, the alias must be passed upon the table's creation.
To be able to access the table by its alias, you must provide a :named_table
option like this:
cool_table = :ets.new(:cool_table, [:named_table])
By the way, if you'd like to rename the table, it can be done using the rename/2
function:
:ets.rename(cool_table, :cooler_table)
Next, as already mentioned, a table cannot contain multiple rows with the same key, and this is dictated by the type. There are four possible table types:
:set
—that's the default one. It means that you can't have multiple rows with exactly the same keys. The rows are not being re-ordered in any particular manner.:ordered_set
—the same as:set
, but the rows are ordered by the terms.:bag
—multiple rows may have the same key, but the rows still cannot be fully identical.:duplicate_bag
—rows can be fully identical.
There is one thing worth mentioning regarding the :ordered_set
tables. As Erlang's documentation says, these tables treat keys as equal when they compare equal, not only when they match. What does that mean?
Two terms in Erlang match only if they have the same value and the same type. So integer 1
matches only another integer 1
, but not float 1.0
as they have different types. Two terms are compare equal, however, if either they have the same value and type or if both of them are numerics and extend to the same value. This means that 1
and 1.0
are compare equal.
To provide the table's type, simply add an element to the list of options:
cool_table = :ets.new(:cool_table, [:named_table, :ordered_set])
Another interesting option that you can pass is :compressed
. It means that the data inside the table (but not the keys) will be—guess what—stored in a compact form. Of course, the operations that are executed upon the table will become slower.
Next up, you can control which element in the tuple should be used as the key. By default, the first element (position 1
) is used, but this can be changed easily:
cool_table = :ets.new(:cool_table, [{:keypos,2}])
Now the second elements in the tuples will be treated as the keys.
The last but not the least option controls the table's access rights. These rights dictate what processes are able to access the table:
:public
—any process can perform any operation to the table.:protected
—the default value. Only the owner process can write to the table, but all the processes can read.:private
—only the owner process can access the table.
So, to make a table private, you would write:
cool_table = :ets.new(:cool_table, [:private])
Alright, enough talking about options—let's see some common operations that you can perform to the tables!
Write Operations
In order to read something from the table, you first need to write some data there, so let's start with the latter operation. Use the insert/2
function to put data into the table:
cool_table = :ets.new(:cool_table, []) :ets.insert(cool_table, {:number, 5})
You may also pass a list of tuples like this:
:ets.insert(cool_table, [{:number, 5}, {:string, "test"}])
Note that if the table has a type of :set
and a new key matches an existing one, the old data will be overwritten. Similarly, if a table has a type of :ordered_set
and a new key compares equal to the old one, the data will be overwritten, so pay attention to this.
The insert operation (even with multiple tuples at once) is guaranteed to be atomic and isolated, which means that either everything is stored in the table or nothing at all. Also, other processes won't be able to see the intermediate result of the operation. All in all, this is pretty similar to SQL transactions.
If you are concerned about duplicating keys or do not want to overwrite your data by mistake, use the insert_new/2
function instead. It is similar to insert/2
but will never insert duplicating keys and will instead return false
. This is the case for the :bag
and :duplicate_bag
tables as well:
cool_table = :ets.new(:cool_table, [:bag]) :ets.insert(cool_table, {:number, 5}) :ets.insert_new(cool_table, {:number, 6}) |> IO.inspect # => false
If you provide a list of tuples, each key will be checked, and the operation will be cancelled even if one of the keys is duplicated.
Read Operations
Great, now we have some data in our table—how do we fetch them? The easiest way is to perform lookup by a key:
:ets.insert(cool_table, {:number, 5}) IO.inspect :ets.lookup(cool_table, :number) # => [number: 5]
Remember that for the :ordered_set
table, the key should compare equal to the provided value. For all other table types, it should match. Also, if a table is a :bag
or an :ordered_bag
, the lookup/2
function may return a list with multiple elements:
cool_table = :ets.new(:cool_table, [:bag]) :ets.insert(cool_table, [{:number, 5}, {:number, 6}]) IO.inspect :ets.lookup(cool_table, :number) # => [number: 5, number: 6]
Instead of fetching a list, you may grab an element in the desired position using the lookup_element/3
function:
cool_table = :ets.new(:cool_table, []) :ets.insert(cool_table, {:number, 6}) IO.inspect :ets.lookup_element(cool_table, :number, 2) # => 6
In this code, we are getting the row under the key :number
and then taking the element in the second position. It also works perfectly with :bag
or :duplicate_bag
:
cool_table = :ets.new(:cool_table, [:bag]) :ets.insert(cool_table, [{:number, 5}, {:number, 6}]) IO.inspect :ets.lookup_element(cool_table, :number, 2) # => 5,6
If you would like to simply check if some key is present in the table, use member/2
, which returns either true
or false
:
cool_table = :ets.new(:cool_table, [:bag]) :ets.insert(cool_table, [{:number, 5}, {:number, 6}]) if :ets.member(cool_table, :number) do IO.inspect :ets.lookup_element(cool_table, :number, 2) # => 5,6 end
You may also get the first or the last key in a table by using first/1
and last/1
respectively:
cool_table = :ets.new(:cool_table, [:ordered_set]) :ets.insert(cool_table, [{:b, 3}, {:a, 100}]) :ets.last(cool_table) |> IO.inspect # => :b :ets.first(cool_table) |> IO.inspect # => :a
On top of that, it is possible to determine the previous or the next key based on the provided one. If such a key cannot be found, :"$end_of_table"
will be returned:
cool_table = :ets.new(:cool_table, [:ordered_set]) :ets.insert(cool_table, [{:b, 3}, {:a, 100}]) :ets.prev(cool_table, :b) |> IO.inspect # => :a :ets.next(cool_table, :a) |> IO.inspect # => :b :ets.prev(cool_table, :a) |> IO.inspect # => :"$end_of_table"
Note, however, that the table traversal using functions like first
, next
, last
or prev
is not isolated. It means that a process may remove or add more data to the table while you are iterating over it. One way to overcome this issue is by using safe_fixtable/2
, which fixes the table and ensures that each element will be fetched only once. The table remains fixed unless the process releases it:
cool_table = :ets.new(:cool_table, [:bag]) :ets.safe_fixtable(cool_table, true) :ets.info(cool_table, :safe_fixed_monotonic_time) |> IO.inspect # => {256000, [{#PID<0.69.0>, 1}]} :ets.safe_fixtable(cool_table, false) # => table is released at this point :ets.info(cool_table, :safe_fixed_monotonic_time) |> IO.inspect # => false
Lastly, if you'd like to find an element in the table and remove it, employ the take/2
function:
cool_table = :ets.new(:cool_table, [:ordered_set]) :ets.insert(cool_table, [{:b, 3}, {:a, 100}]) :ets.take(cool_table, :b) |> IO.inspect # => [b: 3] :ets.take(cool_table, :b) |> IO.inspect # => []
Delete Operations
Okay, so now let's say you no longer need the table and wish to get rid of it. Use delete/1
for that:
cool_table = :ets.new(:cool_table, [:ordered_set]) :ets.delete(cool_table)
Of course, you may delete a row (or multiple rows) by its key as well:
cool_table = :ets.new(:cool_table, []) :ets.insert(cool_table, [{:b, 3}, {:a, 100}]) :ets.delete(cool_table, :a)
To clear out the entire table, utilize delete_all_objects/1
:
cool_table = :ets.new(:cool_table, []) :ets.insert(cool_table, [{:b, 3}, {:a, 100}]) :ets.delete_all_objects(cool_table)
And, lastly, to find and remove a specific object, use delete_object/2
:
cool_table = :ets.new(:cool_table, [:bag]) :ets.insert(cool_table, [{:a, 3}, {:a, 100}]) :ets.delete_object(cool_table, {:a, 3}) :ets.lookup(cool_table, :a) |> IO.inspect # => [a: 100]
Converting the Table
An ETS table can be converted to a list anytime by using the tab2list/1
function:
cool_table = :ets.new(:cool_table, [:bag]) :ets.insert(cool_table, [{:a, 3}, {:a, 100}]) :ets.tab2list(cool_table) |> IO.inspect # => [a: 3, a: 100]
Remember, however, that fetching the data from the table by the keys is a very fast operation, and you should stick to it if possible.
You may also dump your table to a file using tab2file/2
:
cool_table = :ets.new(:cool_table, [:bag]) :ets.insert(cool_table, [{:a, 3}, {:a, 100}]) :ets.tab2file(cool_table, 'cool_table.txt') |> IO.inspect # => :ok
Note that the second argument should be a charlist (a single-quoted string).
There are a handful of other operations available that can be applied to the ETS tables, and of course we are not going to discuss them all. I really recommend skimming through the Erlang documentation on ETS to learn more.
Persisting the State With ETS
To summarize the facts that we have learned so far, let's modify a simple program that I have presented in my article about GenServer. This is a module called CalcServer
that allows you to perform various calculations by sending requests to the server or fetching the result:
defmodule CalcServer do use GenServer def start(initial_value) do GenServer.start(__MODULE__, initial_value, name: __MODULE__) end def init(initial_value) when is_number(initial_value) do {:ok, initial_value} end def init(_) do {:stop, "The value must be an integer!"} end def sqrt do GenServer.cast(__MODULE__, :sqrt) end def add(number) do GenServer.cast(__MODULE__, {:add, number}) end def multiply(number) do GenServer.cast(__MODULE__, {:multiply, number}) end def div(number) do GenServer.cast(__MODULE__, {:div, number}) end def result do GenServer.call(__MODULE__, :result) end def handle_call(:result, _, state) do {:reply, state, state} end def handle_cast(operation, state) do case operation do :sqrt -> {:noreply, :math.sqrt(state)} {:multiply, multiplier} -> {:noreply, state * multiplier} {:div, number} -> {:noreply, state / number} {:add, number} -> {:noreply, state + number} _ -> {:stop, "Not implemented", state} end end def terminate(_reason, _state) do IO.puts "The server terminated" end end CalcServer.start(6.1) CalcServer.sqrt CalcServer.multiply(2) CalcServer.result |> IO.puts # => 4.9396356140913875
Currently our server doesn't support all mathematical operations, but you may extend it as needed. Also, my other article explains how to convert this module to an application and take advantage of supervisors to take care of the server crashes.
What I'd like to do now is add another feature: the ability to log all the mathematical operations that were performed along with the passed argument. These operations will be stored in an ETS table so that we will be able to fetch it later.
First of all, modify the init
function so that a new named private table with a type of :duplicate_bag
is created. We are using :duplicate_bag
because two identical operations with the same argument may be performed:
def init(initial_value) when is_number(initial_value) do :ets.new(:calc_log, [:duplicate_bag, :private, :named_table]) {:ok, initial_value} end
Now tweak the handle_cast
callback so that it logs the requested operation, prepares a formula, and then performs the actual computation:
def handle_cast(operation, state) do operation |> prepare_and_log |> calculate(state) end
Here is the prepare_and_log
private function:
defp prepare_and_log(operation) do operation |> log case operation do :sqrt -> fn(current_value) -> :math.sqrt(current_value) end {:multiply, number} -> fn(current_value) -> current_value * number end {:div, number} -> fn(current_value) -> current_value / number end {:add, number} -> fn(current_value) -> current_value + number end _ -> nil end end
We are logging the operation right away (the corresponding function will be presented in a moment). Then return the appropriate function or nil
if we don't know how to handle the operation.
As for the log
function, we should either support a tuple (containing both the operation's name and the argument) or an atom (containing only the operation's name, for example, :sqrt
):
def log(operation) when is_tuple(operation) do :ets.insert(:calc_log, operation) end def log(operation) when is_atom(operation) do :ets.insert(:calc_log, {operation, nil}) end def log(_) do :ets.insert(:calc_log, {:unsupported_operation, nil}) end
Next, the calculate
function, which either returns a proper result or a stop message:
defp calculate(func, state) when is_function(func) do {:noreply, func.(state)} end defp calculate(_func, state) do {:stop, "Not implemented", state} end
Finally, let's present a new interface function to fetch all the performed operations by their type:
def operations(type) do GenServer.call(__MODULE__, {:operations, type}) end
Handle the call:
def handle_call({:operations, type}, _, state) do {:reply, fetch_operations_by(type), state} end
And perform the actual lookup:
defp fetch_operations_by(type) do :ets.lookup(:calc_log, type) end
Now test everything:
CalcServer.start(6.1) CalcServer.sqrt CalcServer.add(1) CalcServer.multiply(2) CalcServer.add(2) CalcServer.result |> IO.inspect # => 8.939635614091387 CalcServer.operations(:add) |> IO.inspect # => [add: 1, add: 2]
The result is correct because we have performed two :add
operations with the arguments 1
and 2
. Of course, you may further extend this program as you see fit. Still, don't abuse ETS tables, and employ them when it is really going to boost the performance—in many cases, using immutables is a better solution.
Disk ETS
Before wrapping up this article, I wanted to say a couple of words about disk-based ETS tables or simply DETS.
DETS are pretty similar to ETS: they use tables to store various data in the form of tuples. The difference, as you've guessed, is that they rely on file storage instead of memory and have fewer features. DETS have functions similar to the ones we discussed above, but some operations are performed a bit differently.
To open a table, you need to use either open_file/1
or open_file/2
—there is no new/2
function like in the :ets
module. Since we don't have any existing table yet, let's stick to open_file/2
, which is going to create a new file for us:
:dets.open_file(:file_table, [])
The filename is equal to the table's name by default, but this can be changed. The second argument passed to the open_file
is the list of options written in the form of tuples. There are a handful of available options like :access
or :auto_save
. For instance, to change a filename, use the following option:
:dets.open_file(:file_table, [{:file, 'cool_table.txt'}])
Note that there is also a :type
option that may have one of the following values:
:set
:bag
:duplicate_bag
These types are the same as for the ETS. Note that DETS cannot have a type of :ordered_set
.
There is no :named_table
option, so you can always use the table's name to access it.
Another thing worth mentioning is that the DETS tables must be properly closed:
:dets.close(:file_table)
If you don't do this, the table will be repaired the next time it is opened.
You perform read and write operations just like you did with ETS:
:dets.open_file(:file_table, [{:file, 'cool_table.txt'}]) :dets.insert(:file_table, {:a, 3}) :dets.lookup(:file_table, :a) |> IO.inspect # => [a: 3] :dets.close(:file_table)
Bear in mind, though, that DETS are slower than ETS because Elixir will need to access the disk which, of course, takes more time.
Note that you may convert ETS and DETS tables back and forth with ease. For example, let's use to_ets/2
and copy the contents of our DETS table in-memory:
:dets.open_file(:file_table, [{:file, 'cool_table.txt'}]) :dets.insert(:file_table, {:a, 3}) my_ets = :ets.new(:my_ets, []) :dets.to_ets(:file_table, my_ets) :dets.close(:file_table) :ets.lookup(my_ets, :a) |> IO.inspect # => [a: 3]
Copy the ETS's contents to DETS using to_dets/2
:
my_ets = :ets.new(:my_ets, []) :ets.insert(my_ets, {:a, 3}) :dets.open_file(:file_table, [{:file, 'cool_table.txt'}]) :ets.to_dets(my_ets, :file_table) :dets.lookup(:file_table, :a) |> IO.inspect # => [a: 3] :dets.close(:file_table)
To sum up, disk-based ETS is a simple way to store contents in the file, but this module is slightly less powerful than ETS, and the operations are slower as well.
Conclusion
In this article, we have talked about ETS and disk-based ETS tables that allow us to store arbitrary terms in memory and in files respectively. We have seen how to create such tables, what the available types are, how to perform read and write operations, how to destroy tables, and how to convert them to other types. You may find more information about ETS in the Elixir guide and on the Erlang official page.
Once again, don't overuse ETS tables, and try to stick with immutables if possible. In some cases, however, ETS may be a nice performance boost, so knowing about this solution is helpful in any case.
Hopefully, you've enjoyed this article. As always, thank you for staying with me, and see you really soon!