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.
Sound a bit abstract? Let's take web programming as an example. First let us initialize a hello project:
gop mod init hello
Then we have it reference a classfile called yap
as the HTTP Web Framework:
gop get github.com/goplus/yap@latest
We can use it to implement a static file server:
static "/foo", FS("public")
static "/" # Equivalent to static "/", FS("static")
run ":8080"
We can also add the ability to handle dynamic GET/POST requests:
static "/foo", FS("public")
static "/" # Equivalent to static "/", FS("static")
get "/p/:id", ctx => {
ctx.json {
"id": ctx.param("id"),
}
}
run ":8080"
Save this code to hello_yap.gox
file and execute:
mkdir -p yap/static yap/public # Static resources can be placed in these directories
gop mod tidy
gop run .
A simplest web program is running now. At this time, if you 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}
printlnrect.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. For example, our previous yap
framework only has project class, so a file name like hello_yap.gox
can be correctly recognized.
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. spx
will likely be the only classfile to use a custom extension.
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.gox
.
Before using yap
, you need to add it to go.mod
by using go get
:
go get github.com/goplus/yap@latest
Find require github.com/goplus/yap
statement in go.mod
and add //gop:class
at the end of the line:
require github.com/goplus/yap v0.7.2 //gop:class
demo in Go+ classfile (hello_yap.gox):
get "/p/:id", ctx => {
ctx.json {
"id": ctx.param("id"),
}
}
handle "/", ctx => {
ctx.html `<html><body>Hello, <a href="/p/123">Yap</a>!</body></html>`
}
run ":8080"
Static files server demo in Go+ classfile (staticfile_yap.gox):
static "/foo", FS("public")
static "/"
run ":8080"
demo in Go+ classfile (blog_yap.gox, article_yap.html):
get "/p/:id", ctx => {
ctx.yap "article", {
"id": ctx.param("id"),
}
}
run ":8080"
This classfile has the file suffix _ytest.gox
.
Before using yaptest
, you need to add yap
to go.mod
:
require github.com/goplus/yap v0.7.2 //gop:class
Then you can create a example_ytest.gox
file to test your HTTP server:
host "https://example.com", "http://localhost:8080"
testauth := oauth2("...")
run "urlWithVar", => {
id := "123"
get "https://example.com/p/${id}"
ret
echo "code:", resp.code
echo "body:", resp.body
}
run "matchWithVar", => {
code := Var(int)
id := "123"
get "https://example.com/p/${id}"
ret code
echo "code:", code
match code, 200
}
run "postWithAuth", => {
id := "123"
title := "title"
author := "author"
post "https://example.com/p/${id}"
auth testauth
json {
"title": title,
"author": author,
}
ret 200 # match resp.code, 200
echo "body:", resp.body
}
run "matchJsonObject", => {
title := Var(string)
author := Var(string)
id := "123"
get "https://example.com/p/${id}"
ret 200
json {
"title": title,
"author": author,
}
echo "title:", title
echo "author:", author
}
This classfile has the file suffix .spx
. It is the earliest classfile in the world.
Before using spx
, you need to add it to go.mod
by using go get
:
go get github.com/goplus/spx@latest
It's also a built-in classfile so you don't need append //gop:class
after require github.com/goplus/spx
.
Through this example you can learn how to listen events and do somethings.
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.