Skip to content

Instantly share code, notes, and snippets.

@JAForbes
Last active August 4, 2018 23:36
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JAForbes/f677852f99c39dcf9089bd368fff7344 to your computer and use it in GitHub Desktop.
Save JAForbes/f677852f99c39dcf9089bd368fff7344 to your computer and use it in GitHub Desktop.
A simple Javascript

Warning this is purely a thought experiment and will probably go nowhere. The language semantics will likely be incongruent and non-rigourous. Proceed at your own risk.

Philosophy

Javascript always wanted to be a functional language. But for historical, political and perhaps even sensible reasons, JS's original mission was never truly fulfilled.

But what if, we take JS and start removing things. We can remove mutation, remove variables, remove classes, remove variadic signatures, remove default arguments and on and on.

What if we go back to ES1 and re-evaluate a future unperturbed by corporate circumstance.

If we do that we get a tiny expressive language with first class functions and prototypes. This language was built for extension, flexibility and interactivity

Prototypes are a little messy, but what if prototype extensions were module scoped? Great!

First class functions are a little confusing, but perhaps if we build everything on top of them then the pay off for learning them suddenly seems more relevant.

So I present a simple Javascript. A Javascript where almost every feature is removed and all that is added is there as a catalyst for our own local extensibility.

Functions

Only one type of function.

add = a => b => a + b

add (2) (3) //=> 5

Function's can be multi-line, multi-statement, the final expression is always returned.

pythagoras = a => b => 
  
  aSquared = a ^ 2
  bSquared = b ^ 2
  
  sqrt ( aSquared + bSquared ) 

pythagoras (3) (4) //=> 5

Functions only ever accept one argument.

Methods

Methods are significantly different to Javascript. Take this fairly unintuitive example:

o = 
  { value: 1
  , f: x => this => 
      this.value = x
  }
  

The above is valid syntax but the behaviour may surprise you if you are used to Javascript.

You can call this "method" manually like so: o.f (x) (o), but it's not idiomatic to do that.

Instead we tend to call methods with infix syntax: o f x, here f acts as any other operator. That's just sugar for o.f (x) (o).

There's no implicit this context variable. Instead, infix will pass in the left hand side on the 2nd invocation for you.

Additionally: o.f doesn't actually mutate o.value at all. It returns a new object that is exactly the same structure as o but with a new value.

Yes, this is prototypical inheritance.

o2 = o f 2

o2 isPrototypeOf o //=> true

o2.value //=> 2
o.value //=> 1

o.f == o2.f //=> true
o.g == o2.g //=> true

In this language there is no special this keyword. Instead it's just a function parameter like any other. You can manually pass in your own context o.f (2) (o2), which is similar to Javascript's: o.f.call(o2, 2)

Mutation

By default, assignment is immutable in this language. The operations below return a completely new structure as an expression after each assignment.

o = {}

o.a = 2 // => { a: 2 }

o //=> {}

o.b = 3 // => { b: 3 }

If you want to perform several alterations before getting a new structure you can do so in an transaction block.

o = {}
o2 = transaction {
  o.a = 2
  o.b = 3
  o.c = 4
  o //=> { a:2 b:3 c:4 }
}
o //=> {}

o2 //=> { a:2 b:3 c:4 }
xs = [1 2 3]

ys = transaction {
  js.apply ( xs.js.push ) ([4 5 6])
}

ys //=> [1 2 3 4 5 6]

xs //=> [1 2 3]

Transaction blocks automatically detect referenced structures that are being mutated. Any assignments are applied to a new object which uses the original object as a prototype. You can "update" multiple objects in one transaction.

a = { name: 'a' }
b = { name: 'b' }
c = { name: 'c' }

{ a: A, b: B, c: C } = transaction {
  a.value = 'a'
  b.value = 'b'
  c.value = 'c'
  
  { a, b, c }
}

A //=> { name: 'a', value: 'a' }
a //=> { name: 'a' }

A isPrototypeOf a //=> true

B //=> { name: 'b', value: 'b' }
b //=> { name: 'b' }

B isPrototypeOf b //=> true

Do Notation

In Javascript we have keywords like await and yield that allow us to convert atypical control flow into procedural code. Javascript is adding additional similar keywords for different types, e.g. Async Iterators require a combination of await yield.

It turns out you can just have one keyword, one interface for all types that can be sequenced. In this language that keyword is the symbol <=.

When you see <= Future Auth.createToken(user) think await Auth.createToken(user) we specify the type Future so the language knows what kind of await we are dealing with. This allows us to support multiple types of sequencing without having a type system.

login = name => password =>
  
  user = <= Future db.query (SQL.getUser) (name)
  
  valid = <= Future bcrypt.equals (user.password) (password) )
  
  token = 
    if ( valid ) {
      Either.Right ( <= Future Auth.createToken (user)  ) 
    } else {
      Either.Left ( 'Supplied credentials were invalid' )
    }
    
  <= IO.log ( 'User is valid: ' + valid )
    
  token
  • <= is like yield or await.
  • The keyword after after the <= token tells the language what type of side effect we are traversing.
  • We get one syntax for any object that implements the method: chain
  • Think Observables, Promises, Logging, Random numbers, Error handling. All one syntax.
  • When chaining 2 different types, a method <type1>To<type2> will be called. If there is no such method, a type error will be thrown

In this example Future is just an object with a chain method.

Future = {
  chain: f => o => ...
}

Notice <= IO.log did not need to specify the type as IO, that's because, if you are calling a method and you do not mention a type, the language will assume the chain method you want, lives on the object that contains the method. In other words <= IO.log is sugar for <= IO IO.log. We do not get this when using bcrypt, because bcrypt.equals does not live on the Future object.

Composition

You can compose 2 functions manually like so:

h = f => g => x => f (g (x))

But this is so common the operator + is used for composition:

h = f + g

Why plus? Because composition is monoidal. Which means, it has similar properties to addition.

But how does this work? Well it turns out + is just a method on the Function prototype.

Function::['+'] = f => g => x =>
  f ( g (x) )

A few other types support + including Lists, Strings, Numbers and more.

This form of composition is called right to left compose. Which means functions on the right hand side will be executed before functions on the left hand side. The language doesn't have built in left side compose, but we can implement our own.

Function::['>>'] = f => g => x => g( f (x) )
Function::['<<'] = Function::['+']

Almost all binary operators in this language are just methods on the prototype.

Do not fear, prototype modifications are module scoped.

Prototypes

You can modify prototypes manually using :: syntax

Function::x = f => ...

Functions can be executed infix style, but if a method name satisfies the infix instruction and there's a local function in scope with the same name, the method will be invoked.

f => x => y => console.log(['function', x, y])

2 f 3 //=> logs: ['function', 3, 2]
o = {
  f: => x => y => console.log(['method', x, y])
}

f = x => y => console.log(['function', x, y])

// Refers to o.f
o f 3 //=> logs: ['method', 3, { f: ... }]

// 2['f'] does not exist so uses f function in scope
2 f 3 // => logs: '[function', 3, 2]

Assigning to the prototype does not actually mutate anything, it creates a local prototype that all objects, functions, etc will use within that module. Prototype modifications must be top level and cannot be dynamically generated.

Custom operators

You can create some powerful operators with this feature, e.g. here is a definition for F#'s pipeline operator.

Object::['|>'] = f => x =>
  f( x )
  
  
2
|> add (1) //=> 3
|> pow (2) //=> 9
|> subtract(1) //=> 8

//=> 8

Prototype extensions must occur directly after import/export declarations. They must be at the top level scope.

We may want to define greater than > on lists.

Array::['>'] = xs => this =>
  sum( this ) > sum(xs)
  
[1,2,3] > [1,2] //=> true

Classes

Because of the semantics of the language, we get similar behaviour to a Javascript class with a simple struct.

Vector = { 
  x: 0,
  y: 0
  '+': v1 => v2 => transaction {
      v1.x += v2.x
      v1.y += v2.y
      v1
  } 
}

// Probably could do with some sugar...
v1 = transaction{ Vector.x = 4; Vector.y = 4; Vector }
// equivalent to above
v1 = { ...Vector, x:4, y:8 }

v2 = { ...Vector, x:2, y:4 }

v3 = v1 + v2 //=> Vector { x: 6, y: 12 }

v3 isPrototypeOf Vector //=> true

v3['+'] == Vector['+'] //=> true

Objects

Same syntax as JS

Lists

Same syntax as JS

Conditionals

Same syntax as JS but everything is an expression.

Loops

This language reluctantly includes most ES3 JS features but reserves the right to re-imagine their semantics. for, while, do while are all included but obey all other rules in the language.

for( i=0; i<5; i++ ){
  console.log (i)
}

Would be an infinite loop, because i cannot be mutated. We'd just log 0 forever. Worse still, for is an expression, so the language will attempt to create an infinite list filled with undefined (uh oh!).

So the above doesn't make sense. But below does:

for( x of range(0,5) ){
  console.log(x)
}

Keywords like continue, break are supported.

The original for example would work however in a transaction block:

transaction {
  for( i=0; i<5; i++ ){
    console.log (i)
  }
}

The above will log 1, 2, 3, 4, 5 and then exit.

We could also return a list:

xs = transaction {
  for( i=0; i<5; i++ ){
    i * 2
  }
}

xs //=> [2,4,6,8,10]

Expressions

This language has very few statements, almost any line can be stored in a named binding.

Lenses

Lenses are not part of the native language. But because the language is immutable by default creating lens like behavior is simple.

o = { w: 4 }

o : 'x' : 'y' : 'z' = 3
// => { w:4, x: { y: { z: 3 } }}

o
// => { w:4 }

The above looks similar to imperative code but is in fact function composition.

Object::[':'] = k => this => 
  if( !( k in this ) ){
    this[k] = {}
  }

So o : 'x' : 'y' : 'z' actually is just sugar for:

dot = o::[':']

x = o.dot(o, 'x') //=> { w:4, x: {} }
y = x.dot(x, 'y') //=> { w:4, x: { y: {} }}
z = y.dot(y, 'z') //=> { w:4, x: { y: { z: {} } }}

Modules

When exporting and importing modules in language there's some restrictions.

Import all exported properties and functions from a module and alias.

from 'ramda' as R

Import a few function from a module.

from 'ramda' { map, chain }

Export declarations must be at the top of the file. All references are hoisted so you can export before they are defined.

export { add }

add = x => y => x + y

There is no default export, you cannot export in a function definition. If you have multiple exports they must all be in the same statement:

export 
  { add
  , subtract 
  }

add = x => y => x + y

subtract = x => y => a - y

import statements must come before export statements. No code can appear before an import statement. No code except an import statement can appear before an export statement.

Async Imports

To load code asynchronously use do notation in a function context.

moduleName => 
  someModule Future <= from './moduleName' *
  
  someModule.someMethod ('hello')

JS imports

All the above rules apply. Invoking methods requires use of js.apply. default exports are not supported, you must specifically reference the export by name.

Interop with JS

JS objects, lists, functions are supported. But there's some minor pain points.

js.apply (list.js.map) ([ x => x + 1 ])
// Equivalent to list.apply( list.map, x => x + 1)

And everything works fine.

If you wanted to invoke Array::slice which is not unary we have to jump through the same hoops.

js.apply (list.js.slice) ([1,2])
// Equivalent to list.slice(1,2)

The reason for the indirection is, this language only supports unary functions, so to pass multiple args to a Javascript method we simply pass them as a list. Behind the scenes we just call list.apply( list.slice, [1, 2]).

If you want to call a JS function (not a method). Do the following.

js.apply (js.Math.pow) ([2,3])

All Javascript globals are namespaced under js., and all native Javascript methods are name spaced in the same way.

So list.map uses this language map, but list.js.map is a direct reference to the native Javascript map.

If one wanted to take native JS functionality and expose it idiomatically in this language you can just do the following.

pow = x => y => js.apply (js.Math.pow) ([x,y])

If you are ok with passing a list into sqrt you could simply do the following:

pow = js.apply (js.Math.pow)

pow ([2,3]) //=> 8

Algebraic Type Support

  • Any JS object that exposes a fantasyland/map or fantasyland/chain method will interop for free with <= <- map chain.
  • Any JS object that exposes a fantasyland/concat will interop for free with +

Additional support is trivial via local prototype extensions.

Additional work

  • Semicolons, commas?
  • Name the dang thing

Resources

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