Out of the box, back to intuition - Feel Lang Design

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
}
comments powered by Disqus