loading...
Cover image for How I created my own programming language based on JavaScript

How I created my own programming language based on JavaScript

sergeyshpadyrev profile image Sergey Shpadyrev ・7 min read

I have worked with many programming languages during my career. I've used ActionScript 3 for Flash games. I've used Java for backend and Android games. I've used Scala for the backend. I've used JavaScript for NodeJS backend, React web applications, and React Native mobile applications. I've written a million simple scripts in Python.

But no matter what language I used, I had a feeling that the syntax of this language is too verbose, full of excesses, noise, and a syntactic boilerplate that interferes with understanding the code. So I had decided to create my own programming language.

First, I defined a few parameters to measure language quality. So, in my opinion, the perfect language should be:

  • Well readable
  • Laconic
  • Consistent
  • Pure and beautiful

I took a few programming languages that I'd been familiar with and started to improve them by reducing the unneeded syntax. But whatever language I'd taken, in the end, I always got LISP.

It's right. LISP is the most consistent and laconic programming language. But nowadays, Common Lisp and all its offsprings (except Clojure maybe) are more toys to play with than a language to use in production. Moreover, LISP has one awful disadvantage in terms of beauty - too many parentheses.

If today you need to choose a language for business purposes, most probably you will take JavaScript. It has a giant friendly community and package manager NPM with tons of well-made libraries. What's more important, JavaScript itself is a perfectly designed language that allows writing code in two different ways: functional and OOP.

I prefer to write code in JavaScript in a purely functional style.
So my code looks like this:

const incrementNumbers = numbers => numbers.map(number => number + 1)
const takeNumbersGreaterThan = threshold => numbers => numbers.filter(number => number > threshold)

const func = (numbers, threshold) => {
    const incrementedNumbers = incrementNumbers(numbers)
    const filteredNumbers = takeNumbersGreaterThan(threshold)(incrementedNumbers)
    return filteredNumbers
}
Enter fullscreen mode Exit fullscreen mode

The code above doesn't make any real sense. It can be written much simpler but it's a great example of what's wrong in JavaScript when you write code in a functional way. It has too many syntax noises like const and return.

So I like LISP and JavaScript but both of them have disadvantages. LISP has too many parentheses and JavaScript has too many syntax noises. What to do?

So I decided to merge LISP and JavaScript. I took syntax from LISP but reduced the number of parentheses using meaningful tabs like in Python. And I took the platform from JavaScript so my language is being transpiled to JS so it has full interop with it and most operators in my language work just like in JavaScript.

So meet Una - the universal language of unified symmetries.

Syntax

Application order

The most important thing you should know about Una is how application order works.
You can set the application order in two different ways:

  • wrap up expression with parentheses
  • move expression to the next line with additional indentation

Let's look at the example. We won't use real operators, just letters.
Here we apply a to b:

a b
Enter fullscreen mode Exit fullscreen mode

Here we apply a to the result of application of b to c:

a (b c)
Enter fullscreen mode Exit fullscreen mode

This expression we can also write using indentation:

a
  b c
Enter fullscreen mode Exit fullscreen mode

I think the underlying idea is pretty obvious but let's look at more complicated example:

a (b (c d)) (e (f g))
Enter fullscreen mode Exit fullscreen mode

It can be writte like this:

a
  b (c d)
  e (f g)
Enter fullscreen mode Exit fullscreen mode

or even like this:

a
  b
    c d
  e
    f g
Enter fullscreen mode Exit fullscreen mode

Assignment

The most used operator in any programming language is assignment =. Because of Una is pure functional language = is not really assignment but only declaration of a constant.

= name 'John'
Enter fullscreen mode Exit fullscreen mode

This operator takes its second parameter and assigns it to the first one. If there're more parameters, at first it applies the second parameter to the rest of them and then assigns the result to the first one. Sounds complicated but it's simple. It just means that we can write assigning expression with parantheses:

= z (calculate x y)
Enter fullscreen mode Exit fullscreen mode

or without:

= z calculate x y
Enter fullscreen mode Exit fullscreen mode

Arithmetical operators

Una has all basic arithmetical operators that work the same as in JavaScript:

  • + - addition
  • - - subtraction
  • * - multiplication
  • / - division
  • % - modulo

Example:

= a (+ 1 2)
= b (- 2 1)
= c (* 3 2)
= d (/ 4 2)
= e (% 5 2)
Enter fullscreen mode Exit fullscreen mode

Comparison operators

Una has all basic comparison operators that work the same as in JavaScript.

= a (== 1 1)
= b (~= 1 '1')
= c (!= 1 '1')
= d (!~= 1 '2')
= e (> 2 1)
= f (>= 2 1)
= g (< 1 2)
= h (<= 1 2)
Enter fullscreen mode Exit fullscreen mode

The only thing you should mention that == in Una is the strict comparison like === in JavaScript. For unstrict comparison you should use ~=.

Logical operators

The same with logical operators. They're a little bit different in Una:

= a (& true false)
= b (| true false)
= c (! true)
Enter fullscreen mode Exit fullscreen mode

Conditional operators

Una has two conditional operators.

Ternary conditional operator works just like in JavaScript:

= value
  ? (> 2 1) "Greater" "Less"
Enter fullscreen mode Exit fullscreen mode

Returnable conditional operator ?! is used in sync/async functions and sync/async computations to return value by some condition. For example, following code in function will return "One" if number equals 1:

?! (== number 1) "One"
Enter fullscreen mode Exit fullscreen mode

Collections

Una has two collection types: array :: and object :.

Here's an example of creating a array of numbers

= numbers :: 1 2 3
Enter fullscreen mode Exit fullscreen mode

Here's an example of creating an object of user:

= user :
  name 'John'
  age 13
  parents :
    mother :
      name 'Alice'
      age 42
    father :
      name 'Bob'
      age 39
Enter fullscreen mode Exit fullscreen mode

Just like in JavaScript you can deconstruct objects and arrays

= numbers :: 1 2 3
= (:: one two three) numbers
console.log one

= user : (name 'John') (age 12)
= (: name) user
console.log name
Enter fullscreen mode Exit fullscreen mode

And also just like in JavaScript when creating objects and array you can use already declared consts:

= a 1
= numbers :: a 2 3

= name 'John'
= user :
  name
  age 13
Enter fullscreen mode Exit fullscreen mode

To get a field from map or element from array you can use .:

= list :: 1 2 3
= map : (a 1) (b 2)

console.log (. list 0)
console.log (. map 'a')
Enter fullscreen mode Exit fullscreen mode

Also . is used to call methods on any object.
You can do it like this:

= numbers :: 1 2 3
= incrementedNumbers
  numbers.map (-> x (+ x 1))
Enter fullscreen mode Exit fullscreen mode

or like this:

= numbers :: 1 2 3
= incrementedNumbers
  .map numbers (-> x (+ x 1))
Enter fullscreen mode Exit fullscreen mode

Symmetries

The best feature of Una is arrow symmetries.

Sync symmetry

Right sync arrow -> is function. First parameter is function parameters. Last parameter is return of the function. All parameters between are simple code lines.

= sum -> (x y)
  + x y

= onePlusTwo -> ()
  = one 1
  = two 2
  + one two
Enter fullscreen mode Exit fullscreen mode

Calling function is just an application of it to parameters:

= a (sum 1 2)
= b sum 1 2
= c
  sum 1 2
= d sum
  1
  2
Enter fullscreen mode Exit fullscreen mode

To call parameterless function just use ()

= randomNumber
  Math.random ()
Enter fullscreen mode Exit fullscreen mode

These functions can be used as lambda functions and be passed as a parameter to another function or can be returned as value from another function.

Left sync arrow <- is immediatly invoked function. So it allows to isolate some part of code and run it.
In following example result immediatly calculates as 3.

= result <-
  = a 1
  = b 2
  + a b
Enter fullscreen mode Exit fullscreen mode

It's pretty good when you need to calculate something based on conditions:

<-
  ?! (== value 0) "Zero"
  ?! (== value 1) "One"
  ? (< value 10) "Less than ten" "More than ten"
Enter fullscreen mode Exit fullscreen mode

Async symmetry

Right async arrow --> is async function.

= getUserPosts --> user
  database.loadPosts user.postIds
Enter fullscreen mode Exit fullscreen mode

Left async arrow <-- is await.

= checkIfUserIsAdmin --> userId
  = user <-- (database.loadUser userId)
  == user.role 'admin'
Enter fullscreen mode Exit fullscreen mode

Error symmetry

Right error arrow |-> is try-catch operator. First parameter is catch function. Other parameters are try lines. Unlike JavaScript try-catch operator |-> in Una always returns some value and it doesn't have finally block.

|->
  <-
    = getName null
    getName ()
  -> error
    console.log error
    'John'
Enter fullscreen mode Exit fullscreen mode

If you need to run async code in try catch user <-- instead of <- in try or --> instead -> in catch:

|->
  <--
    getNameAsync ()
  --> error
    console.log error
    "John"
Enter fullscreen mode Exit fullscreen mode

Left error arrow <-| is throwing error.

= addOneToNumber -> number
  ?! (isNaN number)
    <-| "number is not valid"
  + number 1
Enter fullscreen mode Exit fullscreen mode

Module symmetry

Una modules are fully compatiable with JavaScript. You can import JavaScript modules to Una and you can import Una modules to JavaScript.

Right module arrow =-> is import.
If you pass modules: 'require' to babel plugin options it works as require.
If you pass modules: 'import' or pass nothing to babel plugin options it works as import.

=-> './index.css'
=-> 'react' React
=-> 'react' (: createElement)
=-> 'react' React (: createElement)
Enter fullscreen mode Exit fullscreen mode

Left module arrow <-= is export.
If you pass modules: 'require' to babel plugin options it works as modules.export =.
If you pass modules: 'import' or pass nothing to babel plugin options it works as export.

Default module export:

<-= a
Enter fullscreen mode Exit fullscreen mode

Constant export:

<-= = a 1
Enter fullscreen mode Exit fullscreen mode

Chaining symmetry

Right chainging arrow |> is chaining by last parameter.
If you want to use such functional programming libraries as rambda you will find |> operator very useful.
In following example phone constant equals 'IPHONE':

=-> 'ramda' R
= electronics ::
  :
    title ' iPhone '
    type 'phone'

= phones |>
  electronics
  R.find
    R.propEq 'type' 'phone'
  R.prop 'title'
  R.toUpper
  R.trim
Enter fullscreen mode Exit fullscreen mode

Left chainging arrow <| is chaining by last parameter.

Because of Lisp-like application order it's hard to do chains with default JavaScript array methods. Look how ugly it looks:

= sum .reduce
  .filter
    .map (:: 1 2 3) (-> x (+ x 1))
    -> x (> x 2)
  -> (x y) (+ x y)
  0
Enter fullscreen mode Exit fullscreen mode

With <| it can be rewritten as:

= sum <| (:: 1 2 3)
  .map (-> x (+ x 1))
  .filter (-> x (> x 2))
  .reduce (-> (x y) (+ x y)) 0
Enter fullscreen mode Exit fullscreen mode

React

There's no JSX in Una. So to work with React instead of JSX you should use React.createElement, where first parameter is component, second parameters is passing props, and the rest of parameters are children.

=-> 'react' React

= (: (createElement e)) React

= Component -> ((: count name))
  e div (: (style (: backgroundColor 'red')))
    e div : count
    e div : name
Enter fullscreen mode Exit fullscreen mode

For styling I recommend to use styled-components. I will make code much cleaner. Here's the short example of React app with styled components:

=-> './index.css'
=-> 'react' React
=-> 'react-dom' ReactDOM
=-> './styles' S

= (: (createElement e)) React

= App -> ((: name))
  = (:: count setCount) (React.useState 0)
  e S.Container :
    e S.Hello (: (color 'green')) 'Hello, '
    e S.Name : name
    e S.IncrementCount
      : (onClick (-> () (setCount (+ count 1))))
      'Press me'
    e S.Count : count

ReactDOM.render
  e App (: (name 'John'))
  document.getElementById 'root'
Enter fullscreen mode Exit fullscreen mode

In the example above : without arguments is just an empty object.

Afterword

So you can look at the documentation and find more examples in the Github repo of Una. Try to run examples, write your own code in Una and feel free to share your feedback. It's a lot left to do but I fell I'm on the right way.

Discussion

pic
Editor guide
Collapse
shadowtime2000 profile image
shadowtime2000

Cool project!