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.
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.
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 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)
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
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 likeyield
orawait
.- 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.
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.
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.
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
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
Same syntax as JS
Same syntax as JS
Same syntax as JS but everything is an expression.
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]
This language has very few statements, almost any line can be stored in a named binding.
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: {} } }}
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.
To load code asynchronously use do notation in a function context.
moduleName =>
someModule Future <= from './moduleName' *
someModule.someMethod ('hello')
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.
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
- Any JS object that exposes a
fantasyland/map
orfantasyland/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.
- Semicolons, commas?
- Name the dang thing