Aus Oxoscript wird NanoPy - mehr Infos

Einführung

Von Python übernommen wurde vor allem die Einrückung und die grundlegenden Sprachkonstrukte, wie if, while, for, def. NanoPy enthält viele Vereinfachungen, sog. “syntactic sugar”, mit dem sie trotz der konzeptionellen Unterschiede wie Python-Code wirkt. Beispielsweise müsste in NanoPy jede Variable mit einem Typ initialisiert werden, bevor man sie brauchen kann. Dank der Erweiterungen bestimmt die Erstzuweisung eines Variablenwertes deren Typ. Man muss diesen nur angeben, wenn dieser nicht automatisch bestimmt werden kann. Dadurch ist die Anweisung in vielen Fällen identisch mit Python-Code.

a = 10
b = 3.145
c = true
d = [1,2,3,4]

Wenn der Typ nicht bestimmt werden kann, wird dieser mit einem Doppelpunkt angefügt:

a:long
b:float[5]

Diese Form gibt es auch in Python, ist dort jedoch optional, da die Variablen keinen fixen Typ haben. Dies macht Python flexibler, hat jedoch Nachteile, unter anderem Speicherverwaltungsproblematiken, die mit zusätzlichen Konzepten gelöst werden müssen (Carbage Collection, etc.). In NanoPy ist die Speicherverwaltung fix. Wir reduzieren dadurch den Speicherverbrauch erheblich und verhindern zudem Speicherfragmentierungen.

NanoPy kennt folgende Typen: byte, int, long, float und bool. Die Sprache kennt ebenfalls den Klassenbegriff, mit dem abstrakte Datentypen erzeugt werden können. Ein abstrakter Datentyp ist ein Variablenklasse, die aus mehreren elementaren Typen zusammengesetzt ist:

class Rectangle:
  left:int
  top:int
  width:int
  height:int

Beim Erzeugen von Variablen kann jetzt auch die neue Klasse verwendet werden.

a:Rectangle

Neben einzelnen Objekten lassen sich auch Listen von Objekten erzeugen, indem man die gewünschte Anzahl in eckige Klammern schreibt:

liste:Rectangle[10]

Die einzelnen Elemente lassen sich dann über den Index in eckigen Klammern lesen/schreiben:

liste[1].left = 10
pixels = liste[1].width * liste[1].height

Es ist auch möglich, ein Variable einer Klasse direkt zu initialisieren. Das folgende Beispiel erzeugt ein Objekt der Klasse Rectangle und legt dabei gleich Werte für left und top fest.

c = Rectangle(left=10,top=50)

Die Initialisierung mehrere Werte eines Objektes kann auch auf Listen angewendet werden:

liste[0](left=10,top=50)

Es wichtig zu wissen, dass die Sprache keine dynamische Speicherverwaltung hat. Deshalb gibt es kein new, wie in anderen Sprachen, um ein Objekt zu erzeugen. Alle Variablen werden durch statische Deklaration erzeugt und bleiben erhalten, solange das Programm, bzw. die betreffende Funktion läuft. Dies führt zu einer sehr speicheroptimierten und schnellen Ausführungsgeschwindigkeit.

Wie in Python üblich, kann man in NanoPy auch Schleifen und Bedingungen deklarieren. Diese sehen sehr ähnlich aus. Die Bedingung mit if, else, bzw elif sieht sogar identisch aus:

if a>b:
  print("a is greater than b")
elif a==b:
  print("both numbers are equal")
else:
  print("b is greater than a")

Die Einrückung ist zentral und identisch gelöst, wie bei Python. Man sieht an dem Beispiel auch, dass der Doppelpunkt übernommen wurde, um kompatibel zu bleiben.

Wie sich in Tests gezeigt hat, bekunden vor allem Einsteiger:innen Mühe mit den syntaktischen Konzepten einer Programmiersprache, wobei Python hier schon deutlich einfacher ist, als beispielsweise Java oder C/C++. Es ginge jedoch auch noch einfacher, wenn man an heute leider vergessen gegangene pädagogischen Programmiersprachen der Anfangszeit denkt. In Basic („Beginner’s All-purpose Symbolic Instruction Code) beispielweise braucht es keinen Doppelpunkt. Bei Funktionsaufrufen ohne Rückgabewert sind die Klammern auch unnötig und könnten wohl weggelassen werden.

Diese Form der Eingabe ist in NanoPy auch möglich und kann so auch verwendet werden:

if a>b
  print "a is greater than b"
elif a==b
  print "both numbers are equal"
else
  print "b is greater than a"

Der Doppelpunkt kann auch bei class, def, for und while weggelassen werden. Man kann also sowohl Python-Kompatibel schreiben oder die unnötigen Konstrukte auch weglassen. Beides ist gültig und möglich.

Es gibt zwei verschieden Schleifentypen, die man verwenden kann, die for- und die while-Schleife.

Bei der for-Schleife ist die Syntax leicht anders als in Python:

for i in 10:
  print(i)

Diese Anweisung zählt von 0 bis 9, d.h. wird zehnmal durchlaufen. Anstelle einer Zahl kann auch eine Variable angegeben werden. Die Zahl bestimmt die Anzahl Durchgänge, die immer bei 0 beginnt.

Die folgende Anweisung zeigt, wie man einen bestimmten Wertebereich durchlaufen kann:

for i in [5..10]:
  print(i)

Die Zahlen werden eingeschlossen, d.h. es wird von 5 bis 10 (inkl.) gezählt. Man kann auch Runterzählen, wenn die zweite Zahl kleiner als die erste ist.

Auch Listen lassen sich mit Schleifen iterieren. Hier ist zu beachten, dass das Listenelement jeweils eine Kopie des Originaleintrags ist. Wenn man einen Listeneintrag ändern möchte, muss man die Einträge über den Index adressieren:

list = [1,2,3,4,5,6,7,8,9,10]
for i in list
  print i

for i in sizeof(list)
  list[i] = list[i] + 1

for i in list
  print i

Die entsprechende Anweisung in Python ist flexibler, jedoch für Anfänger auch schwerer zu verstehen und daher in NanoPy nicht möglich:

for i in range(0,10):
  print(i)

Auch diese Anweisungen können ohne Doppelpunkt und Klammern geschrieben werden:

for i in 10
  print i
for i in [5..10]
  print i

Einzelanweisungen können auch auf einer Zeile ausgeführt werden:

for i in 10 print i
for i in [5..10] print i

Die Klammer bei Funktionsaufrufen kann weggelassen werden, wobei sofern die Klammern danach nicht in Funktionen verwendet werden. Das folgende Beispiel führt zu einem Programmfehler

print (1+2)*3

In diesem Fall muss der gesamte Ausdruck eingeklammert werden:

print ((1+2)*3)

Das folgende Beispiel einer while-Schleife zeigt, wie sich etwas so lange wiederholen lässt, solange eine bestimmte Bedingung erfüllt ist:

i = 0
while i < 10
  print i
  i=i-1

Zeichenketten / Strings werden in NanoPy als Byte-Arrays abgebildet, wobei pro Zeichen ein Byte verwendet wird. Unicode ist momentan nicht möglich, da dies zu viel Speicherplatz verwendet. Wenn einer Variablen ein String zugewiesen wird, wird automatisch ein Byte-Array erzeugt (:byte[114]). Die maximale Länge ist systemabhängig und kann mit sizeof(liste) abgefragt werden. Wenn man eine andere Grösse möchte, kann man diese explizit deklarieren.

a = "Das ist ein Test"
print sizeof(a)
b:byte[40]
b = "Hallo"

Wenn einem Bytearray ein numerischer Wert zugewiesen wird, wir dieser automatisch in einen String umgewandelt:

i = 10
a = "Mein Glückszahl ist " + i
print a

Funktionen werden wie in Python mit def deklariert und können mit oder ohne Parameter deklariert werden. Wenn bei den Parametern kein Typ angegeben wird, wird der Typ int angenommen.

def helloWorld():
  print("Hello world")

def summe(a,b)->int:
  return a+b

def floatsumme(a:float,b:float)->float:
  return a+b

Die Parameter- und Rückgabetypen sind von Python übernommen. Dort sind sie fakultativ, hier sind sie, mit Ausnahme von int-Parametern, zwingend.

Parameterlose Funktionen ohne Rückgabewerte können in einer verkürzten Form geschrieben werden. Hier die gleichen Beispiele in der vereinfachten Form:

def helloWorld
  print "Hello world"

def summe(a,b)->int
  return a+b

def floatsumme(a:float,b:float)->float
  return a+b

Die verkürzte Form ohne Klammern ist nur zulässig, wenn keine Parameter deklariert werden müssen.

Klassen können auch verschachtelt werden. Zudem ist es auch möglich, Klassen mit Funktionen zu ergänzen.

class Shape:
  position:vector
  radius:int

  def init
    position = vector(x=120,y=120)
    radius = 100

  def draw
    clear
    drawCircle position.x,position.y,radius
    update

myShape:Shape
myShape.init
myShape.draw

Die Sprache kennt zusätzlich zu Python auch Konstanten. Dies sind wie Variablen Platzhalter für Werte, jedoch kann deren Wert zu Laufzeit nicht verändert werden. Konstanten lassen sich auch berechnen und man kann diese überall dort einsetzen, wo man Variablen verwenden kann. Zusätzlich lassen sich diese auch bei statischen Deklarationen, z.B. Listen verwenden. Zu Deklaration verwenden wir die Anweisung const:

const WIDTH = 10
const HEIGHT = 20
const AREA = WIDTH * HEIGHT

elements:vector[AREA*2]

Bei der Übergabe von Werten an Funktionen ist Vorsicht geboten. Die Sprache kopiert alle Werte, d.h. wenn eine Liste oder ein Objekt einer Funktion übergeben wird, wird eine Kopie davon erzeugt. Diese Handhabung heisst by value. Andere Sprachen erlauben es, Referenzen auf Variablen zu übergeben (by reference). Dies ist bei NanoPy nicht möglich, kann jedoch gut umschifft werden.

Grosse Objekte oder Listen, die allgemeingültig sind, werden einfach global deklariert. Diese Variablen sind in Funktionen immer sichtbar und müsse nicht als Parameter übergeben werden:

list:byte[200]

def initializeList
  for i in sizeof(list)
    list[i] = 0

initializeList

Die spart wertvollen Speicher und erhöht die Ausführungsgeschwindigkeit des Codes, da keine grossen Objekte kopiert werden müssen.

Klassenvariablen, d.h. Variablen, die in einer Klassendeklaration definiert sind, sind objektglobal, d.h. alle Funktionen des Objekts könne auf diese Variablen auch zugreifen.

class Rectangle
  pos:vector
  size:int

  def init
    pos.x = 10
    pos.y = 20
    size = 30

r:Rectangle
r.init

In der Kommunikation werden häufig Daten zwischen Prozessen, Coprozessoren oder anderen Systemen ausgetauscht. NanoPy stellt mit einen Konvertierungsbefehl zur Verfügung, mit dem Objektdaten in Bytearray umgewandelt werden können. Dafür kann die Anweisung « verwendet werden:

Die folgende Anweisung wandelt das Objekt v in ein Byte-Array buf um. Da vector zwei float-Variablen zu je 4 bytes enthält, is buf 8 bytes lang.

v = vector(x=10,y=20)
buf << v

Umgekehrt lässt sich aus einem Buffer auch wieder in eine Struktur zurückwandeln:

buf = [0,0,32,65,0,0,160,65]
v:vector
v << buf
print v.toString()

Wenn Listen Funktionen übergeben werden müssen, muss die Dimensionierung nicht explizit angegeben werden.

def sumfloats(list:float[])->float:
  result:float

  for v in list:
    result = result + v

  return result

floats:float[5] = [0.1,0.2,0.3,0.4,0.5]

print(sumfloats(floats))

Vereinfachte Form (keine Doppelpunkte, einzeilig, nicht zwingende Klammern wurden weggelassen):

def sumfloats(list:float[])->float
  result:float
  for v in list result = result + v
  return result

floats:float[5] = [0.1,0.2,0.3,0.4,0.5]

print sumfloats(floats)

Die Dimension kann auch beim Funktionsrückgaben offen gelassen werden:

def test(t)->byte[]
  a:byte[20]
  a="byte[20]"

  b:byte[10]
  b="byte[10]"

  if t%2 == 0
    return a
  else
    return b

for i in 10
  print test(i)

Die Sprache NanoPy wird für mehrere Systeme verwendet, deshalb wird in diesem Artikel nicht auf konkrete plattformspezifische Funktionen eingegangen. Ziel ist es, mit der Sprache sämtliche Produkte der Oxon “programmierbar” zu machen, was in den nächsten Monaten schrittweise umgesetzt wird. Aktuell ist NanoPy ausschliesslich auf den pädagogischen Lerncomputern der Oxocard-Mini-Serie nutzbar. Die Oxocard Blockly-Karten, welche vor allem mit Blockly programmiert werden, enthalten eine vereinfachte Vorab-Version von NanoPy, welche nicht mehr weiterentwickelt wird und nicht kompatibel mit der aktuellen Version ist.

Auf den Oxocard Minis, aber auch den zukünftigen Geräten, stellen wir in NanoPy ein ereignisgesteuertes Programmiermodell zur Verfügung. Auf den Oxocards kann man dieses bereits heute nutzen und damit experimentieren. NanoPy stellt hierzu Ereignisprozeduren zur Verfügung, die vom Betriebssystem aufgerufen werden. Auf den Oxocards sind das die beiden Prozeduren onDraw und onClick. onDraw wird aufgerufen, wenn der Bildschirm neu gezeichnet werden soll. Dies geschieht, je nach Auslastung des Systems, bis zu 50 Mal pro Sekunde. onClick wird aufgerufen, wenn der Benutzer einen Button klickt. Auf den IoT-Geräten werden wir andere Ereignisprozeduren zur Verfügung stellen, mit denen die Geräte auf einfache Weise stromsparend betrieben werden können.

Dieses Beispiel schaltet den Bildschirm ein/aus, wenn man den linken/rechten Button drückt:

turnOn = false

def onDraw
  if turnOn
    background 255,255,255
  else
    background 0,0,0
  update

def onClick
  b = getButtons()
  if b.left
    turnOn = false
  elif b.right
    turnOn = true

Zusammenfassung