One language can change the world.
Go+ is a "DSL" for all domains.
Rob Pike once said that if he could only introduce one feature to Go, he would choose interface
instead of goroutine
. classfile
is as important to Go+ as interface
is to Go.
In the design philosophy of Go+, we do not recommend DSL
(Domain Specific Language). But SDF
(Specific Domain Friendliness) is very important. The Go+ philosophy about SDF
is:
Don't define a language for specific domain.
Abstract domain knowledge for it.
Go+ introduces classfile
to abstract domain knowledge.
- STEM Education: spx: A Go+ 2D Game Engine
- Web Programming: yap: Yet Another HTTP Web Framework
- Web Programming: yaptest: HTTP Test Framework
- Web Programming: ydb: Database Framework
- DevOps: gsh: Go+ DevOps Tools
- Unit Test: classfile: Unit Test
- Mechanism: What's Classfile
Sound a bit abstract? Let's take web programming as an example. First let us create a file named get.yap with the following content:
html `<html><body>Hello, YAP!</body></html>`
Execute the following commands:
gop mod init hello
gop get github.com/goplus/yap@latest
gop mod tidy
gop run .
A simplest web program is running now. At this time, if you visit http://localhost:8080, you will get:
Hello, YAP!
YAP uses filenames to define routes. get.yap
's route is get "/"
(GET homepage), and get_p_#id.yap
's route is get "/p/:id"
(In fact, the filename can also be get_p_:id.yap
, but it is not recommended because :
is not allowed to exist in filenames under Windows).
Let's create a file named get_p_#id.yap with the following content:
json {
"id": ${id},
}
Execute gop run .
and visit http://localhost:8080/p/123, you will get:
{"id": "123"}
Why is yap
so easy to use? How does it do it? Let us analyze the principles one by one.
What's a classfile? And why it is called classfile
?
First let's create a file called Rect.gox
:
var (
Width, Height int
)
func Area() int {
return Width * Height
}
Then we create hello.gop
file in the same directory:
rect := &Rect{10, 20}
println rect.area
Then we execute gop run .
to run it and get the result:
200
This shows that the Rect.gox
file actually defines a class named Rect
. If we express it in Go syntax, it looks like this:
type Rect struct {
Width, Height int
}
func (this *Rect) Area() int {
return this.Width * this.Height
}
So the name classfile
comes from the fact that it actually defines a class.
You may ask: What is the value of doing this?
The value lies in its ease of use, especially for children and non-expert programmers. Let's look at this syntax:
var (
Width, Height int
)
func Area() int {
return Width * Height
}
Defining variables and defining functions are all familiar to them while learning sequential programming. They can define new types using syntax they already know by heart. This will be valuable in getting a wider community to learn Go+.
Of course, this is not enough to make classfiles an exciting feature. What's more important is its ability to abstract domain knowledge. It is accomplished by defining base class
for a class and defining relationships between multiple classes
.
What is a classfile
? Usually it consists of a project class
and multiple worker classes
. The classfile not only specifies the base class
of all project class
and worker classes
, but also organizes all these classes together by the base class of project class. There can be no worker classes, that is, the entire classfile consists of only one project class.
This is a bit abstract. Let's take the 2D Game Engine spx as an example. The base class of project class of spx classfile
is called Game
. The base class of worker class is called Sprite
. Obviously, there will only be one Game instance in a game, but there are many types of sprites, so many types of worker classes are needed, but they all have the same base class called Sprite
. Go+'s classfile allows you to specify different base classes for different worker classes. Although this is rare, it can be done.
How does Go+ identify various class files of a classfile? by its filename. By convention, if we define a classfile called foo
, then its project class is usually called main_foo.gox
, and the worker class is usually called xxx_foo.gox
. If this classfile does not have a worker class, then the project class only needs to ensure that the suffix is _foo.gox
, and the class name can be freely chosen.
The earliest version of Go+ allows classfiles to be identified through custom file extensions. For example, the project class of the spx classfile
is called main.spx
, and the worker class is called xxx.spx
. Although this ability to customize extensions is still retained for now, we do not recommend its use and there is no guarantee that it will continue to be available in the future.
Go+ has a built-in classfile to simplify unit testing. This classfile has the file suffix _test.gox
.
Suppose you have a function named foo
:
func foo(v int) int {
return v * 2
}
Then you can create a foo_test.gox
file to test it (see unit-test/foo_test.gox):
if v := foo(50); v != 100 {
t.error "foo(50) ret: ${v}"
}
t.run "foo -10", t => {
if foo(-10) != -20 {
t.fatal "foo(-10) != -20"
}
}
t.run "foo 0", t => {
if foo(0) != 0 {
t.fatal "foo(0) != 0"
}
}
You don't need to define a series of TestXXX
functions like Go, just write your test code directly.
If you want to run a subtest case, use t.run
.
This classfile has the file suffix .yap
. See yap: Yet Another HTTP Web Framework for more details.
YAP uses filenames to define routes. get.yap
's route is get "/"
(GET homepage), and get_p_#id.yap
's route is get "/p/:id"
(In fact, the filename can also be get_p_:id.yap
, but it is not recommended because :
is not allowed to exist in filenames under Windows).
Let's create a file named get_p_#id.yap with the following content:
json {
"id": ${id},
}
Execute gop run .
and visit http://localhost:8080/p/123, you will get:
{"id": "123"}
In most cases, we don't use the html
directive to generate html pages, but use the yap
template engine. See get_p_#id.yap:
yap "article", {
"id": ${id},
}
It means finding a template called article
to render. See yap/article_yap.html:
<html>
<head><meta charset="utf-8"/></head>
<body>Article {{.id}}</body>
</html>
By default the YAP server runs on localhost:8080
, but you can change it in main.yap file:
run ":8888"
Static files server demo (main.yap):
static "/foo", FS("public")
static "/"
run ":8080"
This classfile has the file suffix _ytest.gox
.
Suppose we have a web server (foo/get_p_#id.yap):
json {
"id": ${id},
}
Then we create a yaptest file (foo/foo_ytest.gox):
mock "foo.com", new(AppV2) // name of any YAP v2 web server is `AppV2`
id := "123"
get "http://foo.com/p/${id}"
ret 200
json {
"id": id,
}
The directive mock
creates the web server by mockhttp. Then we write test code directly.
You can change the directive mock
to testServer
(see foo/bar_ytest.gox), and keep everything else unchanged:
testServer "foo.com", new(AppV2)
id := "123"
get "http://foo.com/p/${id}"
ret 200
json {
"id": id,
}
The directive testServer
creates the web server by net/http/httptest and obtained a random port as the service address. Then it calls the directive host to map the random service address to foo.com
. This makes all other code no need to changed.
For more details, see yaptest - Go+ HTTP Test Framework.
This classfile has the file suffix .spx
. It is the earliest classfile in the world.
Through this example you can learn how to implement dialogues between multiple actors.
Here are some codes in Kai.spx:
onStart => {
say "Where do you come from?", 2
broadcast "1"
}
onMsg "2", => {
say "What's the climate like in your country?", 3
broadcast "3"
}
onMsg "4", => {
say "Which seasons do you like best?", 3
broadcast "5"
}
We call onStart
and onMsg
to listen events. onStart
is called when the program is started. And onMsg
is called when someone calls broadcast
to broadcast a message.
When the program starts, Kai says Where do you come from?
, and then broadcasts the message 1
. Who will recieve this message? Let's see codes in Jaime.spx:
onMsg "1", => {
say "I come from England.", 2
broadcast "2"
}
onMsg "3", => {
say "It's mild, but it's not always pleasant.", 4
# ...
broadcast "4"
}
Yes, Jaime recieves the message 1
and says I come from England.
. Then he broadcasts the message 2
. Kai recieves it and says What's the climate like in your country?
.
The following procedures are very similar. In this way you can implement dialogues between multiple actors.
Through this example you can learn how to define variables and show them on the stage.
Here are all the codes of Dragon:
var (
score int
)
onStart => {
score = 0
for {
turn rand(-30, 30)
step 5
if touching("Shark") {
score++
play chomp, true
step -100
}
}
}
We define a variable named score
for Dragon
. After the program starts, it moves randomly. And every time it touches Shark
, it gains one score.
How to show the score
on the stage? You don't need write code, just add a stageMonitor
object into assets/index.json:
{
"zorder": [
{
"type": "stageMonitor",
"target": "Dragon",
"val": "getVar:score",
"color": 15629590,
"label": "score",
"mode": 1,
"x": 5,
"y": 5,
"visible": true
}
]
}
Through this example you can learn:
- Clone sprites and destory them.
- Distinguish between sprite variables and shared variables that can access by all sprites.
Here are some codes in Calf.spx:
var (
id int
)
onClick => {
clone
}
onCloned => {
gid++
...
}
When we click the sprite Calf
, it receives an onClick
event. Then it calls clone
to clone itself. And after cloning, the new Calf
sprite will receive an onCloned
event.
In onCloned
event, the new Calf
sprite uses a variable named gid
. It doesn't define in Calf.spx, but in main.spx.
Here are all the codes of main.spx:
var (
Arrow Arrow
Calf Calf
gid int
)
run "res", {Title: "Clone and Destory (by Go+)"}
All these three variables in main.spx are shared by all sprites. Arrow
and Calf
are sprites that exist in this project. gid
means global id
. It is used to allocate id for all cloned Calf
sprites.
Let's back to Calf.spx to see the full codes of onCloned
:
onCloned => {
gid++
id = gid
step 50
say id, 0.5
}
It increases gid
value and assigns it to sprite id
. This makes all these Calf
sprites have different id
. Then the cloned Calf
moves forward 50 steps and says id
of itself.
Why these Calf
sprites need different id
? Because we want destory one of them by its id
.
Here are all the codes in Arrow.spx:
onClick => {
broadcast "undo", true
gid--
}
When we click Arrow
, it broadcasts an "undo" message (NOTE: We pass the second parameter true
to broadcast to indicate we wait all sprites to finish processing this message).
All Calf
sprites receive this message, but only the last cloned sprite finds its id
is equal to gid
then destroys itself. Here are the related codes in Calf.spx:
onMsg "undo", => {
if id == gid {
destroy
}
}
Through this example you can learn:
- How to keep a sprite following mouse position.
- How to fire bullets.
It's simple to keep a sprite following mouse position. Here are some related codes in MyAircraft.spx:
onStart => {
for {
# ...
setXYpos mouseX, mouseY
}
}
Yes, we just need to call setXYpos mouseX, mouseY
to follow mouse position.
But how to fire bullets? Let's see all codes of MyAircraft.spx:
onStart => {
for {
wait 0.1
Bullet.clone
setXYpos mouseX, mouseY
}
}
In this example, MyAircraft
fires bullets every 0.1 seconds. It just calls Bullet.clone
to create a new bullet. All the rest things are the responsibility of Bullet
.
Here are all the codes in Bullet.spx:
onCloned => {
setXYpos MyAircraft.xpos, MyAircraft.ypos+5
show
for {
wait 0.04
changeYpos 10
if touching(Edge) {
destroy
}
}
}
When a Bullet
is cloned, it calls setXYpos MyAircraft.xpos, MyAircraft.ypos+5
to follow MyAircraft
's position and shows itself (the default state of a Bullet
is hidden). Then the Bullet
moves forward every 0.04 seconds and this is why we see the Bullet
is flying.
At last, when the Bullet
touches screen Edge
or any enemy (in this example we don't have enemies), it destroys itself.
These are all things about firing bullets.