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
do
of methods and the singledo
is on its own line and not indented;the other
do
are 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
,or
andnot
are 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 (likeequals
in 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 != y
is 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
return
with 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
nullable
if 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.
import
indicates a public importation. Importers of a given module will also import its publicly imported modules. An analogy is using#include
in a header file (.h
) in C/C++.private import
indicates a private importation. Importers of a given module will not automatically import its privately imported modules. An analogy is using#include
in a body file (.c
) in C/C++.intrude import
indicates an intrusive importation.intrude
import
bypasses theprivate
visibility 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.
Int
orBool
) 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 + y
wherex
is the receiver,+
is the operator, andy
is the argument.unary operator:
-
. Its definition requires a return value but no parameter. Its invocation is done with-x
wherex
is the receiver.bracket operator:
[]
. Its definition requires one parameter or more and a return value. Its invocation is done withx[y, z]
wherex
is the receiver,y
the first argument andz
the second argument.setters:
something=
wheresomething
can 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 = y
wherex
is the receiver andy
the argument. If there is more that one parameter, the invocation is done withx.something(y, z) = t
wherex
is the receiver,y
the first argument,z
the second argument andt
the last argument.bracket setter:
[]=
. Its definition requires two parameters or more and no return value. Its invocation is done withx[y, z] = t
wherex
is the receiver,y
the first argument,z
the second argument andt
the 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.
init
is 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