_
# Dot-dot symbol `..` means virtual method. It lets you use underscore or lodash
# much easier.
# So you can forget about `_()` and `.value()`.
console.log [2, 3, 4]..max()
console.log [5, 3, [2, 4], 1]..flatten()..sortBy()
# Diamond function `<>` is parameterless. But ignore it at the moment. Run the code
# and view the 2nd example.
# The 2nd example will tell you what `:` and `=` mean.
]]>
". Windows users can customize
# shortcut keys in editor, just like what we customized in this editor.
# Some editors support inserting a snippet with Tab, so you can set "="+"Tab".
]]>
q: encodeURIComponent("
select * from yahoo.finance.quotes where symbol in ("AAPL")
")
env: encodeURIComponent("store://datatables.org/alltableswithkeys")
query: "?q=\(q)&env=\(env)&format=json"
console.log "Querying stock price..."
response: web.jsonGet("https://query.yahooapis.com/v1/public/yql\(query)")'wait
quote: response.body.query.results.quote
console.log "Apple Inc. Bid: \(quote.Bid), Ask: \(quote.Ask)"
# Forget about the "Triangle of Doom".
# Just add a `'wait` to a promise. It will wait until a response is received.
# The function that contains `'wait` automatically becomes an async function.
# There's no callback, just like all synchronous things.
# `do` means to call the function itself.
]]>
is also parameterless. In <> you can use `@`.
# @ is easy to remember. From now on you can pronounce @ as "arg".
#
add: <> @0 + @1 # var add = function() {return arguments[0] + arguments[1];};
console.log add(2, 3)
# `undefined` is also too long? No problem. Fus provides `void`.
console.log void
# In Fus, there's no keyword longer than 7 letters.
]]>
x: point.x
y: point.y
Math.sqrt(x * x + y * y)
console.log distance{x: 3, y: 4}
# Function doesn't need `return`. The last statement is the return value.
# Variables don't need to be declared with `var`. They will be declared
# automatically when first assigned. The scope is the function level.
# It's concise and can avoid global pollution. In JS if you forget writing
# `var` then it will become a global variable that results in global pollution.
# You can strip the # in the next line and see what error it shows.
#console.log x
]]>
me.name: name
me._spoken: 0
speak: <>
me._spoken: self + 1
console.log "\(me.name) meows for the \(me._spoken) time."
# Instantiating a class doesn't require `new`. Fus will check it. If a callee
# (here `Cat`) is capitalized, it will automatically add `new` in the compiled JS.
cat: Cat("Dabai")
cat.speak()
cat.speak()
console.log cat.name
# If a class property name starts with `_`, it will be "private". So
# `cat._spoken` is undefined.
console.log cat._spoken
# In essence, Fus class is just JS class. So you may well import Fus classes
# in your JS file.
]]>
console.log(i * i)
}
# Flexible object (type-2) can apply to `loop` function. It makes you close
# to be able to customize grammars to write a beautiful loop, while in essence
# it's still "an object is passed to a function".
# Don't believe? Replace `loop` with `console.log` and see the output.
# (Because the output is the converted JSON, you can't see the function.)
# Using these 2 types of flexible objects together with "batch import",
# everyone is close to have the power to extend this language.
]]>
Below are tutorial (shown in normal font), and detailed specification that may be uninteresting to developers (shown in gray, smaller font). On first time reading, you can just read the tutorial (tutorial is also part of the specification, though easier to understand).
FutureScript | JS (ES5) | JS (ES6) | CoffeeScript | |
---|---|---|---|---|
Async function | ✓ | |||
Simplified function notation | ✓ | ✓ | ✓ | |
Functional if |
✓ | ✓ | ||
Simplified if |
✓ | ✓ | ✓ | |
Pattern matching | ✓ | ✓ partial | ||
Complex self-assignment | ✓ | |||
Anonymous recursion | ✓ | |||
Instantiating a class doesn't require new |
✓ | |||
Class private properties | ✓ | |||
Class getter, setter | ✓ | ✓ | ||
Class static constructor | ✓ | |||
Current class | ✓ | |||
Pipe | ✓ | |||
Virtual method | ✓ | |||
rem modulo |
✓ | ✓ | ✓ | ✓ |
mod modulo |
✓ | ✓ | ||
Combine instanceof and typeof |
✓ | |||
Import | ✓ | ✓ | ||
Batch import | ✓ | |||
Export | ✓ | ✓ | ||
String interpolation | ✓ | ✓ | ✓ | |
Functional try...catch |
✓ | ✓ | ||
Flexible object | ✓ | |||
Natural enum | ✓ via fus-ext | |||
Natural functional loop | ✓ via fus-ext | |||
Self-invoke | ✓ | ✓ | ||
Existential operators | ✓ | ✓ | ||
Membership operator | ✓ | ✓ | ||
Exponentiation operator | ✓ | ✓ |
These years, JS grammar is getting more and more complex, also more and more cumbersome.
JS | FutureScript |
---|---|
Three symbols to mean strings | Unified to double quotes |
= assignment |
Unified to colon |
[] to access elements |
Unified to dot |
Native loop | Implemented using flexible object |
return |
|
someVar++ |
someVar: self + 1 |
someVar-- |
someVar: self - 1 |
someVar += 2 |
someVar: self + 2 |
someVar -= 2 |
someVar: self - 2 |
someVar *= 2 |
someVar: self * 2 |
someVar /= 2 |
someVar: self / 2 |
<< |
|
>> |
|
>>> |
FutureScript is the first language to have all of these 4 properties:
The author calls this set of properties "the future style".
Some of Fus's features, such as flexible object, have never be implemented by any language before.
Fus's target: We want to make it so that any terser language isn't more readable than Fus, and any more readable language isn't terser than Fus. We are finding the balance point of terseness and readability, while keeping its thrilling beauty and consistency.
(This chapter is not part of the language specification. Because tools will be updated in the future, this chapter may be out-of-date then.)
First confirm you've installed Node.js 18 or higher on your computer.
For detailed usage of fus
command, please click:
https://www.npmjs.com/package/futurescript
We provide syntax highlighting modules for text editors such as Sublime Text and Atom. I think it should also work on WebStorm and other editors that support tmbundle.
Atom users can use language-futurescript package. You can continue using your current theme, or use these 2 themes optimized for FutureScript: 4-color and 4-color-dark.
For other editors, for example Sublime Text, in its menu choose Preferences, Browse Packages. The popped-up window is the directory containing all language packages. Click here to download the latest version, then rename the "tmbundle" directory to "FutureScript". Then copy it to the "Packages" directory.
The syntax highlighting name we use is "FutureScript h1".
The compiler-generated JS conforms to the ECMAScript 2017 specification.
The compiler by itself conforms to the ECMAScript 2017 specification and doesn't rely on any other program. Note that the compiler is just part of the command tool, and the command tool does rely on Node.js.
File extension is .fus
.
Fus file appearance: comprises version line, statements, comments, and meaningless whitespaces. Statements can be nested. Version line and statements are meaningful. Comments and meaningless whitespaces are meaningless.
A statement is not an expression nor treated as expression. An expression is not a statement, but can be treated as statement (named as "expression statement"). There are 2 kinds of statements: expression statements and command statements.
Similar to CoffeeScript, in FutureScript, indents are important, for they form the nested structure. Indent can be substituted by <<
and >>
.
Similar to CoffeeScript, juxtaposed lines can be combined using semicolon;
, and a single line can be splitted by using \
.
Empty line, or blank line, means a line that doesn't have any character.
Whitespace line, means an empty line or a line that only have whitespaces.
Meaningless whitespace, means an empty line, or the whitespace that's meaningless in the right side of a line.
In all text hereinafter, we assume that meaningless whitespaces are removed.
Most statements are expression statements, except that these are command statements:
import
expression)export
at the start of a statement, so excludingexport as
, but includingexport...as
)Given a statement, the maximum consecutive content of the level of the statement is called the block of the statement. Block doesn't include version line. Besides block, there is list. Either block or list is called a "container". Block's child can be statement or comment. List's child can't be statement. A child block (or child list) of a statement means the first-level descendant of the statement. A child statement of a block also means first-level descendant.
These containers are blocks:
then
blockelse
blocktry
blockcatch
blockfinally
blockAll other containers are lists. For example:
class
listmatch
listIf a block (except catch
block) has only a single statement, it can be written in the same line without indent. This is called inline block. For example:
Note that aaa()
still belongs to then
block. Also note, that in this notation, there must be a corresponding keyword or symbol ahead of the block. In the above example the then
can't be omitted. From this we know that every block (except that in post-if) has a leading keyword or symbol in grammar. For example, ->
, then
, andtry
. It can be omitted only when it isn't at the start of a statement and isn't at the start of a line and isn't followed by an inline block (also depends on whether the grammar supports it. For example ->
can't be omitted).
Block's child statements are run from top to bottom. Except for the root block, every block has a value. If the last statement is an expression, then the value is the expression value, otherwise the value is void
.
For a given line B, if the starting part doesn't belong to a string, what's the effect of its indent? Follow the following steps:
1. Force connect: Does the previous line A end with \
? If not, do next step. If yes, then: No matter how much indent, the line is parsed as if connected to the end of last line, then skip to step 5. If failing to parse then skip to step 6. (Terminology: "connect" in these steps means concatenation of two lines with a space inserted between them. The indent of the second line doesn't count.)
2. Juxtaposition: In above code does there exist a line A satisfying that B's indent is equal to A's indent, and there's no line between A and B or the indent of any line between A and B is greater than B's indent, and A is not connected or force connected (Terminology: the "connected" one means the right-hand side after connect, while the "connector" means the left-hand side)? If not, do the next step. If yes, then: A's starting part and B's starting part is parsed as the "juxtaposition" relation, then skip to step 5. If failing to parse, skip to step 4.
3. Hierarchical: In above code does there exist a line A satisfying that B's indent is greater than A's indent, and there's no line between A and B or the indent of any line between A and B is greater than B's indent, and A is not connected or force connected? If not, do the next step. If yes, then: A's ending part and B's starting part is parsed as the "hierarchical" relation, then skip to step 5. If failing to parse, skip to step 4.
4. Connect: In above code does there exist a line A satisfying that B's indent is greater than or equal to A's indent, and there's no line between A and B or the indent of any line between A and B is greater than B's indent? If not, then skip to step 6. If yes, then: B is parsed as if it's connected to the end of the statement of A's end. If failing to parse, skip to step 6. For example:
Or:
Or:
Both equivalent to:
Note that for now this is not supported:
aaa()
.c()
]]>
You need to add a parenthesis pair:
aaa()
).c()
]]>
5. Succeed and exit.
6. Fail and exit.
Indents are very suitable to represent some complex statement. For example, you can write:
s: @0.toLowerCase()
text.includes(s)
)
"cat" ? "meow"
"dog" ? "woof"
| "unknown"
# speak is "woof"
]]>
One thing different from CoffeeScript is the following code:
CoffeeScript compiles it into a.b(aaa).c()
, but we strictly follow the rule above to compile it as a.b(aaa.c())
. If you want the CoffeeScript effect, you should add parentheses:
Usually, Splitting one line into several lines will add readability. But sometimes things are just the opposite - you will find combining several short lines into one makes it beautiful, particularly when you have many such "several lines". For example:
>
B: class << bbb: 1 >>
C: class << ccc: 1 >>
D: class << ddd: 1 >>
E: class << eee: 1 >>
]]>
It's equivalent to:
This leverages the power of <<
and >>
. <<
means to indent one level, and >>
means to unindent one level.
<<
and >>
must be closed in one line. That is, multiple lines can be combined into 1 line, but not into 2 lines.
These 2 symbols, used together with semicolons and commas, can achieve the maximum combination effect. Semicolons combine lines, but object properties, array elements and class properties need to be combined through commas.
After >>
there can be a semicolon or comma, but it's optional. For example:
<< x + 1 >>, bbb: 3 >>
if aaa << task1() >> else << task2() >>; commonTask()
]]>
Here the semicolon and comma can be omitted.
The first line of each file must be version line. It's usually in one line so we call it "version line", but it can be in multiple lines. Version line's syntax:
Default value is:
The items separated by comma after the version number can't be duplicated. Examples of version line:
Version number must be written. As long as the number is not greater than the FutureScript version you've installed, the code will be run according to the specification of the file's version. This is because any version you install contains all historical compilers (at least as much as possible). This is exactly the difference between FutureScript and other languages. You can even use different versions for different files in one project.
Another benefit of writing version number is that when you hand your file to somebody else, he will know the version, so that he can better modify it.
This article only describes one version.
When you want to write the version line in multiple lines, a brace must be added. The left (opening) brace must be in the first line. A comma between the version number and the left brace is optional. For example:
If the file extension isn't .fus
or it has no extension, then there must be a fus
before the version number. If the file extension is .fus
, it's not necessary, but highly recommended, because your text editor can recognize it so that the syntax can be highlighted.
The difference between "radical mode" and "compatible mode" is: radical mode encourages single-argument functions. Did you find the multiple-argument paradigm makes things complicated? If every function can take at most one argument, this argument can be an array, which can also achieve the similar functionality. But this change is too big, so we don't enable it by default. The default mode is compatible mode, in which you can still use all features of multiple-argument functions.
In radical mode, @
means JS's arguments[0]
. In compatible mode, @
means JS's arguments
.
All examples in this article are for compatible mode. In radical mode some code needs modification.
FutureScript code can be used by JS. JS code can also be used by FutureScript.
capitalized new
means you don't need to add new
when creating an instance of a class. The compiler will automatically add the new
to the generated code. It depends on the name of the function or class. If it's capitalized then it will add new
. For example:
This is by default.
Two forms can be recognized: variable and the normal dot (x.y
, it will check y
). Things like x."y"
或x.y'
can't be recognized. For example:
manual new
is just the traditional way. When creating instance of a class you should add new
.
node import
means all imports are compiled into Node.js imports (i.e. require
). node export
means all exports are compiled into Node.js exports (i.e. exports
and module.exports
). node modules
means to do both. es modules
means to do neither (i.e. comply with ES6). For details, see the "module" section.
The generated JS code is in strict mode.
Use #
(inline) and ###
(formatted), similar to CoffeeScript. But one thing different is that CoffeeScript's ###
is compiled for all occurrences, while our ###
is compiled only when the opening ###
is immediately below the version line (or there are only whitespaces between them) and the right side of the closing ###
doesn't have any non-whitespace character, otherwise it will be ignored by the compiler. To comply with JS, the */
in the comment will be compiled into two spaces.
There are 3 ways to create a function: arrow ->
, diamond <>
, dash --
. <>
and --
are shorthand for no parameter. Unlike CoffeeScript, when there's only 1 parameter the parenthesis pair can be omitted.
x + 1
a: (x) -> x + 1
]]>
If there's no parameter, then your must use either of these forms:
Math.random()
a: -- Math.random()
a: () -> Math.random()
]]>
Multiple-parameter is allowed, but must be within a parenthesis pair:
x * y
]]>
If it's single-parameter, and this parameter is an array, then we can use a form similar to "destructuring assignment":
x * y
]]>
With this, radical mode have become truly useful. You can use a[1, 2]
to call it (we'll discuss the details later). In fact, if a purpose can be achieved by multiple-parameter, it can also be achieved by the single array parameter, and even more pure. So, in radical mode we discourage the use of multiple parameters (Multiple-parameter is partially supported in radical mode. You can't use @
to denote the arguments after the first argument).
Inside <>
, we can use @
. @
supersedes JS's arguments
(compatible mode) or arguments[0]
(radical mode). It's so handy that in many cases you'd like to take no parameter. For example, we can write:
@0 + @1
console.log add(2, 3) # output 5
]]>
Parameter can have default value:
x * y
b: [x ifvoid: 0, y ifvoid: 0] -> x * y
]]>
If it has a default value, then there must be a parenthesis pair (or bracket pair) outside even if there's only 1 parameter.
If it's ifvoid
default, then ifvoid
can be omitted. So the above example can be simplified to:
x * y
b: [x: 0, y: 0] -> x * y
]]>
Besides ifvoid
default, there's also ifnull
default. We'll discuss that later.
The following table lists the detailed differences between the 3 function symbols:
Arrow function -> |
Diamond function <> |
Dash function -- |
|
---|---|---|---|
Parameter | Yes | No | No |
@ |
No | Yes | No |
fun |
Yes | Yes | No |
Features | Many | All. Can simulate the other 2 | Fewer |
Conciseness | Concise | Usually very concise, but sometimes not | Very concise |
For example, for the use of @
, the following code:
inner: x -> @
]]>
Here the @
doesn't mean inner
's argument, but mean outer
's argument, because the grammar of ->
doesn't define @
.
fun
is a keyword, meaning "this function". It's usually written in a recursive function. If the function name is very long, it can reduce your typed characters. It even enables you to use a recursive function without naming it.
Similarly, because the grammar of --
doesn't define fun
, the fun
inside --
means the outer ->
or <>
function.
Inner parameter can't have the same name as any of outer parameters. This is stricter than CoffeeScript and JS. It might be less strict in later versions.
We have known that member access all uses .
. Another benefit of it is that in function call you can omit parentheses and use brackets directly.
Function call has 4 notations: space, parenthesis ()
, bracket []
, and brace {}
.
In multiple-argument function call a parenthesis pair must be added. This is different from CoffeeScript. Space call is only applicable in single-argument function call. And of course we all know that if it takes no argument then ()
must be added.
In CoffeeScript, if the passed argument is itself a function which takes no parameters, then it could be written as:
Math.random()
]]>
Because CoffeeScript understands it as:
Math.random())
]]>
But in FutureScript you couldn't write so, because the compiler will treat it as a function with parameter abc
. You must write either of these:
Math.random()
abc -- Math.random()
abc () -> Math.random()
]]>
We can also use '
to achieve the "splat" effect:
Equivalent to ES6's
The apostrophe '
is called variant. '
is a unique symbol in FutureScript. Not limited to this form. We will discuss it later.
You can use the spread symbol ...
. For example:
y.length
a(0, 0, 0) # returns 2
]]>
Spread symbol ...
can also be in function call. For example:
For now, the limitation for the spread symbol ...
is: It must be the last argument / parameter, and can't be in an array. Future version may free this limitation.
Note that these 2 functions are different:
Every function has a return value - the value of the function block, so we don't need to use return
. What does the value of the function block mean? The accurate definition is described before. Now I give an informal definition: It's just the last statement's value in that function. If the last statement isn't an expression, then it's void
. This rule is very important - it's the core of the functional programming. For example:
a: x * x
b: y * y
a + b
calc(2, 3) # returns 13
]]>
This rule applies to not only the function block, but blocks inside if...then...else...
, and all other blocks (except the root block).
Unlike CoffeeScript, we don't have fat arrow =>
, because I think JavaScript's changeable meaning of this
isn't a good design. It should always point to the current object.
The mapping between our function and CoffeeScript's function is as follows:
Use :
and as
. For example:
Note that :
doesn't form an expression, but as
forms an expression. For example:
a: 1
]]>
Run abc()
and the return value is void
not 1
. But
1 as a
]]>
Run abc()
and the return value is 1
.
Variables don't need, and shouldn't be declared, because our variables are automatically declared. This mechanism is like CoffeeScript, but with notable difference mainly in dealing with the inner and outer level:
These two codes are equivalent. Both are compiled to a single variable:
CoffeeScript:
a = 5
]]>
FutureScript:
But these two are different:
CoffeeScript:
a = 5
a = 3
]]>
CoffeeScript compiles it to two variables with the same name a
. But in FutureScript:
will be compiled to a single variable a
.
Auto-declaration is inspired by CoffeeScript. I think it's one of the best features in CoffeeScript. Maybe many people don't agree, feeling it's a bad feature because it inhibits declaring a same-name variable in inner level. But this is not the truth. In a large project, it's reasonable for different developers to manage different files, for modules are isolated; but it isn't reasonable for different developers to manage the inner and outer level of a file respectively, for it will cause chaos. Now that a file's inner and outer level should be managed as a whole, when you name an inner-level variable, it's not difficult to pay attention to the outer level, so there's really no need to support the same-name variable in inner level. We even do it more thoroughly than CoffeeScript.
:
supports array destructuring assignment (without nested). For example:
The effect or a colon is just to let the left side have the value of the right side, be it in object or in assignment.
:
supports assigning a value to multiple identifiers. For example:
If a name isn't a FutureScript keyword, it can be a variable name. Even JS keywords such as function
can be variable names. But JS keywords can't be global variable names.
If you want to continue using Node's require
, exports
and module.exports
, you can simply skip this section. In FutureScript you can still use them as usual.
We know that Node has its own module system, but the problem is that this system doesn't belong to ES6. ES6 defines a native JS module syntax. Because FutureScript is based on JS, our native module syntax is based on ES6.
One file is to one module. Syntaxes related to module are import
, export
and 'export
.
or
Both mean to import ES6's default export, equivalent to JS's:
If the version line uses the default value, it will be compiled to the above JS. But the wonderful thing is: if the version line has node import
or node modules
, the same code will be compiled into:
Without modifying your code, you can control whether to use Node module system or ES6 module system for all import
in this file. So now import
can replace require
.
In fact, because currently Node.js and browsers only provide partial ES6 support, you may have to use Babel to convert it to ES5, and Babel will automatically convert your import
to use the Node module system, so even if you don't specify it in the version line, I think it doesn't matter (but there may be slight differences).
To import all named exports:
Or
Equivalent to JS's:
To import specified named exports:
Or
Equivalent to JS's:
Importing both default export and named exports is not supported. If you have this requirement please split them into two statements. I think it's not good for a module to have both default export and named exports.
Note, that some examples above use the feature that all
can be omitted. The complete form is:
import
can only be followed by an inline normal string. The string can't have interpolations (but can have escapes). import
can't have both colon and as
. These are correct import
statements:
These are illegal import
statements:
An import
assignment is not a normal assignment. It is "binding", like in ES6. So it's limited to variables. It can't be assigned to object properties. There can't be ifvoid
, ifnull
or '
before the colon.
When we use Node we often come into this situation: Each file has a large number of imports, most of which are duplicated, but can't be omitted. Of course you can create a new module and put everything into it, then export, so each module only needs to import this new module. For example:
You create a all.fus
file and put the 3 import
into it and write the following in the original file:
But the problem is: When calling, all.
must be added. So it seems not much shorter than before.
FutureScript has a unique "batch import" feature (even ES6 doesn't have). If there's all
but no colon or as
, then it will be compiled to batch import. It will import all named exports (except the default export) and let them all be variable names. So, we can write:
This feature has an important property: When compiling, it needs to generate variable names by reading the imported module. To ensure the path's parsing mechanism is the same as runtime, we made the parser as simple as possible. So it only supports paths starting with "./"
, "../"
or "/"
. So this is a limitation: it's only suitable for importing another module in the current project, not suitable for importing directly from another project by using the module name. So if you want to import between projects, you need to have a "manifest" module as the "middle" in the current project.
"Batch import" doesn't introduce global variables, so it's not evil. These variables are invisible in other modules, unless other modules batch-import them too.
If A batch imports B, and if some B's named export (imported as A's variable) happen to be a keyword of A's FutureScript version, then this variable will be inaccessible, but it won't cause error.
A normally imported module can be a Fus or JS file, but a batch imported module must be a Fus file (any version number is allowed).
In the above we introduced import
statement. import
can also act as expression. The syntax is: import "module-name"
. It can't be followed by as
. For example:
So the syntax of import
expression gives you more freedom, but it can only import default exports. Also, if used together with assignment, then it's a normal assignment, not a binding.
When export, we can use 4 syntaxes: export
, 'export
, export as
, export ... as
. These are all correct:
@0 * @1
mp: (<> @0 * @1) export as multiply
mp: (<> @0 * @1) as multiply'export
abc export as def
export abc as def
export abc
export: abc # note that this is different from above
export: <> @0 * @1
]]>
These are illegal:
@0 * @1
obj.multiply'export: <> @0 * @1
]]>
Note that abc export as def
and export abc as def
behave similarly, but with two differences. One is obvious: the former is an expression statement while the latter is a command statement. The other is subtle: The former abc
is treated as an expression, while the latter abc
is treated as a variable. This makes it so that: The former is not a binding, but the latter is a binding (can have the benefit of ES6 module's variable binding).
If a statement starts with export:
, it means ES6's default export. For example:
If the version line has node export
or node modules
, this statement will be compiled into:
This kind of statement can appear only once. If it appears, then there can't be any other exports, otherwise a compiler error will be raised. This is because (as mentioned before) it's not good to have both default export and named exports in one module. Although for compatibility FutureScript supports importing a JS module of this architecture, for Fus module we can dismiss this architecture.
Default export can be in this form:
So default export is actually a special named export (for details see ES6 articles). If you name it default
then it's default export.
So these two:
They behave similarly. The only difference is: The former abc
is treated as a variable, so it's a binding. The latter abc
is treated as an expression, so it's not a binding. (This is the same as ES6's rule. ES6's export {... as default}
and export default ...
also has such difference.)
Imports and exports must be in the outermost level, because it's based on ES6. Our imports are static, not dynamic, so it's stricter than Node. But I know in some cases you may need to dynamic import, you can use require
instead of the native module syntax.
Inline string:
Formatted string:
Welcome to "FutureScript"!
"
]]>
The above string is equal to CoffeeScript's
Welcome to "FutureScript"!
"""
]]>
You may wonder why we can use only 1 quotation mark to wrap a text block (the text itself has quotation marks). The secret is that we require every indent in the string to be greater than the indent of the quotation mark, and require that the indent of the opening quotation mark be the same as that of the closing quotation mark. So no matter how many quotation marks there are inside the string, they won't affect the compiler.
The opening quotation mark of a formatted string must be followed immediately by a newline (the 2 newlines for starting and ending doesn't count towards the string characters). In the line of the closing quotation mark, before the quotation mark there should only be whitespaces. Within a line inside the string, the left part of the indent doesn't count towards the string characters until an indent whitespace exceeds the "minimum indent in the string". Note: Here "minimum indent" doesn't include empty lines.
The inside of a formatted string can't be all whitespaces (except those in escape form). For example, these are all illegal:
Although this also represents a space, it's valid:
How does the compiler determine if it's inline or formatted? It checks the character immediately after the opening quotation mark. If it's newline, then formatted. If otherwise then inline.
We don't use apostrophe to represent strings, because I feel when you want to use it, if the string is very long, you can use formatted "
; If the string is very short, such as '"'
, this just add very little readability. I'd rather use"\""
. Neither of these has an obvious advantage over the double quotation mark. So, better not spend the precious symbol resource on this. Also, double quotation mark is consistent with JSON.
Like CoffeeScript, we support string interpolation, but we use \(...)
.
An interpolation can't occupy multiple lines. For now we don't support interpolation with quotation marks in it (i.e. nested strings). In fact, even if we support it, it will look very complicated and thus not beautiful. You may well assign it to a variable and interpolate the variable.
In the above we introduced the normal string. Besides normal string there are special string and string extension. What's a string extension? Regular expression is a typical example.
To represent a regular expression, we use r"..."
. For example:
The inline regular expression suppresses most features of \
except these 2: \"
and the line-end connectable \
. Note, that in JS "
and #
can be written directly in a regular expression, but here we must use \"
and \#
, for these 2 characters have special meanings.
Regular expression can have interpolations. It interpolates strings:
Equivalent to CoffeeScript, if the regular expression interpolation is a normal string, it will be verbose if you want to express the \
character, so it's recommended to use it only to insert non-symbol characters.
Representing flagged regular expression is also possible:
An example of formatted regular expression:
The difference of a formatted regular expression is: Whitespace and newline are meaningless. Support comments. Simpler to represent double quotes. There must be at least one space before the comment character #
. This is different from normal comments.
Verbatim string is inspired by C#. A verbatim string is represented by v"..."
. For example:
A verbatim string suppresses all the features of \
, so you can't connect using \
in line end. In inline verbatim strings you can't represent double quotation marks.
So in summary, if the opening "
is preceded immediately by a letter, then it means a string or string extension.
In essence, the content enclosed by quotation marks in a regular expression is a string. This is different from JS and CoffeeScript. Even the comment in a formatted regular expression belongs to a string in essence (but just got removed during conversion). Note, that the conversion is at runtime not compile time. Because it's possible to have interpolations, we can't convert it while compiling.
There's another difference from CoffeeScript. In CoffeeScript:
This will be compiled to "join multiple lines by a space". But we do not support. One reason is that it's not compatible to our grammar. Another reason is that this only applies to western languages. For example, if it's Chinese then adding a space is wrong. So I think it's better to use\
to join. If it's English, then manually add a space before it.
The following table lists the escape rule for all kinds (the escape during conversion is not included, for example the \
in regular expression itself):
Inline " | Formatted " | Inline v" | Formatted v" | Inline r" | Formatted r" | Inline js" | Formatted js" | |
---|---|---|---|---|---|---|---|---|
\ Normal Escape | Yes | Yes | No | No | No | No | No | No |
\\ | Yes | Yes | No | No | Yes | No | No | No |
\" | Yes | Yes but not required | No | Not required and no | Yes | Not required and no | No | Not required and no |
\ Connect | Yes | Yes | No | No | Yes | Yes | No | No |
Interpolation | \(...) | \(...) | No | No | #(...) | #(...) | No | No |
The bracket pair [...]
is to represent an array literal or an array-like thing (in destructuring assignment). No other uses.
The dot .
is to access an object property. Because an array is also an object, JS's arr[3]
is superseded by arr.3
, which is more consistent. In fact an array is an object whose property name is the index (converted to string). If you don't believe you can test by typing Object.keys(arr)
. JS is different than other languages in this part, but it's not a bad part, because the concept is consistent.
Of course, when used together with fractions it may look unsuitable. In this case you can add a parenthesis pair. Compare the following:
a.(b)
is equivalent to JS's a[b]
.
The notation of array literal is the same as CoffeeScript.
The notation of object literal is similar to CoffeeScript, but it must be enclosed with a brace pair {...}
, which can't be omitted.
In object property access, if the left side of a dot .
is a number, then the number must be enclosed by parentheses, for example (1).toString()
. This restriction is to avoid ambiguity, such as 1.3
. You may argue that there's no ambiguity because 1
is unlikely to be an array, then how about 1.3.a
? It feels weird, less intuitive than (1.3).a
. So in this point we are stricter than JS (JS doesn't allow 1.a
, but allow 1.3.a
).
FutureScript provides the unique "flexible object notation". It supports two types of flexibilities. In type-1, colons and true
can be omitted. For example:
Equivalent to JS's:
In type-2, colons and commas are omitted. For example:
console.log(i * i)
}
]]>
Equivalent to JS's:
Another example of flexible object notation is enum:
Because it's equivalent to JS's:
The above 2 examples use the loop
function (type-2) and enum
function (type-1) in fus-ext package, respectively. The flexible object notation is so expressive and powerful that you are close to have the ability to customize grammars. We will never need the grammar of native loop or enum. Simply call a function and it will be as concise or even conciser than a language with native grammars.
Note, that you can't apply both type-1 and type-2 to an object. Additionally, in type-2 each line must have at least two items (key or value counts as one item respectively). So {a b}
is of type-2 (equivalent to {a: b}
), while {a}
is of type-1 (equivalent to {a: true}
).
In type-2, if the number of items is odd, then the first item is parsed as a value rather than a key. Its key is an empty string. See loop
example.
In type-2, you can't use space to call a function. You should use parenthesis pair.
Regardless of the type, if a key isn't followed by a colon, then the key itself can't be written as an expression. For example this is illegal:
In type-2, a keyword acts as a keyword only if the item is a value. That is, the following code:
Will be compiled to:
You can write:
But if you want to modify the operator to or
, then you should add a parenthesis pair:
In type-2, key name can't be not
or as
.
Finally, please note that since the examples in this section contain external functions, these examples are likely to be outdated.
There are the following kinds of literal:
null
literalvoid
literalA literal is an expression. Its difference from a normal expression is:
We have 6 keywords or symbols to denote the contexts, as follows:
me
Me
super
fun
@
self
The dot immediately after @
can be omitted, except that the dot is followed by (
or string or string extension such as "
, v"
and r"
.
self
can represent complex self-assignment. For example, when you want to self-add something to the left side rather than the right side:
The equals sign =
is just to determine equality. No longer need to distinguish between =
, ==
and ===
.
/=
or ≠
means inequality. It can also be written as not =
, where the space can be omitted. The highlight here is ≠
. If you use Mac, you can directly input ≠
by holding "Alt" key and pressing "=". Mac natively supports inputting inequality signs. If you use Windows, you can define shortcut keys in your editor, so actually everyone can input ≠
easily. This greatly improves readability.
Why don't we use !=
? Because !
isn't used by FutureScript so far, I really don't want a precious symbol to be only usable with =
. /=
also looks more like ≠
than !=
.
Like CoffeeScript, we support chained comparison (for now only support 3 operands):
But our limitation is more than CoffeeScript. You can only compare in the same direction. Such is not supported:
c
console.log "success"
]]>
For logical operation, the biggest difference in FutureScript is the precedence of not
. not
is lowered, only higher than and
and or
. This means we can omit many parentheses. You must have complained in JS you have to write:
But now we can:
But in fact this example is illegal, because we don't have instanceof
. We use is
. You can write not abc is Abc
but it's a bit ugly (so we support abc isnt Abc
). So are there any beautiful examples? Yes, in this example you can also omit parentheses:
Back to the inequality sign. You can also use not ... =
. These 5 forms behaves the same:
But not ... =
generates different code than the other 3, though the behavior is the same.
Useif
, ?
, then
, else
, |
. For example:
Or
Can be combined into one line:
Or
Or even
?
is equivalent to then
, and |
is equivalent to else
. This rule is not limited to if
, but rather in all the language. Any rule mentioned hereinafter is applicable to its equivalent. For example, the rule of then
also applies to ?
.
When the condition is prefixed, if
can be omitted, and then
can be omitted if then
block isn't an inline block. When if
is omitted take note of the precedence problem it may cause. For example:
100 then 100 else b
]]>
Here if the if
is omitted the meaning will be changed, unless a parenthesis pair is added.
When then
is followed by a keyword of command statement, then
can also be omitted (this also applies to pattern matching, which we will discuss later). For example:
100 throw new Error()
]]>
If there's no else
block, then if the condition matches else
, the whole if
expression's value will be void
.
But note that however omitted, you can't omit both if
and then
in the prefixed-condition form.
Condition can also be postfixed. For example:
When condition is postfixed, if
can't be omitted. One statement can only have one postfixed if
. It must be in the outermost level of a statement (can't be enclosed with parentheses). Also, the block before if
can't be a normal block. It can only be an inline block. Because postfixed if
must be in the outermost level, so if a colon assignment is before it, then the code between the colon and if
should belong to the colon not if
.
Another thing different from CoffeeScript is when come across semicolons:
100 then aaa(); bbb()
]]>
CoffeeScript treats aaa(); bbb()
as child statements. But this rule looks inconsistent with other rules. Also we have unique symbols for this so it can be written as:
100 << aaa(); bbb() >>
]]>
So CoffeeScript's "semicolon rule" is cancelled in our language. In FutureScript, the left and right side of semicolon can pass through any obstacles, provided there's no grammar error.
match
can be regarded as an enhanced version of switch
, called pattern matching. For example:
Above is the simplest pattern matching, where day
is called the input, and 1, 2, 3... is called patterns.
The "comparison" is by default using =
to directly compare the input with a pattern. match
can also be followed by not the input, but a comparison function. For example:
statusCode >= @0
600 ? "unsupported"
500 ? "server error"
400 ? "client error"
300 ? "redirect"
200 ? "success"
100 ? "informational"
| "unsupported"
# message is "client error"
]]>
If using radical mode, the above @0
can be simplified to @
.
else
can be combined with a preceding pattern by using or else
, but this pattern can't contain then
. This is called the combination of else
pattern and other pattern, but in essence it's still two patterns. In addition, if or
isn't treated as an operator, it can be simplified as ,
. So the above example can be written as:
statusCode >= @0
600, | "unsupported"
500 ? "server error"
400 ? "client error"
300 ? "redirect"
200 ? "success"
100 ? "informational"
]]>
When using or else
for patterns, or
can be omitted. So it can be further simplified to:
statusCode >= @0
600 | "unsupported"
500 ? "server error"
400 ? "client error"
300 ? "redirect"
200 ? "success"
100 ? "informational"
]]>
Comparison function is very powerful. For example:
match <> pos.x = @0.0 and pos.y = @0.1
[0, 0] ? "king"
[0, 1] ? "pawn"
[4, 5] ? "queen"
| "empty"
piece: chessBoard {x: 4, y: 5} # will be "queen"
]]>
(Note: In radical mode @0.0
and @0.1
can be simplified to @0
and @1
.)
Another example:
text.includes(@0)
"cat" ? "meow"
"dog" ? "woof"
| "unknown"
# speak is "woof"
]]>
Once match
is followed by a function, it will be recognized as a comparison function. ->
or <>
must be directly written here. It can't be replaced by a variable.
There are also or
pattern and and
pattern. or
pattern is very commonly used. For example:
and
pattern is less common. But here is an example:
array.includes(@0)
"a" and "b" and "c" ? "The array includes all of the first 3 letters."
"a" ? "The array includes a."
"b" ? "The array includes b."
"c" ? "The array includes c."
| "The array doesn't include any of the first 3 letters."
]]>
From this we know, that the use of or
and and
as operators are limited within a pattern, for they must be enclosed in a parenthesis pair, otherwise they will be treated as a pattern. But it seems that logical operators are rarely used within patterns.
When treated as patterns, it must be either "all or
" or "all and
". They can't coexist, otherwise the compiler can't determine. This rule doesn't apply to those in parentheses.
Note that or else
is a special syntax, which doesn't belong to or
pattern, so or else
can coexist with and
.
Like if
, in pattern matching then
block can be indented. In this way we can omit then
:
We can find that even when simulating the traditional switch
, it's shorter than switch
.
In fact, loops violates the idea of functional programming. That's why I deliberately take away loops in FutureScript. But I'm confident that FutureScript can do anything that a loop can do, with the same or shorter length of syntax. (This chapter doesn't belong to the FutureScript specification. It just introduces the loop
function in the fus-ext
package. This chapter may be outdated. The fus-ext
tutorial governs the usage. For convenience, "loop" in this article usually means loop
.)
We can use FutureScript's extension package fus-ext
to implement loops.
Copy node_modules/fus-ext/examples/manifest.fus
file to your code's directory.
Your code can be like this:
console.log "This is \(i) time"
)
]]>
Here we used the "batch import" feature (for details see the "Module" section above). Note that the export of manifest.fus
should match the import of yours. If you want to use node
instead of es
, then you'll need to make corresponding changes in the version line of the two files.
If the iterator returns break
then it means to jump out of the loop, similar to JS's break
, but different in essence. Here break
is an expression, and only capable of cancelling the remaining cycles, not capable of cancelling the remaining part of the function. If break
then the loop
function returns break
. This example is a loop, from 0 to 9, but it will jump out on 5:
if i < 5
console.log "This is \(i) time"
else
break
)
]]>
If no count is set, it means forever, equivalent to JS's while (true)
:
This corresponds to JS's for
loop, from 1 to 10:
console.log i
}
]]>
This corresponds to JS's for
loop, from 10 to 1:
console.log i
}
]]>
Recursion is not special, but we added some simpler way. You even don't need to name a recursive function. Just think that if it invokes itself, why it must have a name? Just use the fun
keyword and it's OK. For example, if you don't want to use an external library, you can simulate a loop using recursion:
if i < 10
console.log i
fun(i + 1)
]]>
Use try
, catch
, finally
and throw
, like CoffeeScript. For example:
throw
can even be followed by nothing. If it's in catch
block then compiled into throw catchExceptionVar
, otherwise compiled into throw undefined
. For example:
When you want to rethrow an exception in catch
block, you can type less.
In essence, try...catch
is functional. For example, you can write:
Another thing different from CoffeeScript: If it has try
and finally
but doesn't have catch
, the compiled JS will have an empty catch
. For example:
In the above code, the outer catch
won't be triggered. The final result of a
will be 4, not 12. That's because in the compiled JS, the inner level will have an empty catch
.
So, in compiled JS, try
will always take a catch
. Then what's the equivalent of JS's try...finally
without catch
? Simply add a catch
and rethrow:
a
will be 12 now. Basically it's equivalent to the following JS:
The following table can help understand the mechanism of catching exceptions:
JS | CoffeeScript | FutureScript | |
---|---|---|---|
try...catch |
catchable | catchable | catchable |
try |
illegal | catchable | catchable |
try...catch...finally |
catchable | catchable | catchable |
try...finally |
uncatchable | uncatchable | catchable |
We can find that the rules of JS or Fus are simple. JS catches only in catch
. Fus always catches. But the rule of CoffeeScript is complex because of a special case try...finally
.
There's one thing to note, that a catch
variable is no different than a normal variable. Its scope is not restricted to the catch
block. This is the same as CoffeeScript but different from JS. See the code below:
The output is false
not true
. So, we should try to avoid using the same variable name in nested catch
.
If try
block or finally
block is an inline block, then it must occupy the whole line.
'wait
makes an asynchronous function internally synchronous-style, without changing its asynchronous essence. Consider the following JS:
ES2017 simplifies it into:
{
console.log(await (await (await aaa()).bbb()).ccc());
})();
]]>
In Fus, we simplify it further:
Once the code has 'wait
, the parent function will be compiled into asynchronous function. We don't need the async
keyword, which is totally redundant.
Another difference is that in JS we put await
to the left, but in Fus we put 'wait
to the right. Ours is better because it eliminates many parentheses and improves readability.
'wait
must be in a function.
"Field" and "property" of a class are synonyms. A field can be called a "method" if it's a function. Class fields (including static fields) and constructors (including static constructors) are called class members. Note: Class member is conceptually different from object member.
Example:
me.name: name
speak: <>
console.log me.name + " makes a noise."
Lion: class from Cat
speak: <>
super()
console.log me.name + " roars."
]]>
constructor
is simplified to new
.
JS's this
is replaced by me
. We also have another keyword Me
, where M is in uppercase, meaning the class of me
. When a method is called statically, Me
is identical to me
, but it's recommended to use Me
because it looks more like a class.
A highlight of FutureScript's classes is the "anti-conflict field", which is similar to "private field". The benefit of an anti-conflict field is not that the external code can't access it (though can't access it directly by object property). The benefit is that derived classes can safely define a field with the same name, without worrying about the possible conflicts. This is especially important in projects with very complex class structures.
First we look at a negative example. Assuming someone has written a library with an ES6 class in JS:
He has also written an API documentation of how to use User
's constructor and getUsername
method. So you write a HumanUser
class that inherits User
in JS:
Because he didn't tell you in documentation which "private fields" he had used, and because your "private field" happens to be _name
, your class can't work correctly.
In FutureScript, once a field starts with _
, it automatically becomes an anti-conflict field. These 2 classes can be modified as follows in FutureScript:
me._name: username
getUsername: <>
me._name
]]>
And
super()
me._name: name
getName: <>
me._name
]]>
Why can it avoid conflicts? Because me._name
isn't compiled into this._name
, but rather compiled into this[_name]
, where _name
isn't a string but a Symbol
instance. Once the compiler goes to me.
followed by _
, it will compile as such. You can test if you can still access this property using instance._name
in JS console.
Anti-conflict fields can also be in the class definition. For example:
An anti-conflict field can't be enclosed with quotation marks. So, if you want a normal field to start with _
, you can write it like me."_name"
, therefore it's not an anti-conflict field.
For static fields, we use static
. For example:
Anti-conflict fields also apply to static fields. Just rename me.
to Me.
.
Like C#, it also supports "static constructor". Just make static
followed by nothing. For example:
result: longTimeTask()
Me.part1: result.0
Me.part2: result.1
static ok: <> Me.part1'ok
]]>
Equivalent to:
Me.part1'ok
result: longTimeTask()
Website.part1: result.0
Website.part2: result.1
]]>
The benefit of a static constructor is that all class-related code can be held inside the class. This looks nicer, brings less typing, and makes variables unexposed.
In static constructors, me.
and Me.
both mean the class in the closure (i.e. the class itself), not the class of the current object.
C# programmers will not feel strange about getters and setters. We also support them:
me._name: @0
name'get: <>
me._name.toUpperCase()
name'set: <>
me._name: @0
]]>
The function of fun
can't be a class member.
You don't need to use new
when creating instances, because JS's new
is redundant - a class isn't useful as a normal function. So, in FutureScript, if a function is recognized as a class, then the function call will automatically have new
in generated JS. If not, then it won't have new
. The detailed logic is specified by the version line. In rare occasions, a function name of a library may violate the good naming conventions, causing it to be recognized as a class. In this case you can use nonew
keyword, which has a similar grammar as new
.
JS supports the form of new a
(without parentheses), but we don't support because of the risk of misunderstanding.
JS supports the form of new new a(b)(c)
, but we don't support, because it's too unreadable. You can write as new (new a(b))(c)
.
As long as there's a use case for instantiating, you should use a capitalized name for a class, otherwise the compiler will be affected. But if all use cases are static, then you can use any name.
Number
, String
, Boolean
and Symbol
are always recognized as non-classes, regardless of the version line.
FutureScript classes are ES6 classes, not those represented by a function in ES5 or CoffeeScript. So what's the difference? In ES5 or CoffeeScript, super
and this
could be in arbitrary order in the constructor of a derived class; But in FutureScript (or ES6) in the constructor of a derived class, me
can't be used before super
. This is stricter, but in fact it's the same as C# and most mainstream languages. Also it makes this possible: to inherit built-in classes like Array
and Error
, which is impossible in ES5.
But the grammar of FutureScript's super
is like CoffeeScript. You don't need to write super.methodName()
. Simply use super()
. Of course, we also support super.methodName()
because this can be used to call a method with a different name. We don't support CoffeeScript's standalone super
, but you can use super'(@)
or super(@...)
.
We know that generally a constructor doesn't have a return value. But in fact in JS (including ES6), a constructor can have a return value. The return value can be any object, thus you will create an object that's not an instance of the class. This is weird and useless (because this purpose can be achieved by using a static method). So in FutureScript, although constructors can have return values, they almost don't have any effects. What it creates is always an instance of this class.
Do not assign value to a prototype of another object inside a class. For example, this is unreasonable (though it won't raise error):
Abc.prototype.def: <>
doSomething()
]]>
It's because me
is bound to the outer class even in the inner prototype
function. The correct way is to write it outside the class. Regarding nested classes, they will work as expected, because the compiler can recognize the class
keyword and then interpret me
correctly. But nested classes are not very useful. It's suggested that classes be flat.
If you have used the library "underscore", you must be familiar with this code:
Pipe operator is |>
. It can adjust the order to a natural order, so it can be written as:
_.map(x -> x * 2) |> _.max
]]>
Note, that because of the precedence, you should avoid using space function call in pipe. For example, the above code can't be written as:
_.map x -> x * 2 |> _.max
]]>
Another operator is ::
, called "fat dot", used specifically in pipe expressions. If you want to use pipe along with dots, then fat dot brings more readability. For example:
_.map(x -> x * 2) :: map(x -> x + 1) |> _.max
]]>
Equivalent to:
_.map(x -> x * 2)).map(x -> x + 1)) |> _.max
]]>
So basically a fat dot is equal to a normal dot, except that it acts as an ending tag when it's on the right side of the pipe operator. Also, it is as "fat" as the pipe operator, so looks better.
Pipe can be used with asynchronous code. For example:
lib.process1'wait |> lib.process2'wait
y: data |> lib.remoteCombine(anotherData1)'wait |> lib.remoteCombine(anotherData2)'wait
console.log(x, y)
]]>
Equivalent to:
The equivalent JS code is:
{
var x = await lib.process2(await lib.process1(data));
var y = await lib.remoteCombine(await lib.remoteCombine(data, anotherData1), anotherData2);
console.log(x, y);
})();
]]>
Pipe also supports automatically adding new
. For example:
Foo
bar: 2 |> Bar()
]]>
Equivalent to JS's:
But if you use new
or nonew
, then you must add parentheses. For example, this is illegal:
nonew Foo
]]>
The code using pipe can be in either compatible mode or radical mode. But if an external library is also written in FutureScript, then this external library should better be written in comptatible mode, because the functions in it must take multiple arguments because pipe itself is to split arguments.
The dot-dot ..
method can be treated as a virtual method. Its use case is similar to pipe, but makes it even easier to use underscore:
_
console.log [3, 4, 5]..map(x -> x * 2)..max()
]]>
Equivalent to JS's:
You can mix dot-dot with dot, where dot is the object's native method. For example:
x * 2).map(x -> x + 1)..max()
]]>
The second map
is Array
's native method.
To assign to ..
you can use colon or as
.
It supports exporting or importing ..
, therefore you can put it into your manifest module for batch import, so that you don't need to assign to ..
in each file. For example you can write in your "manifest.fus":
_
]]>
All valid forms of import and export apply to ..
as well. For example:
Or:
The value assigned to ..
must be a function. This seems redundant but makes it stronger, for you can set different libraries for different types. Therefore, a library can truly become a "virtual method" of a certain class. There won't be any conflict even if there're duplicate function names in two libraries. For example:
if x is String
require("lodash")
else
require("underscore")
]]>
When ..
acts as an operator, it must be followed by a letter, _
or $
.
do
is a shorthand for (...)()
. For example:
Equivalent to:
The features are a little less, but purer than CoffeeScript's do
. It can be used to:
Which CoffeeScript feature is missing in FutureScript? See this CoffeeScript code:
button.onclick = ->
console.log button.textContent
]]>
Using do
to "hold" a variable name is not supported, because we strictly prohibit same-name variables in different levels. But now it seems there's no need to support it, because we have a better way:
button.onclick: --
console.log button.textContent
]]>
Or:
button.onclick: --
console.log button.textContent
true
]]>
This is where the functional style surpasses loops.
But what's the difference between forEach
and every
? This is beyond this specification but important so I want to point it out. These 2 are ES5 methods. When using forEach
, you can't simulate break
. But when using every
, you can neatly simulate break
- just let it return false
. For example:
console.log i
i < 5 ? true | false
]]>
We can also replace the true
and false
with the shorter 1
and 0
:
console.log i
i < 5 ? 1 | 0
]]>
Although do
can't hold a variable name like CoffeeScript, you can reach the target by naming it differently:
But the problem is that we don't have native loops, so this example is useless.
do
must be followed by a function literal. This is stricter than CoffeeScript.
These keywords or symbols are about "existence": 'ok
, ifvoid
, ifnull
.
a'ok
means a
isn't void
or null
.
ifnull
and ifvoid
have two forms respectively. One is for expression and the other is for colon assignment. For example:
When used in colon assignment, for now we don't support they coexist with [...]
, ,
or 'export
. For example, these are illegal:
You've already seen many apostrophes. These are called variants. Now let's gather them together.
There are 2 forms: unary '
and binary '
. Unary '
is the function's single-argument variant. Others are all binary '
.
How does the compiler determine if a '
is unary or binary? It checks the character immediately after '
. If it's a number or letter, it's binary; otherwise (e.g. a symbol, space or nothing at all) it's either unary or a parsing failure.
So these two are the same:
But this is different:
For readability, a unary'
can't be preceded by a space. So this is illegal:
A line can't start with '
. So this is illegal (but may be illegal in future versions):
For now there can be at most two '
before the colon.
in
is almost the same as CoffeeScript. Of course we don't support !in
, but you can use ... not in ...
or not ... in ...
. For example:
is
means the left side is of the right side type, similar to JavaScript's instanceof
but a little different. It can also check primitive types. So it's a mix of instanceof
and typeof
. If the right side is Number
, Boolean
, String
or Symbol
(must be verbatim, not a reference), it will use typeof
.
Similar to in
, to negate it you can use not is
, but it's recommended to use the good-looking isnt
. You can even use is not
.
These 4 behaves the same. You might ask if a is not A
is ambiguous. No, because a is (not A)
is meaningless - a boolean isn't a class.
delete
is a bit different from JS. It's no longer an expression. This is because in JavaScript strict mode delete
will never be false
, but will instead throw an error. Since we always use strict mode, there's no need to make it an expression.
rem
(remainder) means JS's %
. mod
is just another modulo operation, where the result has the same sign as the divisor. If the divisor is positive, it's guaranteed that the result is non-negative. So I think mod
can be used more widely than rem
. a mod b
is equivalent to JS's (a % b + b) % b
.
**
means exponentiation. For example:
The grammar of the unary +
and -
(treated as sign) is similar to CoffeeScript with slight difference. We require that it must be followed immediately by an operand. If it's followed by a space then it will be treated as the binary addition and subtraction. For example:
If it's not followed by a space, then if it's preceded by a letter, digit, _
, $
, )
, ]
, }
, "
or @
, then it will be treated as binary, otherwise unary.
Also, when treated as an operator, +
, -
, *
or /
can't be followed immediately by +
or -
.
Use pause
to insert a breakpoint. It's equivalent to JS's debugger
. We renamed it to pause
because debugger
is longer than 7 characters.
Use js"
. For example:
It suppresses all features of \
. For inline JS, double quotes can't be represented. But you can use apostrophes. If you must use double quotes, use formatted JS like the above example.
To work correctly, the inserted JS must be an expression as a whole, such as JS function expression, JS assignment expression. This acts the same as CoffeeScript.
Because it must be an expression as a whole, the outermost level can't contain semicolons. For example you can write this:
But you can't write this:
A variable name can't be a keyword. A variable name can be a JS keyword (as long as it's not a FutureScript keyword). For example, function
and instanceof
can be variable names.
In class definition, a property name, if not enclosed with double quotes, can't be new
or static
. Note that static
isn't a keyword. new
isn't a keyword in class definition (i.e. followed by a colon).
Object literal's property name doesn't have any restrictions.
Of course, if not enclosed with double quotes, the above names must meet the identifier restriction, that is: each character must be a letter, digit, _
or $
, and it doesn't start with a digit.
The result of an operation is an expression, but an operator isn't always an expression. It can be a block or a non-value thing. Not all operators have precedence. Those whose operands must occupy all the remaining area of the line (unless using <<
and >>
) don't have precedence, for example match
and class
. Postfixed if
also doesn't have precedence. In the following table, a
, b
, c
mean expression operands; m
, n
mean block operands; x
, y
, z
mean operands that's not always expressions or blocks.
Note: Precedence is ordered from lowest to highest. In references of other languages they are all from highest to lowest, but I think it's improper for this language because from the functional (recursive) point of view, it should be from "big" (low) to "small" (high). As for associativity, we are also opposite to the traditional way. The traditional table shows the order of execution, but our table shows the order of parsing (to be more accurate, parsing recursively). That is: If executed from left to right, because the right side is "bigger", it's defined as from right to left, simplified as "right"; If executed from right to left, because the left side is "bigger", it's defined as from left to right, simplified as "left". For example, 1 + 2 + 3
is parsed as plus(plus(1, 2), 3)
, and the bigger is shown first. In brief, you can just think we show everything in reverse order in the table.
Precedence | Operator type | Examples |
---|---|---|
0 left | Arrow Function | x -> m |
Diamond Function | <> m |
|
Dash Function | -- m |
|
1 left | Conditional | if a then m else n |
2 left | Space Function Call | a b |
Space new | new a b |
|
Space nonew | nonew a b |
|
3 right | or | a or b |
4 right | and | a and b |
5 left | not | not a |
6 right | Equality | a = b |
Inequality | a /= b |
|
Less Than | a < b |
|
Less Than Or Equal | a <= b |
|
Greater Than | a > b |
|
Greater Than Or Equal | a >= b |
|
7 right | in | a in b |
Negative in | a not in b |
|
is | a is b |
|
Negative is | a isnt b |
|
8 right | Addition | a + b |
Subtraction | a - b |
|
9 right | Multiplication | a * b |
Division | a / b |
|
Remainder | a rem b |
|
Modulo | a mod b |
|
10 left | Exponentiation | a ** b |
11 left | Unary Plus | +a |
Unary Negation | -a |
|
12 right | ifvoid | a ifvoid b |
ifnull | a ifnull b |
|
13 right | as | a as x |
export as | a export as x |
|
14 right | Pipe | a |> b |
15 right | Member Access | a.x |
Dot Dot Member Access | a..x |
|
Fat Dot Member Access | a :: x |
|
Function Call | a(b) |
|
new | new a(b) |
|
nonew | nonew a(b) |
|
Variant | a'x |
|
Function Variant | a' |
|
import | import "x" |
Q: CoffeeScript has return
, but FutureScript goes so far as to drop the return
?
A: return
is a jump statement, which functional style should avoid. Many functional languages, like F#, don't have return
. F# has loops, but doesn't have the break
statement that can jump out of the loop. Just like that people all think C's goto
is evil because goto
is jumping. Although return
is only able to jump out, not jump freely, so it's not as evil as goto
, but I think it's better to be pure in the beginning period of FutureScript. In fact it's very easy to reach the same target. For example you want to write:
if x isnt Number return
x * 2
]]>
But now you can write:
if x isnt Number
void
else
x * 2
]]>
If you don't like more lines, you can write:
x isnt Number ? void |
x * 2
]]>
The benefit of this way is that the structure is clearer. As long as it doesn't throw error, the exit of the function is always one: the bottom.
Q: Why are there no bitwise operators?
A: First, FutureScript is a high-level language, so there is no need to support them. Second, JavaScript's bitwise operations are specifically targeted towards 32-bit numbers, which is unreasonable.
Q: Why are there no generators?
A: Generators are suited for specialized uses, the general applications are limited. As for shortening the number of lines of code? Adding a framework can achieve this goal, why do we need to modify the language? As for applications to asynchronous operations, FutureScript already has better methods for dealing with them.
Q: Why must the indent inside a formatted string be greater than the indent of the quotation marks? Can't they be equal?
A: If we allow it to be equal or smaller, this can lead to compilation errors, if some line within the string happens to start with a quotation mark.
Q: Why are there diamond functions in addition to arrow functions?
A: My objective was to eliminate the parentheses around the arguments that are found in CoffeeScript. But if there are no parentheses then it can lead to ambiguity. So I came up with this solution, if you do not add parenthesis, then there must be one argument. But what about the case when there are zero arguments? So I made another symbol. As for multiple arguments, you must add parentheses, but multiple arguments are not in my ideal (radical mode) concept, so this is also acceptable. There is another reason, which is that CoffeeScript's use of the arrow to indicate a function with zero arguments is awkward and misleading, as with arrows it feels like there should be something on the left.
Q: Why doesn't radical mode get rid of the arrow? There is also @
available for use.
A: In radical mode, under most conditions there is no need to give arguments names, but if you have multiple levels of functions, when the inner function is using outer arguments, you need to name the arguments.
Q: Why are there no reserved words?
A: Many languages have many reserved words, but I think that if you try to predict future features when you design the language, then you might not predict them correctly. In FutureScript, the version number achieves the goal of "safety", because so long as you do not change the version number, there will not be any compatibility issues.
Q: Not supporting var
is inspired by CoffeeScript, but why not support ES6's let
and const
?
A: My idea is, a programmer might not be familiar with the files outside the one he is currently developing, but he is usually familiar with other parts of the file. let
provides block-level safety, but so long as you are familiar with the block, you can use a different variable name. Block-level safety is too small, function-level safety is already strong enough. Otherwise the language complexity will increase. let
also conflicts with CoffeeScript's style. Furtheremore, that's because we currently compile variables to var
. I thought of compiling all variables to let
, but considering code like this:
0
a: 1
b: 2
c: 3
else
a: 4
b: 5
c: 6
console.log(a + b + c)
]]>
If we compile to let
, then this code will be incorrect, we must assign values to a
, b
and c
at the top for it to work. So I think that it is better to compile to var
.
As forconst
, I think that the idea has a problem, const
just applies to constants, and making it such that some things cannot change is not to prevent us from modifying them, but rather to prevent others from modifying them. Normally you make a library, and when others use it, at runtime they might change the internal logic. Variables are visible to just yourself, and others cannot modify them, what need is there to make them constants? What you really want to protect are the attributes and methods of objects, and this cannot be accomplished by const
. You can use Object.defineProperty
to accomplish it. But to achieve this you must alter all objects including built-in objects to make these methods more rigid (i.e. become almost like static programming languages), and this work would take too much time, while the value is questionable, so at the moment it's not done.
Q: Why not support object destructuring assignment and complex array destructuring assignment?
A: Object destructuring assignment is overly complex. They suit some part of human's intuition, but violate other part of human's intuition, causing them to be confused from the start. Complex array destructuring assignment is medium complexity, it may be supported in the future. As for object destructuring assignment, even if supported in the future, the grammar will be changed to be more natural.
Q: Why @
means arguments
?
A: First the appearance of @
resembles an "a". Second, @
is the symbol which looks least like a symbol, we will often use an @
alone as an expression, so if we use another symbol here, it will not look like an expression, which will look awkward.
Q: Why do multi-argument functions need parentheses? Can't you omit them in CoffeeScript?
A: Multiple arguments need commas, otherwise it is difficult to see what the commas are delimiting, and it will often be unintuitive. Furthermore, compatibility mode is now the default, but because I think that radical mode is more perfect, if we could omit parentheses with multiple arguments, then it would encourage more people to use compatibility mode.
Q: @
is handy. If in the context of catch
we could also use a symbol that would be great.
A: I originally thought of using a new symbol to indicate a context inside a child statement, not just limited to "catch", but after some thought, I found it's not suitable. When using @
you can see very clearly which function corresponds to the @
, because function symbols are very easy to identify. But the only characteristic of a child statement is the indent, and we use indents a lot. This symbol's meaning can easily change, causing confusion. If this new symbol is restricted to just "catch", then the applicability is too small.
Q: Why in as
we can't use commas to assign a value to multiple identifiers?
A: If supported, expressions like [aaa as a, bbb]
can easily become ambiguous.
Q: If we keep versions of the compiler, won't it become increasingly large? Sounds scary!
A: Actually it's not that scary, while building FutureScript I adopted some techniques to ensure that the increase in size is not so large.
Q: Are all the older versions of the compiler really included?
A: We have included as many as possible. But if we discover a security hole in an older compiler version, then we will delete it. If there is no compiler equivalent to that of the code, during compilation there will be a warning, and it will automatically use a newer compiler.
Q: CoffeeScript's chaining method call doesn't need parentheses, why do we need parentheses here?
A: CoffeeScript actually forcibly adds a rule, which breaks the unity of the langauge, and although for some needs the language becomes more concise, but for other needs it becomes more complex. So I decided not to use this method.
Q: Why it's not good for a module to have both default exports and named exports?
A: You may well separate them to two modules, or give the "default export" a name other than default
(Actually a default export is one of named exports, with the name default
).
Q: Why ->
doesn't define @
?
A: If arguments have names, using @
is unnecessary. This behavior is like the =>
function in ES6.
Q: In JS we can use typeof a === "undefined"
to check whether a variable is declared without throwing exceptions, but why we can't here?
A: This seems useless. FutureScript's variables are all declared at the top of the function body, so whether a variable is declared isn't dynamic, so this shouldn't be what the runtime should do.
Q: Why the precedence of the exponentiation operator **
differs from that in CoffeeScript?
A: Although it's different with CoffeeScript, it's the same as other languages that have this operator, such as F# and ES2017. CoffeeScript may have considered that if -3 ** 2
means -32 then it's -9, but this looks strange, unless you add a space so that it's - 3 ** 2
, or you use no space so that it's -3**2
. But we rarely write this way, not to mention the former conflicts with multiline connection. Another reason is that CoffeeScript makes **
special so that it's inconsistent. In fact the precedence of *
should be lower by just one level than **
, but luckily (-a) * b
is always equivalent to -(a * b)
so maybe they think it doesn't matter so they didn't adjust.
Q: The precedence of not
is greatly lowered. Is it reasonable?
A: This looks more natural and more in line with human reading. I admit that such change has never been done by anybody, so it takes risks. To see if there're counter-examples that supports maintaining the common precedence, I reviewed the source code of GitHub Atom (It's written in CoffeeScript) as well as my own code. I found that almost no not
is simpler under the common precedence. This gave me confidence. I think it's very bad to use not
in operands in an arithmetic operation such as addition and multiplication. Maybe some people wants to take advantage of this trick, but such code is very unreadable. Good code will not have not a + not b
. We shouldn't use such counter-examples. The same goes for =
, <
and >
.
Q: Why use ###
for comments? Shouldn't it be more concise if's like formatted string?
A: Sometimes we want to suppress a code region by commenting it, in this case we don't want to adjust the indents.
Q: Shouldn't it be better if export {abc}
means named export and export abc
means default export?
A: Originally I had this idea, but this can lead to ambiguity, for {abc}
can also be treated as an object.
Q: Why not support Unicode variable names?
A: We rarely need this. But later versions may support it.
Q: What's the use of above
keyword?
A: above
was intended to help you read code in special forms, but later I decided to support this "special forms" in future versions, so now above
has no effect.
Q: CoffeeScript's ?.
is very handy. Why Fus used to support the similar 'ok.
but later dropped it?
A: If support, then consider the following code:
What you really want is to call foo()
when 'ok
and the entire chain isn't 3
. But in fact, when not 'ok
, the entire chain is void
, which is also not 3
, so it will also call foo()
. So I think this feature can create traps, which isn't perfect.
Another reason is that the original support isn't complete. It has flaws and bugs. To make it complete, the structure of the compiler should be greatly changed. That's a huge task. So it's better drop it for now and change it later if needed.