np Tutorial
Getting Started
Enter and execute code right from the convenience of your own browser. This tutorial is intended for people who already know one or more other programming languages. Click on the blue arrows to run any piece of example code.
Hello World
Here's a very simple np program:
⇥print('Hello World!')
will print out:
Hello World!
Now, that's not very interesting. Let's make use of variables:
⇥new greeting = 'Hello from a variable' print(greeting)
Variables are declared with the keyword new
, at which
time they can optionally get a value. There are different types of
values in np, but these are the basic ones:
new imAString = 'Striiiing' new imANumber = 42 new imABoolean = true new imAList = (: 1 2 3) new imANothing = nil print(imAString imANumber imABoolean imANothing) print(imAList)
Let's have a closer look at the list type.
⇥new places = (: 'earth' 'solar system' 'galaxy') places:each { thing | print('Hello' thing) }
This already contains some juicy stuff: the places
variable is filled with
a list of places, and finally each of the list entries is printed out in a loop.
FizzBuzz
Say you're faced with the following problem: count from 1 to 100, and everytime the number is a multiple of 3 say "Fizz", if the number is a multiple of 5 say "Buzz", if it's both say "FizzBuzz", otherwise just say the number. This is where drinking games tend to go wrong. Luckily, with np you can program a FizzBuzzBuddy to help you with this.
⇥for i = 1, 100 { new fbz if (i % 3 == 0) fbz = 'Fizz' if (i % 5 == 0) fbz <<= 'Buzz' if (!fbz) fbz = i print(fbz) }
for Loops
The first interesting thing that happens here is the for loop, it
counts from a number to another number, storing the result in a variable each time.
We called this variable i
in the example. The generic for of the for loop
looks like this:
for loopVariable = fromNumber, toNumber doStuff()
if Statements
As in other languages, if
statements in np handle conditional code execution.
Technically, you would not need the parens around the condition itself, however, it makes
the code more readable. The generic form looks like this:
if (condition1) thenDoSomeStuff() elseif (condition2) doOtherStuff() else elseWhatever()
If there is just one statement after the condition, you can omit the curly braces for terseness.
This example shows one else
condition, but an if
statement can
in fact have as many more as may be required.
Operators
The FizzBuzz example shows off some of np's operators as well. First, there are the arithmetic
operators which perform math operations: +
(add), -
(subtract),
/
(divide), *
(multiply), %
(modulo),
and ^
(to-the-power-of).
Next are the logical operators: and
(logical and), or
(logical or),
not
(logical negation) and its short form !
which can optionally be
used as an unary negation as shown in the example.
In np, only the values false
and nil
are considered false-y. All other
values will evaluate to true
in logical statements.
To chain two strings together, np employs <<
(the concatenation operator).
Lastly, there are the assignment operators, with =
being the most basic form.
Combined with some of the other operators, compound assignments are possible where
the right-hand side of an assignment is an operation relative to the left-hand side.
In the FizzBuzz example, we're using the concatenation compound assignment to
append a value to an existing string.
The following compound assignments are supported: +=
(add to), -=
(subtract from),
*=
(multiply by), /=
(divide by), <<=
(append to).
Functions
Functions are pieces of code you can call. They may return one or more results, and they may take one or more parameters in doing so. Consider for your amusement, the FizzBuzzer:
⇥new fizzBuzzer = { i | new fbResult if (i % 3 == 0) fbResult = 'Fizz' if (i % 5 == 0) fbResult <<= 'Buzz' if (!fbResult) fbResult = i => fbResult, i } print(fizzBuzzer(15))
What we've done here is package the FizzBuzz logic from the previous example into a function called fizzBuzzer(). When we call fizzBuzzer() with a number, it will return two things: the fizz buzz value and the original number.
Say you wanted to store the function result in a variable for later:
⇥new myFavoriteResult = fizzBuzzer(150)
This stores only the first value of the result. You can store both, like this:
⇥new myFavoriteResult, original = fizzBuzzer(150)
np has some handy features that deal with multiple return values and function parameters. For example, let's say you only want the second result that comes out of the fizzBuzzer():
⇥print(select(2, fizzBuzzer(150)))
Or you can condense all results into a list:
⇥-- condense all return values into a list print( list.condense(fizzBuzzer(150)) )
Likewise, you can expand a list into several function parameters:
⇥-- there new myPreciousss = list.condense(fizzBuzzer(150)) -- and back again new backAgain = { fz org n | print('fizz:' fz 'original:' org 'params' nr) } backAgain(list.expand(myPreciousss))
But whatever happened to "n"? Well, list.expand()
works only on list entries that don't have a name. But more about list
features later!
Scope
Every variable you use must be declared beforehand. An error is triggered if you attempt to assign a value to a non-existant variable.
⇥bla = 1
When you declare a variable, you can assign a value to it immediately, or you can leave it empty for future use:
⇥new someNumber = 2 new downForWhatever print(someNumber downForWhatever)
Functions have access to variables declared in the parent context, thereby creating closures.
⇥new outerVar = 100 new myFunc = {| print(outerVar) } myFunc()
In np, every function is in its own little world, and local variables can overshadow outer ones.
⇥new vegetable = 'Potato' print(vegetable) new f = {| new vegetable = 'Bacon' print(vegetable 'is my vegetable') } f() print(vegetable)
However, if no local variable by a certain name can be found, np will try and look if it's available in the outer context:
⇥new vegetable = 'Potato' new f = {| print(vegetable 'is my vegetable') } f()
Block Scope
Let's give the block a formal introduction. A block is a piece of
code enclosed by curly braces: { ... }
. It's like a function,
but can't do a lot of the things that functions can. Blocks cannot:
- have parameters
- return values
- be passed around in variables
- tie multiple statements together
- limit the scope of variables
if true { new hubris = 'superiority' print("Look at me, I'm a block.") print("I even got my own variable.") print("It's a sign of my " hubris) }
Scope Shenanigans
Using an outer variable in a function can be very useful. Local variables and hierarchical contexts are not limited to functions, they can be used in blocks, too. For example, consider this function that wants to keep a secret:
⇥new guessMe { new secretNumber = 42 guessMe = { guess | if guess == secretNumber => 'Darn, you guessed it' else => 'Nope, fail haha' } } print( guessMe(1) ) print( guessMe(13) ) print( guessMe(42) )
This example shows how easy it is to give functions and objects an internal
state that can't be accessed from the outside, and all without polluting the
variable space. Since secretNumber
is bound to the block it
was created in, it's not visible to anything outside that block. Since
guessMe()
was created within the block, it's the only witness
of secretNumber
's existence once the block terminates.
Events
One of the ways to make lists do things is to define events they can respond to. There are some standard events that allow you to modify how a list responds to common incidents - but you can also specify your own. Event handlers in np are a lot like class methods in other languages, except there are no classes involved. Events are simply a way of assigning behavior to objects.
As an example, let's introduce Bjørn the Bugbear:
⇥new bjorn = (: name = 'Bjørn' ) print( 'My name is' bjorn.name )
Bjørn has a name, but he lacks the ability to introduce himself. Let's change that by giving him the ability to speak:
⇥new BurpTalker = (: say = { me text | print( me.name << ' says: ' << text << ' BURP!' ) } ) new bjorn = BurpTalker:create(: name = 'Bjørn' ) bjorn:say('Hi!')
By using the create
event, we have made a new object
containing the name Bjørn, and assigned the BurpTalker behavior
to it.
Since event lists such as the BurpTalker in the example are just normal lists containing functions, np allows makes it easy to mix event lists together in order to combine and manipulate capabilities. You can also change the event list of an already existing object. Let's see both in action:
⇥new BurpTalker = (: say = { me text | print( me.name << ' says: ' << text << ' BURP!' ) } ) -- make a Bjørn new bjorn = (: name = 'Bjørn' ) -- assign normal list behavior and burp behavior bjorn:bind( list << BurpTalker ) -- take him for a spin bjorn:say('Hi!')
In the example before, Bjørn was just a BurpTalker, lacking the normal
capabilities of a list object. In this example, we combine the
standard list
behavior with the burping to
achieve a more fully-featured Bugbear.
Summary
So in this tutorial we covered a lot and illustrated it with contrived examples! You learned about basic functions, how to use functions and other syntax, how scope works, that lists are the basic object type in np, and how objects can respond to events.