Skip to content

Latest commit

 

History

History
538 lines (389 loc) · 14.8 KB

classfile.md

File metadata and controls

538 lines (389 loc) · 14.8 KB

Go+ Classfiles

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 classfile

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.

classfile: Unit Test

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.

yap: Yet Another Go/Go+ HTTP Web Framework

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

Router and Parameters

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

Static files server demo in Go+ classfile (staticfile_yap.gox):

static "/foo", FS("public")
static "/"

run ":8080"

YAP Template

demo in Go+ classfile (blog_yap.gox, article_yap.html):

get "/p/:id", ctx => {
	ctx.yap "article", {
		"id": ctx.param("id"),
	}
}

run ":8080"

yaptest: HTTP Test Framework

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
}

spx: A Go+ 2D Game Engine for STEM education

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.

tutorial/01-Weather

Screen Shot1 Screen Shot2

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.

tutorial/02-Dragon

Screen Shot1

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
    }
  ]
}

tutorial/03-Clone

Screen Shot1

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
	}
}

tutorial/04-Bullet

Screen Shot1

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.