JavaScript and Julia Difference in Core Concepts

A discussion of inheritance, string interpolation, null values, iterators and generators

When learning a new language, it is usually very quick to pick up the core stuff like if statements, while loops, variable assignments etc.

I wrote this guide for people who don’t want to spend time with articles which meticulously go through every feature of language you want to learn. If you already know Julia or JavaScript and want to learn the other one, you don’t want plow through obvious stuff. You want to get to the meat.

You already know stuff like inheritance and iterators exist. What you want to know is how they are different in the language you want to learn, compared to the one you know.

I am writing this from a Julia perspective learning JavaScript, but I think JavaScript developers who want to learn Julia better can read this as well.

Defining Types or Classes

Define a type representing a point in Julia:

type Point
x::Int
y::Int
end

julia> p = Point(2, 3)

JavaScript example:

class Point {
constructor (x, y) {
this.x = x
this.y = y
}
}

es6> p = new Point(2, 3)

Class Inheritance

This is an area which is hard to compare, because Julia and JavaScript are entirely different. ES6 follows and object oriented approach while Julia is not really object oriented.

In JavaScript you can inherit functionality, so you can define some behavior in base class and add to that functionality in subclasses.

However JavaScript has not concept of abstract types of interfaces. Making say an abstract Shape type which a Circle and Rectangle must inherit is rather pointless in JavaScript unless you actually add some functionality to it.

Julia in contrast has no implementation inheritance but allows you to create hierarchies of abstract types which you can implement.

Let us look at something we can implement in both approaches.

abstract type Shape end

struct Rectangle <: Shape
x::Float64
y::Float64
width::Float64
height::Float64
end

struct Circle <: Shape
radius::Float64
end

describe(s::Shape) = "a $(shape_name(s)) shape"

function shape_name(s::Rectangle)
"rectangle($(s.x), $(s.y), $(s.width), $(s.height))"
end

shape_name(c::Circle) = "circle($(c.radius))"

In this approach we are creating a Circle and Rectangle type which are both subtypes of Shape. In OOP parlance they are subclasses of Shape.

All subtypes of Shape have to implement the shape_name function to allow us to call describe to give a text representation of any given type.

Here is an example of usage:

julia> c = Circle(5);

julia> r = Rectangle(10, 5, 40, 50);

julia> describe(c)
"a circle(5.0) shape"

julia> describe(r)
"a rectangle(10.0, 5.0, 40.0, 50.0) shape"

This is achieving inheritance like behavior through the usage of the template method pattern, which is the primary way of simulating inheritance in Julia.

Lets look at the JavaScript version.

class Shape {
describe() {
return `a ${this.shape_name()} shape`
}
}

class Circle extends Shape {
constructor(radius) {
super()
this.radius = radius
}

shape_name() {
return `circle(${this.radius})`
}
}

class Rectangle extends Shape {
constructor(x, y, width, height) {
super()
this.x = x
this.y = y
this.width = width
this.height = height
}

shape_name() {
return `rectangle(${this.x}, ${this.y}, ${this.width}, ${this.height})`
}
}

This is quite similar, and also utilizes the template method pattern. However JavaScript supporting implementation inheritance can call super in overloaded methods.

So we could do a twist to this code (I am leaving out the constructors for clarity):

class Shape {
describe() {
return "shape"
}
}

class Circle extends Shape {
describe() {
return "circle " + super.describe()
}
}

class Rectangle extends Shape {
describe() {
return "rectangle " + super.describe()
}
}

So you can see we are calling the describe() method of the superclass. That is not possible in Julia, because there never is a superclass.

When we run this in the REPL, we get this result:

es6> r = new Rectangle(4, 5, 20, 30)
Rectangle { x: 4, y: 5, width: 20, height: 30 }
es6> c = new Circle(5)
Circle { radius: 5 }
es6> r.describe()
'rectangle shape'
es6> c.describe()
'circle shape'

Actually, I investigate this more thoroughly and discovered you can actually hack it to invoke a method of super type in Julia. Although if you look at how it is done, I think you can agree, it is not something the language is intended for.

First lets redefine describe in our base type.

describe(s::Shape) = "shape"

Then we’ll use the invoke function which allows us to invoke a specific implementation of a function.

function describe(r::Rectangle)
"rectangle " * invoke(describe, Tuple{Shape}, r)
end

In this case we are telling invoke to call the function describe, and select the implementation where the signature of the arguments are Tuple{Shape}. If wanted to call an implementation taking two integer arguments, I would write Tuple{Integer, Integer} instead.

This version will work, but it is not bullet proof. Most specifically, it will break if we should decide to change the super type. Here is a version that will handle any super type.

function describe(r::Rectangle)
SuperType = supertype(typeof(r))
"rectangle " * invoke(describe, Tuple{SuperType}, r)
end

Obviously this is not meant to be a common pattern for writing Julia code. However Julia programmers will usually solve the problem OOP programmers solve with implementation inheritance using entirely different approaches and features.

String Interpolation

Assign values to variables

julia> name = "Erik"
"Erik"
julia> age = 41
41

Interpolate variables into strings.

julia> "Hello, $name. You are $age."
"Hello, Erik. You are 41."

JavasScript example:

es6> name = "Erik"
"Erik"
es6> age = 41
41

Interpolate variables into strings.

es6> `Hello, ${name}. You are ${age}.`
"Hello, Erik. You are 41."

Both languages give some more detailed control of how strings are interpolate. In both languages we can prefix the text strings.

Define a regular expression.

julia> r"(foo|bar)"
r"(foo|bar)"

julia> typeof(r"(foo|bar)")
Regex

Define an array of 8 bit data elements.

julia> b"ABC"
3-element Base.CodeUnits{UInt8,String}:
0x41
0x42
0x43

All of these are just short for writing the macros:

julia> @r_str "(foo|bar)"
r"(foo|bar)"

julia> @b_str "ABC"
3-element Base.CodeUnits{UInt8,String}:
0x41
0x42
0x43

So why is this defined in terms of macros rather than say functions? There is a great performance benefit r"(foo|bar)" is by the macro turned into Regex("(foo|bar)") which is evaluated and place right into the syntax tree. Hence if this is located in a loop, the regular expression is only parsed once, and not on each iteration of the loop.

JavaScript does not have macros, so instead it just invokes a regular function, with the same name as the prefix. So say we create a function foobar

function foobar(strings, a, b) {
console.log(strings)
console.log(a)
console.log(b)
}

We can then prefix a template string with it:

es6> foobarfirst ${20}
[ 'first ', '' ]
20
undefined
undefined
es6> foobar`first ${20} second ${false}`
[ 'first ', ' second ', '' ]
20
false

So basically this allows you to customize how the string interpolation is done.

Null Values

Julia does not really have null, but libraries are designed to return an instance of the Nothing type, which is a singleton whenever data was not found.

julia> a = nothing

julia> typeof(a)
Nothing

For statistical work and science where we deal with recordings of data and where some of it is missing, we use missing values instead of nothing.

julia> b = missing
missing

julia> typeof(b)
Missing
julia> A = [2, 3, missing];
julia> sum(skipmissing(A))
5

JavaScript Example:

es6> a = null
null
es6> typeof(a)
"object"

Undefined

JavaScript and Julia deal with undefined or uninitialized values entirely different.

I’ve made this nonsensical code example, where an inner-constructor for the Point type fails to initialize x and y.

struct Point
x::Int
y::Int
function Point()
new()
end
end

The result of this is that the values become uninitialized, and essentially random.

julia> p = Point()
Point(4663002520, 0)

julia> p = Point()
Point(4658742296, 0)

However their type does not change.

julia> typeof(p.x)
Int64

I can use the undef value to construct uninitialized arrays in Julia:

julia> Array{Int}(undef, 3)
3-element Array{Int64,1}:
0
4294967297
0

However each value in this case is still an integer. They are not their own type.

However I cannot access members that don’t exist. That will produce a runtime error:

julia> p.z
ERROR: type Point has no field z
Stacktrace:
[1] getproperty(::Any, ::Symbol) at ./sysimg.jl:18

JavaScript in contrast is far more forgiving.

es6> p.z
undefined
es6> typeof(p.z)
"undefined"

You can assign undefined to variables

es6> c = undefined
undefined
es6> typeof(c)
"undefined"

Symbols

This is another era where Julia and JavaScript are profoundly different. In Julia symbols mainly exist to facilitate meta programming. They offer a way to represent identifiers in the language. In Julia "batman" is a string while :batman is a symbol.

julia> :batman == "batman"
false

julia> :batman == :batman
true

julia> :batman == Symbol("batman")
true

Store a Julia expression in a variable :() quotes code, turning it into data.

julia> expression = :(batman = "batman")
:(batman = "batman")

If we inspect this data structure representing a Julia expression, it may become more clear why we distinguish between symbols and strings in Julia.

julia> expression.head
:(=)

julia> expression.args
2-element Array{Any,1}:
:batman
"batman"

julia> [typeof(arg) for arg in expression.args]
2-element Array{DataType,1}:
Symbol
String

However JavaScript has an entirely different rational for using symbols. The language does not support representing code as data, and so symbols have no usage in this regard. Instead they exist to make unique keys in dictionaries.

Symbols are created with Symbol("batman"), however equality works different from Julia.

es6> batman = Symbol("batman")
Symbol(batman)
es6> batman == batman
true
es6> batman == Symbol("batman")
false
es6> Symbol("batman") == Symbol("batman")
false

As you can see, every symbol object is unique regardless of the string we pass as an argument. So why pass a string as argument? Most likely so it is more user-friendly to look at the value of a symbol.

Symbols are used extensively with dictionaries to get unique lookup keys. To use an .

es6> ironman = 7
7
es6> heros = {superman: 10, [ironman]: 20, [batman]: 30}
{7: 20, superman: 10, Symbol(batman): 30}
es6> heros.superman
10

Only works for strings:

es6> heros.ironman
undefined
es6> heros[ironman]
20

batman is a symbol and not a string:

es6> heros[batman]
30
es6> heros.batman
undefined

ironman is equivalent to 7.

es6> heros[7]
20

However the batman symbol will only ever be equivalent to a specific instance created.

es6> heros[Symbol("batman")]
undefined

Generators and Iterators

For demonstration purposes, let us just make our own array of integers, to iterate over.

struct IntArray
elements::Array{Int}
end

Implement the iteration protocol.

function iterate(a::IntArray, i = 1)
if i > length(a.elements)
nothing
else
(a.elements[i], i + 1)
end
end

Make some test data

julia> nums = IntArray([4, 5, 6])
IntArray([4, 5, 6])

Try out iteration:

julia> for n in nums
println(n)
end
4
5
6

julia> [2x for x in nums]
3-element Array{Int64,1}:
8
10
12

Let us look at the same in JavaScript. Iterators in JavaScript rely on utilizing symbols as keys in a JavaScript object (dictionary), which is why I had to cover symbols first.

class IntArray {
constructor(elements) {
this.elements = elements
}
[Symbol.iterator]() {
let i = 0
let elements = this.elements
return {
next() {
let value = elements[i]
i += 1
return {
done: i > elements.length,
value: value
}
}
}
}
}

Try out iteration

es6> nums = new IntArray([4, 5, 6])
IntArray {elements: Array(3)}

es6> for (n of nums) {
console.log(n)
}
4
5
6

Unfortunately iteration is not nearly as useful as in Julia or Python, because functions such as map and filter are only defined on arrays, not iterable objects.

As was the case when I , creating an iterator is quite verbose. However just like Python JavaScript ES6 actually has quite a nice syntax for generators. Generators are built on top of the concept of coroutines. Coroutines are similar to the idea of threads, with the exception that the operating system switches execution between threads without any user involvement. In a coroutine the developer has to specifically write code however to relinquish control and hand it over to the scheduler which will switch to another coroutine. It will not switch back until that coroutine relinquish control.

Below is an example. You must prefix a function name with * if you want it to be a generator.

function *generator(elements) {
for (element of elements) {
yield element
}
}

The yield statement is where you coroutine is relinquishing control and handing it over to the scheduler. Let us try it out:

es6> g = generator([4, 8, 10])
Object [Generator] {}
es6> g.next()
{ value: 4, done: false }
es6> g.next()
{ value: 8, done: false }
es6> g.next()
{ value: 10, done: false }
es6> g.next()
{ value: undefined, done: true }

Just like an iterator, a generator automatically gets a next() method. The way this code works is that you are in the main coroutine in your REPL environment. When you run g.next() the scheduler switches to a different coroutine and the REPL coroutine blocks. However when it hits yield it gives up control, and the scheduler puts the REPL coroutine back in charge, while also passing on a value from yield. You can do anything you want now, including resuming the coroutine by calling g.next() again.

We can create a similar coroutine approach to iteration in Julia, but we have to utilize channels to pass data between coroutines, as Julia does not use yield the same was as JavaScript and Python.

function generator(elements)
Channel() do channel
for element in elements
put!(channel, element)
end
end
end

Then we can use it in similar fashion:

julia> iterate(g)
(2, nothing)

julia> iterate(g)
(4, nothing)

julia> iterate(g)
(8, nothing)

Remember the Julia protocol for iteration does not use next but iterate. Channels implement the iterate protocol in Julia, which is why this works.

For more details about the Julia approach, read my take in , as well as my talk about .

Written by

Geek dad, living in Oslo, Norway with passion for UX, Julia programming, science, teaching, reading and writing.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store