C# and Jupyter/BoSSSpad introduction

Whats new?

Jupyter Notebook is a scripting environment which, among other languages, also supports C#. BoSSSpad is a C#-library which can be used to access BoSSS through Jupyter. While it is not necessary to be a master-class programmer for the tutorials which come up, it is required to have some basic knowledge about the C# syntax.

Here, the following topics are covered:

Declaration of primitive variables

Normally, a program/application written in C# consists of one or more text files (source code, ending with .cs) which are read by a compiler and converted into an executable (ending with .exe).

In Jupyter, instead, one can enter single statements or snippets of C# code and execute them on the fly. (These snippets are not complete programs on their own, and they would not compile in the traditional way.)

Note:

  1. This tutorial can be found in the source code repository as as CsharpAndBoSSSpad.ipynb. One can directly load this into Jupyter to interactively work with the following code examples.
  2. First, we need to load the BoSSSpad.dll library in order to load BoSSS-specific functions into Jupyter. In the following line, the reference to BoSSSpad.dll is required. You must either set #r "BoSSSpad.dll" to something which is appropirate for your computer (e.g. C:\Program Files (x86)\FDY\BoSSS\bin\Release\net5.0\BoSSSpad.dll if you installed the binary distribution), or, if you are working with the source code, you must compile BoSSSpad and put it side-by-side to this worksheet file (from the original location in the repository, you can use the scripts getbossspad.sh, resp. getbossspad.bat).

Any data like single numbers, vectors, strings or composite data objects (like the numerical grid) is stored resp. referenced via a \emph{variable}. A variable declaration consists of a type and a name, and reads e.g. as:

The above statement created a variable named i of type int (for integer numbers). The most commonly used number types in BoSSS are int for integers and double for real numbers.

A full list of C#-build-in variable types for numbers can be found online (see integral-types-table and floating-point-types-table)

Next, we assign a value to the new variable:

If we just enter the variable name without semicolon, BoSSSpad, resp. Jupyter prompts the variable in the result window (if the variable can anyhow be presented as text):

Declaration and assignment can also be put into a single line:

Next, we test assign a new value to the already-defined variable i:

Note on the snippet above:

While assigning double to int requires a cast operation, the other way works automatically (this is called an implicit cast), since very int can be represented as double (but not vice-versa):

C# defines the standard operations on numbers found in most programming languages:

Which also supports some (but not all) operators:

Arrays

For every type, one can define arrays; e.g. an array of integers is defined and initialized as

The line above creates an array with 5 entries. Initially, all entries are set to the default value 0:

One can access and manipulate individual entires by index-brackets; Note that C# indices (usually) start at zero:

It is also possible to initialize an array with non-default values:

Multi-dimensional arrays

C# also defines arrays with more than one index, e.g.:

In BoSSS, there is a separate type for multidimensional arrays of real numbers.

In contrast to the multidimensional arrays build into C#, their BoSSS counterparts offer more operations, e.g. reshaping the array or tensor multiplications.

An array with three dimensions can be initialized as:

However, a comprehensive introduction of the MultidimensionalArray type is beyond the scope of this section. We may refer the reader to the API reference

Classes

So far, we used only primitive (build-in) datatypes. The core idea of object orientation is to define composite data structures (called classes) upon the primitive types and other classes.

Furthermore, classes provide the concept of encapsulation: any member variable that should be manipulated from code outside of the class must be decorated with the public keyword. Otherwise, the member is private and can only be manipulated form inside the class. (This is demonstrated in below)

One may think of classes as blueprints, which are used to create objects. Whenever the new operator is applied to a class, a new object is created in computer memory. This is called instantiation.

Objects are accessed via variables, just in the same way as arrays.

We declare a variable of type MyVector and intialize it with an instance of this class:

Then, we can manipulate its variables (also called members):

Each object owns its own space in memory; so when we instantiate a second object of this type, the members of vec stay the same:

We see, both objects have their independent variables:

Member Functions (Methods)

The next important idea of object orientation is to bundle data structures with the code which manipulates this data. In imperative programming languages (e.g. C, FORTRAN, Matlab) these code is organized in so-called functions, aka. subroutines.

In C# such functions are put into the class and they can access all variables in the class, including private ones. These functions are often called methods or member functions.

A function consists of the following parts:

Next, we demonstrate how to execute member functions of our new class:

Properties

Properties are special methods, which look like functions. Most of the time, they are used to execute additional code before setting a variable.

Again, we instantiate the class:

Access to the member variable through methods would work as

while the property-notation is more compact, but has the same functionality (syntactic sugar):

Static functions

So far, we always had to instantiate a class to execute its methods. Now think e.g. about mathematical functions, like the sinus. In languages like C or FORTRAN, one would just evaluate them as sin(x).

The designers of C# could have allowed functions like in C, but decided against it because it would compromise the object-oriented syntax. On the other hand, creating an object before one is able to evaluate a sinus function also seems over-complicated. The compromise are static functions:

Then, static functions can be involved without instantiating an object:

Class libraries and Namespaces

Like most programming languages, C# also ships with an extensive programming library (class libraries), e.g. to read and save files, mathematical functions, graphical output;

The full list of standard libraries can be found at here.aspx).

These libraries are collections of classes; they are organized in namespaces.

Any class within a loaded namespace can be accessed directly; e.g. the classes Console and Math, which are part of the System namespace:

(By the way: the Console class, resp. the Console.WriteLine method is the preffered way to write text output (for non-windowed apps without graphical user interface).

In BoSSSpad, it is very handy if one wants to write more information than just the last object in a snippet.

This is the same as calling the methods by their full namespace path:

If a namespace is not loaded, its classes must be accessed via their full path, e.g.

If we load the namespace by the using keyword, we can access the BitArray class directly:

Note that, in order to work with a class library, it may not be enough to just add a using directive. All libraries are contained in dll files, e.g. System.dll (standard C# library) or BoSSS.Foundation.dll (part of BoSSS).

These files must be loaded before any class from the library can be used. (Namespaces are only for the internal organization of the library.) Initially, BoSSSpad loads a set of libraries which are useful for working with BoSSS.

For most use-cases, this set may be sufficient; however, not that by the Mono.CSharp.InteractiveBase.LoadAssembly(string) method, it is possible to load further assemblies.

Control Flow

Regarding the standard syntax of the language for implementing a function, C# designers tried to stay close to popular existing languages like C, C++ and Java.

In this tutorial, we briefly discuss two control structures, branches and loops. At first, the if-branch;

We use a random number generator create random input data for our example. Therefore, the following snippet runs differently each time it is executed:

Next, the for-loop. It has three `Arguments':

Another kind of loops is the foreach statement. It executes the loop body for each element of a list or array:

Functions, Methods and Delegates

In its heart, C# is an object-oriented programming language, and methods are implemented in a imperative style. However, the designers also included concepts from the functional programming model.

A key idea of functional programming is that functions/methods can be treated as variables; such variables are called delegates.

In order to demonstrate the use of delegates, we create a class with some static methods:

Now, we want to create variables/delegates which point to the functions/methods defined above. As for any variable declaration, we need to specify type and name. For functions which have a return type, it works like this: Note: the last type in the '< >' - List:

Then, the method can be called via the delegate: Note: the 'f'-suffix is for single precision types; this also shows round-of errors of floating point arithmetics

Functions without a return type (void) are also called actions:

By the keyword delagate, C# designers finally (in the C# version 3.0) reversed their original decision that functions are only allowed within classes by the introduction of anonymous functions:

Now, the variable delPow2 does not anymore point to the method DelDemo.Pow2, but to the newly defined, anonymous function:

To make notation more compact, a function-like syntax was introduced:

Since delegates can be treated as normal variables, they can also be used as arguments to other functions; here is a more advanced example, an action (no return value) which takes as inputs another action, a function and an integer value:

Inheritance

As already discussed, classes are blueprints for objects: once a class is implemented, it can be used to create an infinite number of objects. Of course, only until the computers memory is full.

The model of a single blueprint is, however, sometimes a bit inflexible. Engineers knew this for a long time: in a group of products, you may have two products that have a lot in common, but also significant differences regarding some functionality.

Object oriented programming languages provide the concept of inheritance: Common functionality is implemented in a common base class.

First we define some class; functions or properties which are decorated as virtual

Now, we derive two classes from BaseClass:

As usual, we can instantiate and use BaseClass:

Of course, we can do the same with DerivA; since it is derived from BaseClass, an object of type DerivA can be stored in (resp. referenced from) a variable of type BaseClass.

In short, any DerivA-object is autamtically also a BaseClass-object, due to inhertance (but not the other way).

We can also store DerivB-objects in variable b:

Note: implicitly, all classes in C# are derived vom the class ystem.Object, and has all the methods of this class.

Abstract Classes

In lots of situations, there is no reasonable base-implementation for a method.

Note:

Interfaces

When we finish the concept of abstract classes, we come to classes that only have abstract members. These are called interfaces and C# has a special keyword for them:

Note:

there is a widly used convention to start interface names with capital letter `I'. This is only a convention, not a rule.

An important difference between classes and interfaces is, that any class can have only one base-class, but it can implement an unlimited number of interfaces:

Of course, we can instantiate DerivD and call any method or property:

But we can also store the object in references with the interface type:

The execution model of BoSSSpad

The term which describes the execution model of BoSSSpad best is called read-eval-print-loop (REPL): commands are read from the input window, evaluated by the interpreter, the result is printed to the screen and this is repeated in an infinite loop (until the app is terminated).

Each time a command is executed, the internal state of the interpreter is changed. E.g. new variables or classes are defined. If a variable or
class name is already used, it will be overwritten. The special command restart resets the interpreter to an initial state.

On top of that, BoSSSpad puts a document model: the user can navigate backward and re-execute any command; this may or may not change the behavior of the entire script. A simple example is:

We are defining an int variable:

Next, we overwrite the name i with an new variable

And execute some code which depends on i, e.g.:

Obviously, if we would navigate back to the int-definition-statement, re-execute it, but skip the execution of the double-definition statement, the following statement will behave differently:

As we see,...

This behavior might seem confusing (and very often, it is). However, it is desired because it speeds up the workflow, because it allows to alter or correct a script, without (time-consuming) re-evaluation of the entire script. As a rule of thumb, take this:

Further reading

Obviously, this minimal tutorial does not cover the entire C# language. It should, however, demonstrate everything which is needed to understand about 90\% of the tutorials in the BoSSS handbook.

Finally: there are lots of tutorials on C# online. Some of them are excellent, some are not so good. And: Google is your friend (at least in this respect)...