Design background
When I was developing a multi-platform XyKey a few years ago, I didn’t have the option because the cross-platform solution wasn’t mature enough at the time Instead of a cross-platform implementation, I chose to develop for each platform using the official language, for which I approached Java/ Kotlin (Android), Swift/OC (iOS), C# (UWP). I also work on blockchain technology, and the existing mainstream blockchain solutions also make extensive use of JS + Go’s Combining front and back-end product development.
After switching back and forth between languages, I became interested in the grammatical differences in expressing the same functionality in different languages, which led to an exploration of grammatical design, which led to the birth of Feel.
Language design problem number one
In many languages, there is more than one way to express a function.
Do we need to design different syntaxes for the same requirements?
Below are some examples of functions in the languages I’ve used, all eg are functions.
Go: Most of the time functions are declared with func, it’s one of the more consistent design, but in interface still uses a different way of describing things.
func eg1(x int) int {
return 1
}
var eg2 = func(x int) int {
return 1
}
type foo struct {
eg3 func(int) int
}
type bar interface {
eg4(int) int
}
C#: uses C-style descriptions most of the time, but uses counterintuitive generic types in function types. And the Lambda syntax doesn’t see the connection to functions.
int eg1(int x)
{
Func<int,int> eg2 = (int x) =>
{
return 1;
};
return 1;
}
interface foo
{
int eg3(int x);
}
Action<int> eg4;
Kotlin: function definition, function type, and lambda are the three styles.
fun eg1(x: Int): Int {
val eg2: (Int) -> Int = { i ->
1
}
return 1
}
val eg3 = fun(x: Int): Int {
return 1
}
interface name {
fun eg4(x: Int)
val eg5: (Int) -> Unit
}
Swift: The better thing about swift is that function definitions and function types are represented using the same arrows, but in the Lambda, however, uses in for partitioning.
func eg1(x: Int) -> Int {
let eg2: (Int) -> Int = { i in
return 1
}
return 1
}
protocol name {
func eg3(x: Int)
}
var eg4: (Int) -> ()
Bind a name to a resource, also can be written in many different ways.
Below are some of the language identifier representations I’ve used, all eg is an identifier of a resource.
Swift: Using different prefixes for different types of bind identifiers is one of the better practices.
var eg1 = 1
let eg2 = 1
func eg3() {}
class eg4 {}
protocol eg5 {}
Go: variables, constants, and functions use one style, and defined types use another.
var eg1 = 1
const eg2 = 1
func eg3() {}
type eg4 struct{}
type eg5 interface{}
C#: Classes and interfaces use one style, variables, constants, and functions use three different styles.
int eg1 = 1
const int eg2 = 2
int eg3() {}
class eg4 {}
interface eg5 {}
Language design problem number two
Once we start developing a project of a certain size, the importance of coding conventions is always repeated. Varying code styles can make it difficult for us to collaborate.
If the specification is so important, should we force it at the language level?
Below I give several different styles of code, and in the absence of a uniform style, we may see the following codes coexist.
if (foo == 0)
{
print(foo)
}
else if (foo == 1)
{
print(foo)
}
else
{
print(foo)
}
//////////////
if (foo == 0) {
print(foo)
}
else if (foo == 1) {
print(foo)
}
else {
print(foo)
}
//////////////
if (foo == 0) {
print(foo)
} else if (foo == 1) {
print(foo)
} else {
print(foo)
}
Of course, one might also write the following.
if (foo == 0)
{
print(foo)
}
else if (foo == 1)
{
print(foo)
}
else
{
print(foo)
}
//////////////
if (foo == 0) print(foo)
else if (foo == 1) print(foo)
else print(foo)
Language design problem number three
Some languages are dynamic and some are static, and they have their own merits, but we try to add static features to dynamic languages (TypeScript) and dynamic features to static languages (Go).
Is there a balance between this static and dynamic solution?
TypeScript: Through an implicit interface, dynamic types are given a type check so that only objects that meet the interface requirements can be used.
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
Go: implements a static duck type via an implicit interface, where only objects that meet the function signature requirements are used.
type LabelledValue interface {
Label() string
}
func printLabel(labelledObj LabelledValue) {
println(labelledObj.Label())
}
type Obj struct {
size int
}
func (this Obj) Label() string {
return "Size 10 Object"
}
Language design problem number four
There are more and less keywords in different languages, but in fact most languages can be Turing-complete.
Do we need a lot of keywords? Or, if there are no keywords, is it okay?
Below are keywords for a language, and some contextual keywords not shown.
| keyword | | | |
| --------- | ---------- | --------- | --------- |
| abstract | as | base | bool |
| break | byte | case | catch |
| char | checked | class | const |
| continue | decimal | default | delegate |
| do | double | else | enum |
| event | explicit | extern | false |
| finally | fixed | float | for |
| foreach | goto | if | implicit |
| in | int | interface | internal |
| is | lock | long | namespace |
| new | null | object | operator |
| out | override | params | private |
| protected | public | readonly | ref |
| return | sbyte | sealed | short |
| sizeof | stackalloc | static | string |
| struct | switch | this | throw |
| true | try | typeof | uint |
| ulong | unchecked | unsafe | ushort |
| using | virtual | void | volatile |
| while | | | |
Key issues
1 . Do I need more than one syntax for the same function?
We don’t need multiple grammars, the same needs can be unified organically.
2 . Is compulsory regulation necessary?
Mandating specifications reduces the stress of code reading and maintenance, and implementing specifications at the language level improves collaboration efficiency for all users.
3 . Do we want a static type or a dynamic type?
We want both static checks and dynamic degrees of freedom. Static duck type might be a solution.
4 . Are keywords necessary?
If the syntax structure is small enough, we can try removing the keyword.
Functions
Our needs for function syntax can be summarized as follows.
- Functions are also values, and the way you define a function should be the same as the way you define a function variable.
- The type in a function definition should be the same as the type of the function.
- Lambda expressions should be remarkably similar to function definitions.
Suppose that all our resources are defined using the form let id : type = XXX
.
Then we can start by giving a function syntax like this.
let foo = func(x : int) int {
return 1
}
The syntax is very common, using the func
keyword to define types, a formal parameter in ()
, and a return value after that.
Next we consider that modern function designs often allow multiple return values, so we need to use ()
to wrap more return values.
let foo = func(x : int, y : bool) (int, bool) {
return (1, true)
}
This is too close to ()
of the form parameter and return value type, which is difficult to read and requires a separator, so we can introduce ->
.
let foo = func(x : int, y : bool) -> (int, bool) {
return (1, true)
}
When ->
is added, ()->()
can form the type structure of a function, at which point the existence of func
is not necessary, so we remove the keyword.
let foo = (x : int, y : bool) -> (int, bool) {
return (1, true)
}
Writing ()
twice every time is a bit of a pain, but we don’t really need two ()
s. We can combine the formal parameter and return type put them together and split them using ->
. By the way, you can also omit the ()
of return
as well.
let foo = (x : int, y : bool -> int, bool) {
return 1, true
}
Wait, do we still need the return
keyword? I don’t think it’s needed, we could use a better looking <-
.
let foo = (x : int, y : bool -> int, bool) {
<- 1, true
}
The left and right functions seem a bit unbalanced, we can force the return value types to have the same name description to make them look more consistent. and can also give more user-friendly instructions.
let foo = (x : int, y : bool -> a : int, b : bool) {
<- 1, true
}
Here the function description has been shaped, we use (->)
for the function type and {}
for the function logic.
As long as we have some type derivation skills, we can continue to omit argument and return types, and both lambda and function declarations can share a common description.
let foo : (int, bool -> int, bool) = (x, y) {
<- 1, true
}
Example of an unparallel function with function parameters.
let foo = () {}
let bar = (fn : (->)) {}
Definitions
Our need for a definition grammar can be summarized as follows.
- All acts of creating a name are definitions and should be used in the same way.
- Names have a higher reading priority than types and should be preceded by the name.
- Distinguish between variable and immutable.
Suppose we start by defining immutable with let xxx : type = value
and define variable with var xxx : type = value
.
let foo : int = 0
var bar : int = 0
Using var
is equivalent to one more way of defining declarations, rather than using mut
to declare variability, which may have consistency.
let foo : int = 0
let mut bar : int = 0
Let’s think about the fact that this structure actually holds without the let
keyword, so we remove it.
foo : int = 0
mut bar : int = 0
Now we are left with the keyword mut
, which in many specifications is recommended to describe constants in upper case and variables in lower case. Let’s just let it be the syntax.
Foo : int = 0
bar : int = 0
By this point, we don’t need to explicitly write the type either, as long as type derivation is supported, and omitting the type allows us to combine it into :=
.
Foo := 0
bar := 0
Of course, if it doesn’t necessarily carry a value, we can also omit the right side and keep the type.
Foo : int
bar : int
Now that our definition syntax is complete, replacing the example of the previous function doesn’t require changing much of anything.
Foo := (x : int, y : bool -> a : int, b : bool) {
<- 1, true
}
Select structure
The form and functionality of the if
selection structure is very mature, but there are possibilities for further simplification of the format.
Let’s start with a common if
statement, and assume that {
is not line feedable.
if (foo == 0) {
......
} else if (foo == 1) {
......
} else if (foo == 2) {
......
} else {
......
}
Removing ()
doesn’t seem to affect anything, so let’s remove it first.
if foo == 0 {
......
} else if foo == 1 {
......
} else if foo == 2 {
......
} else {
......
}
We assume that else if
can’t be a line break either, it has to be followed by }
. With this constraint, we can omit else if
.
if foo == 0 {
......
} foo == 1 {
......
} foo == 2 {
......
} else {
......
}
By the same token, else
is not really needed; we can replace it with |
.
For consistency, we also use |
in the else if
position.
if foo == 0 {
......
} | foo == 1 {
......
} | foo == 2 {
......
} | {
......
}
Now there is only one if
left, we replace it with ?
for selectivity, replacing it with ?
? foo == 0 {
......
} | foo == 1 {
......
} | foo == 2 {
......
} | {
......
}
Now that we’ve completed the basic form of the selection structure, we’re compressing more code by forcing the specification.
Loop structure
for
is the most common type of loop structure, and we continue to try to simplify it further.
The simplest example is given first.
for (let i in foo) {
......
}
Omit the less useful `()'.
for let i in foo {
......
}
let
is used to define the object to be retrieved from the set, and we can change the definition syntax to the previous one.
for i := in foo {
......
}
We can replace in
with another syntax, ...
to represent the expansion of a set, so we can replace in
.
for i := foo... {
......
}
Now there is only the for
keyword left, and @
is perfect for specifying the selected thing, which we replace.
@ i := foo... {
......
}
This gives us the basic shape of the circular structure.
Object templates
A type is a way for us to describe the data and behavior of an object, and we can use it both as a template for constructing data and should be able to think of it as an interface for describing behavior.
This allows us to use it as a static duck type that carries data, behavior, and the ability to impose constraints at the same time.
Suppose we call it a class
.
class Foo {
var label = "I am Label"
let Show = func() {
Print(label)
}
}
Foo
contains both fields and functions, which can be used consistently.
Let’s start by replacing the syntax with a uniform definition.
Foo := class {
label := "I am Label"
Show := (->) {
Print(label)
}
}
We can remove the unique keyword class
and replace it with another $
, which is often used for templates.
Foo := $ {
label := "I am Label"
Show := (->) {
Print(label)
}
}
Every type needs to construct data objects in some way, and this behavior is actually very similar to functions. We pass the value needed for the field, and an object is returned. So why don’t we associate types with functions and use arguments to construct the fields.
Foo := $(label := "I am Label") {
Show := (->) {
Print(label)
}
}
This allows us to reuse the function’s properties, such as named parameters, default parameters, etc., at the location of the field.
Now we build the object as a function. For some degree of distinction, $
has been left in the call here.
a := Foo$()
a.Show()
Based on the properties of the duck type, we can define an interface to the Shower
to use the behavior of Foo
.
With the above syntax, we can describe an interface by simply dropping ()
.
Shower := $ {
Show : (->)
}
Use_Shower := (s : Shower->) {
s.Show()
}
Use_Shower( Foo$() )
Since Foo
has a Show
method, it can be used naturally as a Shower
.
We then introduce a syntax that introduces a delegate within the object template, which automatically implies everything about it according to the type it specifies, thus making it easy to reuse code.
For example, Reader
contains a Shower
, then Reader
contains Show
.
Reader := $ {
Shower
Read : (->v : string)
}
Then we can also make the Bar
contain Foo
, then Bar
contain the label
and Show
.`
Bar := $(foo := Foo$()) {
foo
Read := (->v : string) {
<- label
}
}
At this point we can use the Bar
as if it were a Reader
.
Use_Reader := (r : Reader->) {
r.Show()
Print(r.Read())
}
Use_Reader( Bar$() )
We implement some of the object-oriented features with the help of delegate and duck types, without relying on inheritance, choosing a balance between static and dynamic types.
Finally
Welcome star https://github.com/kulics-works/feel
The Feel language is currently experimental and not production ready, and some designs may be released with the llvm side of the Changes due to push, discussion email (kulics@outlook.com) or issue are welcome.
Posting another snippet of leetcode #16 for reference
ThreeSumClosest := (nums : List[Int], target : Int -> v : Int) {
length := nums.Size()
nums.sort()
closs := nums.(0) + nums.(1) + nums.(2)
@ i := 0.Up_until(length)... {
l, r := i + 1, length - 1
@ l < r {
sum := nums.(i) + nums.(l) + nums.(r)
? abs(sum - target) < abs(closs - target) {
closs = sum
} | sum > target {
r -= 1
} | {
l += 1
}
}
}
<- closs
}