np

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.

try it out ⇥

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
Unlike a function, a block is always executed where it's found. It is very useful to
  • 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.