Basic Syntax of Nit
The syntax of Nit follows the Pascal tradition and is inspired by various script languages (especially Ruby). Its main objective is readability.
Indentation is not meaningful in Nit; blocks usually starts with a
specific keyword and finish with end. Newlines are only meaningful at
the end of declarations, at the end of statements, and after some
specific keywords. The philosophy is that the newline is ignored if
something (a statement, a declaration, or whatever) obviously needs more
input; while the newline terminates lines that seems completed. See the
complete Nit grammar for more details.
# a first complete statement that outputs "2" print 1 + 1 # the second statement is not yet finished print 2 + # the end of the second statement, outputs "4" 2
Nit aims to achieve some uniformity in its usage of the common
punctuation: equal (=) is for assignment, double equal (==) is for
equality test, column (:) is for type declaration, dot (.) is for
polymorphism, comma (,) separates elements, and quad (::) is for
explicit designation.
Identifiers
Identifiers of modules, variables, methods, attributes and
labels must begin with a lowercase letter and can be followed by
letters, digits, or underscores. However, the usage of uppercase letters
(and camelcase) is discouraged and the usage of underscore to separate
words in identifiers is preferred: some_identifier.
Identifiers of classes and types must begin with an uppercase letter and
can be followed by letters, digits, or underscores. However
the usage of camelcase is preferred for class identifiers while formal types should be written in all uppercase: SomeClass and SOME_VIRTUAL_TYPE.
Style
While Nit does not enforce any kind of source code formatting, the following is encouraged:
indentation uses the tabulation character and is displayed as 8 spaces;
lines are less than 80 characters long;
binary operators have spaces around them:
4 + 5,x = 5;columns (
:) and commas (,) have a space after them but not before:var x: X,[1, 2, 3];parenthesis and brackets do not need spaces around them;
superfluous parenthesis should be avoided;
the
doof methods and the singledois on its own line and not indented;the other
doare not on a newline.
Comments and Documentation
As in many script languages, comments begin with a sharp (#)
and run up to the end of the line. Currently, there is no
multiline-comments.
A comment block right before any definition of module, class, or property, is considered as its documentation and will be displayed as such by the autodoc. At this point, documentation is displayed verbatim (no special formatting or meta-information).
# doc. of foo module foo # doc. of Bar class Bar # doc. of baz fun baz do end end
Basic Types
Object
Nit is a full object language. Every value is an instance of a class. Even the basic types described in this section.
Object is the root of the class hierarchy. All other classes,
including the basic ones, are a specialization of Object.
Classes, methods and operators presented in this section are defined in the standard Nit library that is implicitly imported by every module. Many other classes and methods are also defined in the standard library. Please look at the specific standard library documentation for all details.
Int and Float
1, -1 are Int literals, and 1.0, -0.1 are Float
literals. Standard arithmetic operators are available with a common
precedence rules: *, /, and % (modulo) ; then + and -. Some
operators can be composed with the assignment (=).
var i = 5 i += 2 print i # outputs 7
Conversion from Int to Float and Float to Int must be done with
the to_f and to_i methods.
String
Literal strings are enclosed within quotes (").
To insert a value
inside a literal string, include the values inside braces ({}).
Braces have to be escaped.
+ is the concatenation operator, but is less efficient than the brace form.
var j = 5 print "j{j}; j1{j+1}" # outputs "j5; j16"
Common escaping sequences are available (\", \n, \t, etc.) plus the escaped brace \{.
print "hel"lonwo{rld" # outputs `hel"lo` on a first line # and `wo{rld` on a second line
Multi-line strings are enclosed with triple quotes (""").
Values are inserted with triple braces ({{{value}}}).
The multi-line form thus allows verbatim new-lines, quotes and braces
print """some text with line breaks and characters like " and { but {{{ 1+2 }}} is rendered as 3 """
All objects have a to_s method that converts the object to a String.
print is a top-level method that takes any number of arguments and
prints them to the standard output. print always adds a newline to the output, another
top-level method, printn, does not add a newline.
var x: String x = 5.to_s # -> the String "5" print x # outputs "5"
Bool
true and false are the only two Bool values. Standard
Boolean operators are available with the standard precedence rule:
not; then and; then or.
Common comparison operators are available: == and != on all objects;
<, >, <=, >= and <=> on Comparable objects (which include
Int, String and others).
==,<,>,<=,>=and<=>are standard Nit operators thus are redefinable.and,orandnotare not standard Nit operators: they are not redefinable, also they are lazy and have adaptive typing flow effects.==is not for reference equality but for value equality (likeequalsin Java). There is a special reference equality operator,is, but it cannot be redefined and its usage is not recommended. Note that while==is redefinable, it has a special adaptive typing flow effect when used withnull.!=is not a standard Nit operator. In factx != yis syntactically equivalent tonot x == y.
Array
Array is a generic class, thus Array[Int] denotes an array
of integers and Array[Array[Bool]] denotes an array of array of
Booleans. Literal arrays can be declared with the bracket notation
([]). Empty arrays can also be instantiated with the new keyword and
elements added with the add method. Elements can be retrieved or
stored with the bracket operator.
var a = [1, 2, 3, 4] # A literal array of integers print a.join(":") # outputs "1:2:3:4" var b = new Array[Int] # A new empty array of integers b.add(10) b.add_all(a) b.add(20) print b[0] # outputs "10" print b.length # outputs "6" b[1] = 30 print b.join(", ") # outputs "10, 30, 2, 3, 4, 20"
Note that the type of literal arrays is deduced using the static type combination rule.
Range
Range is also a generic class but accepts only Discrete
types (Int is discrete). There are two kinds of literal ranges, the
open one [1..5[ that excludes the last element, and the closed one
[1..5] that includes it.
print([1..5[.join(":")) # outputs "1:2:3:4" print([1..5].join(":")) # outputs "1:2:3:4:5"
Ranges are mainly used in for loops.
HashMap
HashMap is a generic class that associates keys with values.
There is no literal hashmap, therefore the new keyword is used to
create an empty HashMap and the bracket operators are used to store
and retrieve values.
var h = new HashMap[String, Int] # h associates strings to integers h["six"] = 6 print h["six"] + 1 # outputs "7"
Control Structures
Traditional procedural control structures exist in Nit. They also often exist in two versions: a one-liner and a block version.
Control Flow
Control structures dictate the control flow of the program. Nit heavily refers to the control flow in its specification:
No unreachable statement;
No usage of undefined variables;
No function without a
returnwith a value;Adaptive typing.
Some structures alter the control flow, but are not described in this
section: and, or, not, or else and return.
Note that the control flow is determined only from the position, the order and the nesting of the control structures. The real value of the expressions used has no effect on the control flow analyses.
if true then return else return end print 1 # Compile error: unreachable statement
if true then return end print 1 # OK, but never executed
if
var exp = true # ... if exp then print 1 if exp then print 2 else print 2 if exp then print 1 print 2 end if exp then print 1 print 2 else if exp then print 10 print 20 else print 100 print 200 end
Note that the following example is invalid since the first line is
syntactically complete thus the newline terminate the whole if
structure; then an error is signaled since a statement cannot begin with
else.
if exp then print 1 # OK: complete 'if' structure else print 2 # Syntax error: unexpected 'else'
while
var x = 0 while x < 10 do x += 1 print x # outputs 10 while x < 20 do print x # outputs 10 11 ... 19 x += 1 end
for
for declares an automatic variable used to iterates on Collection (Array and Range are both Collection).
for i in [1..5] do print i # outputs 1 2 3 4 5 for i in [1, 4, 6] do print i # outputs 1 4 6 end
for can also be used with reversed ranges to iterate in reverse order.
Step can also be used to specify the size of each increment at the end of a for cycle.
for i in [9 .. 4].step(-1) do print i # outputs 9 8 7 6 5 4 for i in [9 .. 4[.step(-2) do print i # outputs 9 7 5
loop
Infinite loops are mainly used with breaks. They are useful to implement until loops or to simulate the exit when control of Ada.
loop print 1 if exp then break print 2 end
Note that loop is different from while true because the control flow does not consider the values of expressions.
do
Single do are used to create scoped variables or to be attached with labeled breaks.
do var j = 5 print j end # j is not defined here
break, continue and label
Unlabeled break exits the current for, while, loop, Unlabeled continue skips the current for, while, loop.
label can be used with break or continue to act on a specific control structure (not necessary the current one). The corresponding label must be defined after the end keyword of the designated control structure.
for i in [0..10[ do for j in [0..10[ do if i + j > 15 then break label outer_loop print "{i},{j}" # The 'break' breaks the 'for i' loop end end label outer_loop
label can also be used with break and single do structures.
do print 1 # printed if exp then break label block print 2 # not printed because exp is true end label block
abort
abort stops the program with a fatal error and prints a stack trace. Since there is currently no exception nor run-time-errors, abort is somewhat used to simulate them.
assert
assert verifies that a given Boolean expression is true, or else it aborts. An optional label can be precised, it will be displayed on the error message. An optional else can also be added and will be executed before the abort.
assert bla: exp else # `bla` is the label # `exp` is the expression to verify print "Fatal error in module blablabla." print "Please contact the customer service." end
Local Variables and Static Typing
var declares local variables. In fact, there is no global variable in Nit, so in this document variable always refers to a local variable. A variable is visible up to the end of the current
control structure. Two variables with the same name cannot coexist: no nesting nor masking.
Variables are bound to values. A variable cannot be used unless it has a value in all control flow paths (à la Java).
var exp = 10 # ... var a if exp > 0 then a = 5 else a = 7 end print a # OK
var b if exp > 0 then b = 6 end print b # Compile error: y is possibly not initialized
Adaptive Typing
Nit features adaptive typing, which means that the static type of a variable can change according to: the assignments of variables, the control flow, and some special operators (and, or,
or else, ==, !=, and isa).
var c # a variable c = 5 # static type is Int print c + 1 # outputs 6 c = [6, 7] # static type is Array[Int] print c[0] # outputs "6"
# ... var d if exp > 0 then d = 5 else d = 6 end # Static type is Int print d + 1
Variable Upper Bound
An optional type information can be added to a variable declaration. This type is used as an upper bound of the type of the variable. When an initial value is given in a variable declaration without a specific type information, the static type of the initial value is used as an upper bound. If no type and no initial value are given, the upper bound is set to nullable Object.
var e: Int # Upper bound is Int e = "Hello" # Compile error: expected Int var f = 5 # Upper bound is Int f = "Hello" # Compile error: expected Int
var g: Object # Upper bound is Object g = 5 # OK since Int specializes Object var h: Object = 5 # Upper bound is Object h = "Hello" # OK
The adaptive typing flow is straightforward, therefore loops (for, while, loop) have a special requirement: on entry, the upper bound is set to the current static type; on exit, the upper bound is reset to its previous value.
var l: Object # static type is Object, upper bound is Object l = 5 # static type is Int, bound remains Object while l > 0 do # static type remains Int, bound sets to Int l -= 1 # OK l = "Hello" # Compile error: expected Int end # static type is Int, bound reset to Object l = "Hello" # OK
Type Checks
isa tests if an object is an instance of a given type. If the expression used in an isa is a variable, then its static type is automatically adapted, therefore avoiding the need of a specific cast.
var m: Object = 5 # ... if m isa Int then # static type of m is Int print m * 10 # OK end
Remember that adaptive typing follows the control flow, including the Boolean operators.
var n = new Array[Object] n.add(1) n.add(true) n.add("one") n.add(11) for i in n do # the static type of i is Object if not i isa Int then continue # now the static type of i is Int print i * 10 # OK end
An interesting example:
var max = 0 for i in n do if i isa Int and i > max then max = i # the > is valid since, in the right part # of the "and", the static type of i is Int end print max # outputs 11
Note that type adaptation occurs only in an isa if the target type is more specific than the current type.
var col: Collection[Int] = [1, 2, 3] if col isa Comparable then # the static type is still Collection[Int] # even if the dynamic type of a is a subclass # of both Collection[Int] and Comparable # ... end
Nullable Types
null is a literal value that is only accepted by some specific static types. However, thanks to adaptive typing, the static type management can be mainly automatic.
nullable annotates types that can accept null or an expression of a compatible nullable static type.
var o: nullable Int var p: Int o = 1 # OK p = 1 # OK o = null # OK o = p # OK
p = null # Compile error p = o # Compile error
Adaptive typing works well with nullable types.
var q if exp > 0 then q = 5 else q = null end # The static type of q is nullable Int
Moreover, like the isa keyword, the == and != operators can adapt the static type of a variable when compared to null.
var r: nullable Int = 10 # ... if r != null then # The static type of r is Int (without nullable) print r + 6 end # The static type of r is nullable Int
And another example:
var s: nullable Int = 10 # ... loop if s == null then break # The static type of s is Int print s + 1 s = null # The static type of s is null end
or else can be used to compose a nullable expression with any other expression. The value of x or else y is x if x is not null and is y if x is null. The static type of x or else y is the combination of the type of y and the not null version of the type of x.
var t: nullable Int = 10 # ... var u = t or else 0 # the static type of u is Int (without nullable)
Note that nullable types require a special management for attributes \goto{attribute} and constructors \goto{constructor}.
Explicit Cast
as casts an expression to a type. The expression is either casted successfully or there is an abort.
var v: Object = 5 # static type of v is Object print v.as(Int) * 10 # outputs 50
print v.as(String) # aborts: cast failed
Note that as does not change the object nor does perform conversion.
var w: Object = 5 # static type of w is Object print w.as(Int) + 10 # outputs "15" print w.to_s + "10" # outputs "510"
Because of type adaptation, as is rarely used on variables. isa (sometime coupled with assert) is preferred.
var x: Object = 5 # static type of x is Object assert x isa Int # static type of x is now Int print x * 10 # outputs 50
as(not null) can be used to cast an expression typed by a nullable type to its non nullable version. This form keeps the programmer from writing explicit static types.
var y: nullable Int = 5 # static type of y is nullable Int print y.as(not null) * 10 # cast, outputs 50 print y.as(Int) * 10 # same cast, outputs 50 assert y != null # same cast, but type of y is now Int print y * 10 # outputs 50
Static Type Combination Rule
Adaptive typing, literal arrays, and or else need to determine a static type by combining other static types. This is done by using the following rule:
The final type is
nullableif at least one of the types isnullable.The final type is the static type that is more general than all the other types.
If there is no such a type, and the thing typed is a variable, then the final type is the upper bound type of the variable; else there is a compilation error.
var dis: Discrete = 'a' # Note: Int < Discrete < Object var z if exp > 0 then z = 1 else z = dis # static type is Discrete if exp < 0 then z = 1 else z = "1" # static type is nullable Object (upper bound) var a1 = [1, dis] # a1 is a Array[Discrete]
var a2 = [1, "1"] # Compile error: # incompatible types Int and String
Modules
module declares the name of a module. While optional, it is recommended to use it, at least for documentation purposes. The basename of the source file must match the name declared with module. The extension of the source file must be nit.
A module is made of, in order:
- the module declaration;
- module importations;
- class definitions (and refinements) ;
- top-level function definitions (and redefinitions) ;
- main instructions .
Module Importation
import declares dependencies between modules. By default (that is without any import declaration), a module publicly imports the module standard. Dependencies must not produce cycles. By importing a module, the importer module can see and use classes and properties defined in the imported module.
importindicates a public importation. Importers of a given module will also import its publicly imported modules. An analogy is using#includein a header file (.h) in C/C++.private importindicates a private importation. Importers of a given module will not automatically import its privately imported modules. An analogy is using#includein a body file (.c) in C/C++.intrude importindicates an intrusive importation.intrudeimportbypasses theprivatevisibility and gives to the importer module full access on the imported module. Such an import may only be considered when modules are strongly bounded and developed together. The closest, but insufficient, analogy is something like including a body file in a body file in C/C++.
Visibility
By default, all classes, methods, constructors and virtual types are public which means freely usable by any importer module. Once something is public it belongs to the API of the module and should not be changed.
private indicates classes and methods that do not belong to the API. They are still freely usable inside the module but are invisible in other modules (except those that use intrude import).
protected indicates restricted methods and constructors. Such methods belong to the API of the module but they can only be used with the self receiver. Basically, protected methods are limited to the current class and its subclasses. Note that inside the module (and in
intrude importers), there is still no restriction.
Visibility of attributes is more specific and is detailed in its own section.
module m1 class Foo fun pub do ... protected fun pro do ... private fun pri do ... end private class Bar fun pri2 do ... end var x: Foo = ... var y: Bar = ... # All OK, it is # inside the module x.foo x.pro x.pro y.pri2
module m2 import m1 class Baz super Foo fun derp do self.pro # OK end end var x: Foo = ... x.pub # OK x.pro # Compile error: # pro is protected x.pri # Compile error: # unknown method pri var y: Bar # Compile error: # unknown class Bar
Visibility Coherence
In order to guarantee the coherence in the visibility, the following rules apply:
Classes and properties privately imported are considered private: they are not exported and do not belong to the API of the importer.
Properties defined in a private class are private.
A static type is private if it contains a private class or a private virtual type.
Signatures of public and protected properties cannot contain a private static type.
Bounds of public generic class and public virtual types cannot contain a private static type.
Classes
interface, abstract class, class and enum are the four kinds of classes. All these classes can be in multiple inheritance, can define new methods and redefine inherited method (yes, even interfaces).
Here are the differences:
interfaces can only specialize other interfaces, cannot have attributes, cannot have constructors, cannot be instantiated.
abstract classes cannot specialize enums, can have attributes, must have constructors, cannot be instantiated.
concrete classes (i.e.
class) cannot specialize enums, can have attributes, must have constructors, can be instantiated.enums (e.g.
IntorBool) can only specialize interfaces, cannot have attributes, cannot have constructors, have proper instances but they are not instantiated by the programmer—it means nonew Int. Note that at this point there is no user-defined enums.
All kinds of classes must have a name, can have some superclasses and can have some definitions of properties. Properties are methods, attributes, constructors and virtual types. All kinds of classes can also be generic. When documentation refers to “classes” , it generally refers to all four kinds. The term “concrete classes” is used to designate the classes declared with the class keyword alone.
Class Specialization
super declares superclasses. Classes inherit methods, attributes and virtual-types defined in their superclasses. Currently, constructors are inherited in a specific manner.
Object is the root of the class hierarchy. It is an interface and all other kinds of classes are implicitly a subclass of Object.
There is no repeated inheritance nor private inheritance. The specialization between classes is transitive, therefore super declarations are superfluous (thus ignored).
Class Refinement
redef allows modules to refine imported classes (even basic ones). Refining a class means:
adding new properties: methods, attributes, constructors, virtual types;
redefining existing properties: methods and constructors;
adding new superclasses.
Note that the kind or the visibility of a class cannot be changed by a refinement. Therefore, it is allowed to just write redef class X regardless of the kind or the visibility of X.
In programs, the real instantiated classes are always the combination of all their refinements.
redef class Int fun fib: Int do if self < 2 then return self return (self-1).fib + (self-2).fib end end # Now all integers have the fib method print 15.fib # outputs 610
Methods
fun declares methods. Methods must have a name, may have parameters, and may have a return type. Parameters are typed; however, a single type can be used for multiple parameters.
fun foo(x, y: Int, s: String): Bool # ...
do declares the body of methods. Alike control structures, a one-liner version is available.
Therefore, the two following methods are equivalent.
fun next1(i: Int): Int do return i + 1 end fun next2(i: Int): Int do return i + 1
Inside the method body, parameters are considered as variables. They can be assigned and are subject to adaptive typing.
self, the current receiver, is a special parameter. It is not assignable but is subject to adaptive typing.
return exits the method and returns to the caller. In a function, the return value must be provided with a return in all control flow paths.
Method Call
Calling a method is usually done with the dotted notation x.foo(y, z). The dotted notation can be chained.
A method call with no argument does not need parentheses. Moreover, even with arguments, the parentheses are not required in the principal method of a statement.
var a = [1] a.add 5 # no () for add print a.length # no () for length, no () for print
However, this last facility requires that the first argument does not start with a parenthesis or a bracket.
foo (x).bar # will be interpreted as (foo(x)).bar foo [x].bar # will be interpreted as (foo[x]).bar
Method Redefinition
redef denotes methods that are redefined in subclasses or in class refinements. The number and the types of the parameters must be invariant. Thus, there is no need to reprecise the types of the parameters, only names are mandatory.
The return type can be redefined to be a more precise type. If same type is returned, there is no need to reprecise it.
The visibility, also, cannot be changed, thus there is also no need to reprecise it.
class Foo # implicitly an Object # therefore inherit '==' and 'to_s' var i: Int redef fun to_s do return "Foo{self.i}" redef fun ==(f) do return f isa Foo and f.i == self.i end
Abstract Methods
is abstract indicates methods defined without a body. Subclasses and refinements can then redefine it (the redef is still mandatory) with a proper body.
interface Foo fun derp(x: Int): Int is abstract end class Bar super Foo redef fun derp(x) do return x + 1 end
Concrete classes may have abstract methods. It is up to a refinement to provide a body.
Call to Super
super calls the “previous” definition of the method. It is used in a redefinition of a method in a subclass or in a refinement, It can be used with or without arguments; in the latter case, the original arguments are implicitly used.
The super of Nit behaves more like the call-next-method of CLOS than the super of Java or Smalltalk. It permits the traversal of complex class hierarchies and refinement. Basically, super is polymorphic: the method called by super is not only determined by the class of
definition of the method, but also by the dynamic type of self.
The principle is to produce a strict order of the redefinitions of a method (the linearization). Each call to super call the next method definition in the linearization. From a technical point of view, the linearization algorithm used is based on C3. It ensures that:
A definition comes after its redefinition.
A redefinition in a refinement comes before a redefnition in its superclass.
The order of the declaration of the superclasses is used as the ultimate disambiguation.
class A fun derp: String do return "A" end class B super A redef fun derp do return "B" + super end class C super A redef fun derp do return "C" + super end class D super B super C redef fun derp do return "D" + super # Here the linearization order of the class D is DBCA # D before B because D specializes B # B before A because B specializes A # D before C because D specializes C # C before A because C specializes A # B before C because in D 'super B' is before 'super C' end var b = new B print b.derp # outputs "BA" var d = new D print d.derp # outputs "DBCA"
Operators and Setters
Operators and setters are methods that require a special syntax for their definition and their invocation.
binary operators:
+,-,*,/,\%,==,<,>,<=,>=,<<,>>and<=>. Their definitions require exactly one parameter and a return value. Their invocation is done withx + ywherexis the receiver,+is the operator, andyis the argument.unary operator:
-. Its definition requires a return value but no parameter. Its invocation is done with-xwherexis the receiver.bracket operator:
[]. Its definition requires one parameter or more and a return value. Its invocation is done withx[y, z]wherexis the receiver,ythe first argument andzthe second argument.setters:
something=wheresomethingcan be any valid method identifier. Their definitions require one parameter or more and no return value. If there is only one parameter, the invocation is done withx.something = ywherexis the receiver andythe argument. If there is more that one parameter, the invocation is done withx.something(y, z) = twherexis the receiver,ythe first argument,zthe second argument andtthe last argument.bracket setter:
[]=. Its definition requires two parameters or more and no return value. Its invocation is done withx[y, z] = twherexis the receiver,ythe first argument,zthe second argument andtthe last argument.
class Foo fun +(a: Bar): Baz do ... fun -: Baz do ... fun [](a: Bar): Baz do ... fun derp(a: Bar): Baz do ... fun derp(a: Bar, b: Baz) do ... fun []= (a: Bar, b: Baz) do ... end var a: Foo = ... var b: Bar = ... var c: Baz = ... c = a + b c = -b c = a[b] # The bracket operator '[]' c = a.derp(b) # A normal method 'derp' a.derp(b) = c # A setter 'derp' a[b] = c # The bracket setter '[]='
+= and -= are combinations of the assignment (=) and a binary operator. These feature are extended to setters where a single += is in fact three method calls: a function call, the operator call, then a setter call.
a += c # equiv. a = a + c a[b] += c # equiv. a[b] = a[b] + c a.foo += c # equiv. a.foo = a.foo + c a.bar(b) += c # equiv. a.bar(b) = a.bar(b) + c
Variable Number of Arguments
A method can accept a variable number of arguments using ellipsis (...). The definition use x: Foo... where x is the name of the parameter and Foo a type. Inside the body, the static type of x is Array[Foo]. The caller can use 0, 1, or more arguments for the parameter x. Only one ellipsis is allowed in a signature.
fun foo(x: Int, y: Int..., z: Int) do print "{x};{y.join(",")};{z}" end foo(1, 2, 3, 4, 5) # outputs "1;2,3,4;5" foo(1, 2, 3) # outputs "1;2;3"
Top-level Methods and Main Body
Some functions, like print, are usable everywhere simply without using a specific receiver. Such methods are just defined outside any classes. In fact, these methods are implicitly defined in the
Object interface, therefore inherited by all classes, therefore usable everywhere. However, this principle may change in a future version.
In a module, the main body is a bunch of statements at the end of a file. The main body of the main module is the program entry point. In fact, the main method of a program is implicitly defined as the redefinition of the method main of the Sys class; and the start of the program is the implicit statement (Sys.new).main. Note that because it is a redefinition, the main part can use super to call the “previous” main part in the imported modules. If there is no main part
in a module, it is inherited from imported modules.
Top-level methods coupled with the main body can be used to program in a pseudo-procedural way. Therefore, the following programs are valid:
print "Hello World"
fun sum(i, j: Int): Int do return i + j end print sum(4, 5)
Intern and Extern Methods
intern and extern indicate concrete methods whose body is not written in Nit.
The body of intern methods is provided by the compiler itself for performance or bootstrap reasons. For the same reasons, some intern methods, like + in Int are not redefinable.
The body of extern methods is provided by libraries written in C; for instance, the system libraries required for input/output. Extern methods are always redefinable. See FFI \goto{FFI} for more information on extern methods.
Attributes
var, used inside concrete and abstract classes, declares attributes. Attributes require a static type and can possibly have an initial value (it may be any kind of expression, even including
self)
class Foo var i: Int = 5 fun dec(x: Int) do var k = self.i if k > x then self.i = k - x else self.i = 0 end end
Note that from an API point of view, there is no way to distinguish the read access of an attribute with a normal method neither to distinguish a write access of an attribute with a setter. Therefore, the read access of an attribute is called a getter while the write access is called a setter.
var x = foo.bar # Is bar an attribute or a method foo.bar = y # Is bar an attribute or a setter # In fact, we do not need to know.
Visibility of Attributes
By default, a getter is public and a setter is private. The visibility of getters can be precised with the private or protected keywords. The visibility of setters can be specified with an
additional writable keyword.
class Foo2 var pub_pri: Int protected var pro_pri: Int var pub_pub: Int is writable private var pri_pro: Int is protected writable var pub_pri2: Int is private writable # the default end
Redefinition of Attributes
Getters and setters of attributes behave like genuine methods that can be inherited and redefined. Getters and setters can also redefine inherited methods. redef var declares that the getter is
a redefinition while redef writable declares that the setter is a redefinition.
interface Foo3 fun derp: Int is abstract fun derp(o: Int) is abstract end class Bar3 super Foo3 redef var derp is redef writable end class Baz3 super Bar3 redef fun derp do return 1 redef fun derp(o) do end end
Constructors
Constructors in Nit behave differently.
Their objective is double :
- be compatible with full multiple-inheritance
- be simple enough to be KISS and compatible with the principle of least surprise.
new construction and simple classes
Classes in OO models are often a simple aggregates of attributes and methods.
By default, the new construction requires a value for each attribute defined in a class without a default value.
class Product var id: String var description: String var price: Float end var p = new Product("ABC", "Bla bla", 15.95) assert p.id == "ABC"
In subclasses, additional attributes are automatically collected.
class Product var id: String var description: String var price: Float end class Book super Product var author: String end var book = new Book("ABC", "Bla bla", 15.95, "John Doe")
special init method
The special init method is automatically invoked after the end of a new construction.
It is used to perform additional systematic tasks.
Because the init is run at the end of the initialization sequence, initialized attributes are usable in the body.
class Product var id: String var description: String var price: Float end class OverpricedProduct super Product init do price = price * 10.0 end end var op = new OverpricedProduct("ABC", "Bla bla", 15.95) assert op.price.is_approx(159.50, 0.001)
Uncollected attributes
There are three cases for which an attribute is not collected in a new construction.
- Attributes with a default value
- Attributes with the annotation
noinit - Attributes introduced in refinement of classes
class Product var id: String var description: String var price: Float end class TaxedProduct super Product var tax_rate = 9.90 var total_price: Float is noinit init do total_price = price * (1.0 + tax_rate100.0) end end var tp = new TaxedProduct("ABC", "Bla bla", 15.95) assert tp.total_price.is_approx(17.52905, 0.00001)
Note: The orchestration here is important. In order, the following is executed:
- All defauts values are computed and set
- Setters are invoked.
initis invoked.
Therefore, total_price cannot be initialised with a default value, because at the time of the computation of the default values, the attribute price in not yet initialised.
Generalized initializers
Initializers are methods that are automatically invoked by new.
In fact, by default, the setter of an attribute is used as an initializer.
autoinit is used to register a method as a setter.
class Product var id: String var description: String var price: Float end class FooProduct super Product fun set_xy(x, y: Int) is autoinit do z = x * 10 + y var z: Int is noinit end var fp = new FooProduct("ABC", "Bla bla", 15.96, 1, 3) assert fp.z == 13
Generalized setters are a powerful tool, but only needed in rare specific cases.
In most cases, there is no reason for an argument of a new construction to not be stored in the object as a real attribute.
Inheritance
As explained above, one of the main advantage of these constructors is their compatibility with multiple inheritance.
class Product var id: String var description: String var price: Float end class OverpricedProduct super Product init do price = price * 10.0 end end class TaxedProduct super Product var tax_rate = 9.90 var total_price: Float is noinit init do total_price = price * (1.0 + tax_rate100.0) end end class FooProduct super Product fun set_xy(x, y: Int) is autoinit do z = x * 10 + y var z: Int is noinit end class MultiProduct super OverpricedProduct super TaxedProduct super FooProduct end var mp = new MultiProduct("ABC", "Bla bla", 15.96, 1, 3) assert mp.id == "ABC" assert mp.price.is_approx(159.6, 0.001) assert mp.total_price.is_approx(175.4, 0.001) assert mp.z == 13
Named init
Named init are less flexible trough inheritance, thus should no be used.
They allow to have additional constructor for classes and more control in the construction mechanism.
class Point var x: Float var y: Float init origin do init(0.0, 0.0) end init polar(r, phi: Float) do var x = r * phi.cos var y = r * phi.sin init(x, y) end redef fun to_s do return "({x},{y})" end var p1 = new Point(1.0, 2.0) assert p1.to_s == "(1.0,2.0)" var p2 = new Point.origin assert p2.to_s == "(0.0,0.0)" var p3 = new Point.polar(1.0, 2.0) assert p3.to_s == "(-0.416,0.909)"
Legacy init
nameless init defined with argument or with an explicit visibility are still accepted as a fallback of the old-constructors.
They should not be used since they will be removed in a near future.
new factories
new factories allow to completely shortcut the class instantiation mechanism.
It could be used to provide new syntax on non-concrete class (mainly extern class).
new factories behave like a top-level function that return the result of the construction.
It is basically some kind of syntactic sugar.
abstract class Person var age: Int new(age: Int) do if age >= 18 then return new Adult(age) else return new Child(age) end end end class Adult super Person # ... end class Child super Person # ... end
Generic Classes
Generic classes are defined with formal generic parameters declared within brackets. Formal generic parameters can then be used as a regular type inside the class. Generic classes must always be qualified when used.
class Pair[E] var first: E var second: E fun is_same: Bool do return self.first == self.second end end var p1 = new Pair[Int](1, 2) print p1.second * 10 # outputs "20" print p1.is_same # outputs "false" var p2 = new Pair[String]("hello", "world") p2.first = "world" print p2.is_same # outputs "true"
Unlike many object-oriented languages, generic classes in Nit yield a
kind of sub-typing. For example, Pair[Int] is a subtype of
Pair[Object].
Virtual Types
type declares a virtual types in a class. A bound type is
mandatory. Virtual types can then be used as regular types in the class
and its subclasses. Subclasses can also redefine it with a more specific
bound type. One can see a virtual type as an internal formal generic
parameter or as a redefinable typedef.
class Foo type E: Object var derp: E end class Bar super Foo redef type E: Int end var b = new Bar(5) print b.derp + 1 # outputs 6