Discussion:
Re-Export, module alias and nested module in M2R10
(too old to reply)
Xin Wang
2016-06-04 03:21:33 UTC
Permalink
Three questions about rationals behind designing of module facility in M2R10:

1. In M2R10, we can append `+` to indicate that module is re-exported. If I'm reading correctly, after import an aggregation module, like `FooBarBaz`, we can use `Foo.quux` directly. There is a problem that names of `Foo` and `FooBarBaz` may be not related directly, it will be surprising that `Foo` is usable without being imported before. Why not support `FooBarBaz.Foo.quux`?

2. In description of "Constant Definitions", M2R10 report stated explicitly that "a constant may not be defined as an alias of a module...", it will forbid the possibility to define module alias, why this restriction is needed?

3. When reading through report, I do not sure whether M2R10 support nested module or not, something like `foo.bar.baz` in Python. It is partially related to question 1.


Thanks,
Xin Wang
trijezdci
2016-06-05 05:33:13 UTC
Permalink
There are two use cases that you may have confused.

One is an import aggregator module, for example:

DEFINITION MODULE Bitsets;

IMPORT BITSET16+, BITSET32+, BITSET64+, BITSET128+;

END Bitsets.

When you import this aggregator, it is as if you imported each re-exported module verbatim.

IMPORT Bitsets;

is equivalent to

IMPORT BITSET16, BITSET32, BITSET64, BITSET128;

All identifiers of the imported modules need to be qualified with their respective module identifiers. They cannot be qualified with the module identifier of the aggregator module.

Another use case is a library module that imports and re-exports a third library, for example:

DEFINITION MODULE Status;

TYPE Status = ( Success, Failure );

END Status.


DEFINITION MODULE IOStatus;

IMPORT Status;

TYPE IOStatus = ( +Status, AccessDenied, DeviceError, ... );

END IOStatus.


DEFINITION MODULE FileIO;

IMPORT IOStatus+;

TYPE Status = ALIAS OF IOStatus;

...

END FileIO.


In this example, module FileIO imports IOStatus and re-exports it, but it also defines an alias type for the IOStatus type. As a result, when importing FileIO, the status type could be references as FileIO.Status, or as IOStatus.

The alias is of the type, not of the module. This is due to the fact that a type defined in a library with the same name as the module is referenced by unqualified name.

DEFINITION MODULE Foo;

TYPE Foo = ( Bar, Baz, Bam );

END Foo.

then

IMPORT Foo;

allows referencing type Foo.Foo as Foo.

This is called a module type.

http://modula-2.info/m2r10/pmwiki.php/Reference/Glossary#AdtLibraryModule

hope this clarifies.
Xin Wang
2016-06-06 08:13:08 UTC
Permalink
Post by trijezdci
There are two use cases that you may have confused.
DEFINITION MODULE Bitsets;
IMPORT BITSET16+, BITSET32+, BITSET64+, BITSET128+;
END Bitsets.
When you import this aggregator, it is as if you imported each re-exported module verbatim.
IMPORT Bitsets;
is equivalent to
IMPORT BITSET16, BITSET32, BITSET64, BITSET128;
All identifiers of the imported modules need to be qualified with their respective module identifiers. They cannot be qualified with the module identifier of the aggregator module.
DEFINITION MODULE Status;
TYPE Status = ( Success, Failure );
END Status.
DEFINITION MODULE IOStatus;
IMPORT Status;
TYPE IOStatus = ( +Status, AccessDenied, DeviceError, ... );
END IOStatus.
DEFINITION MODULE FileIO;
IMPORT IOStatus+;
TYPE Status = ALIAS OF IOStatus;
...
END FileIO.
In this example, module FileIO imports IOStatus and re-exports it, but it also defines an alias type for the IOStatus type. As a result, when importing FileIO, the status type could be references as FileIO.Status, or as IOStatus.
The alias is of the type, not of the module. This is due to the fact that a type defined in a library with the same name as the module is referenced by unqualified name.
DEFINITION MODULE Foo;
TYPE Foo = ( Bar, Baz, Bam );
END Foo.
then
IMPORT Foo;
allows referencing type Foo.Foo as Foo.
This is called a module type.
http://modula-2.info/m2r10/pmwiki.php/Reference/Glossary#AdtLibraryModule
hope this clarifies.
Both cases are similiar.

Take first case as an example. When reading code, if encountered something like `BITSET16.Foo`, there is no way to determine that `BITSET16` is imported by `Bitsets` but not some other modules.

This kind of implicit may cause confusion for code readers.
trijezdci
2016-06-06 11:38:36 UTC
Permalink
Post by Xin Wang
Post by trijezdci
There are two use cases that you may have confused.
DEFINITION MODULE Bitsets;
IMPORT BITSET16+, BITSET32+, BITSET64+, BITSET128+;
END Bitsets.
When you import this aggregator, it is as if you imported each re-exported module verbatim.
IMPORT Bitsets;
is equivalent to
IMPORT BITSET16, BITSET32, BITSET64, BITSET128;
All identifiers of the imported modules need to be qualified with their respective module identifiers. They cannot be qualified with the module identifier of the aggregator module.
DEFINITION MODULE Status;
TYPE Status = ( Success, Failure );
END Status.
DEFINITION MODULE IOStatus;
IMPORT Status;
TYPE IOStatus = ( +Status, AccessDenied, DeviceError, ... );
END IOStatus.
DEFINITION MODULE FileIO;
IMPORT IOStatus+;
TYPE Status = ALIAS OF IOStatus;
...
END FileIO.
In this example, module FileIO imports IOStatus and re-exports it, but it also defines an alias type for the IOStatus type. As a result, when importing FileIO, the status type could be references as FileIO.Status, or as IOStatus.
The alias is of the type, not of the module. This is due to the fact that a type defined in a library with the same name as the module is referenced by unqualified name.
DEFINITION MODULE Foo;
TYPE Foo = ( Bar, Baz, Bam );
END Foo.
then
IMPORT Foo;
allows referencing type Foo.Foo as Foo.
This is called a module type.
http://modula-2.info/m2r10/pmwiki.php/Reference/Glossary#AdtLibraryModule
hope this clarifies.
Both cases are similiar.
Not really.

One is the use of a module as a shopping list.

The other is a use of a module as a data type.
Post by Xin Wang
Take first case as an example. When reading code, if encountered something like `BITSET16.Foo`, there is no way to determine that `BITSET16` is imported by `Bitsets` but not some other modules.
This is a non-issue.

Import aggregators beat the holy crap out of classic Modula-2 where some software libraries needed tens and hundreds of imports and you needed a PhD just to figure out what you needed to import to use the library. It also cluttered the modules to the point where you couldn't see the actual library for the forest of imports.

By contrast, our solution lets practitioners focus on the actual work.
Post by Xin Wang
This kind of implicit may cause confusion for code readers.
There is no danger of any confusion.

Wherever you see BITSET16 by itself, it will be immediately clear that it is a type. When you see it in a qualified name it will also be clear: a lowercase/camelcase name within an expression is a variable or function call, within a statement sequence it is a procedure call; a titlecase name is a constant.

Having said that, exporting variables is discouraged and you should only use that in extreme rare circumstances. The only case where our standard library exports variables is for stdIn, stdOut and stdErr. In general, variables should be hidded in an implementation part and access to them provided by accessor functions and mutator procedures.

That leaves constants, functions and procedures. However, module types will generally bind their constants, functions and procedures to built-in syntax.

For example, the constants of a library defined integer type that define the minimum and and maximum legal values for the type are required to be bound to TMIN and TMAX. Likewise, arithmetic functions are required to be bound to arithmetic operators.

So, you are not going to see any identifier being used other than the type identifier.

Instead of INTEGER64.MinValue, you will see TMIN(INTEGER64). Instead of INTEGER64.MaxValue, you will see TMAX(INTEGER64). Instead of INTEGER64.Add(i, j), you will see i+j, instead of INTEGER64.div(i, j) you will see i DIV j.

Likewise for collection types. Instead of RealPQ.Capacity, you will see TLIMIT(RealPQ) and instead of NameTable.forIterator(table, key, NameTable.ForLoopBodyProc), you will see FOR key IN table DO ... etc etc.

In any event, in any software development environment you need to have a certain grasp of the standard library.

As for third party libraries, it is the responsibility of the library authors to follow name conventions and structure them in such a way that they are easily discoverable and usable.
Xin Wang
2016-06-07 02:04:55 UTC
Permalink
Post by trijezdci
Post by Xin Wang
Post by trijezdci
There are two use cases that you may have confused.
DEFINITION MODULE Bitsets;
IMPORT BITSET16+, BITSET32+, BITSET64+, BITSET128+;
END Bitsets.
When you import this aggregator, it is as if you imported each re-exported module verbatim.
IMPORT Bitsets;
is equivalent to
IMPORT BITSET16, BITSET32, BITSET64, BITSET128;
All identifiers of the imported modules need to be qualified with their respective module identifiers. They cannot be qualified with the module identifier of the aggregator module.
DEFINITION MODULE Status;
TYPE Status = ( Success, Failure );
END Status.
DEFINITION MODULE IOStatus;
IMPORT Status;
TYPE IOStatus = ( +Status, AccessDenied, DeviceError, ... );
END IOStatus.
DEFINITION MODULE FileIO;
IMPORT IOStatus+;
TYPE Status = ALIAS OF IOStatus;
...
END FileIO.
In this example, module FileIO imports IOStatus and re-exports it, but it also defines an alias type for the IOStatus type. As a result, when importing FileIO, the status type could be references as FileIO.Status, or as IOStatus.
The alias is of the type, not of the module. This is due to the fact that a type defined in a library with the same name as the module is referenced by unqualified name.
DEFINITION MODULE Foo;
TYPE Foo = ( Bar, Baz, Bam );
END Foo.
then
IMPORT Foo;
allows referencing type Foo.Foo as Foo.
This is called a module type.
http://modula-2.info/m2r10/pmwiki.php/Reference/Glossary#AdtLibraryModule
hope this clarifies.
Both cases are similiar.
Not really.
One is the use of a module as a shopping list.
The other is a use of a module as a data type.
Post by Xin Wang
Take first case as an example. When reading code, if encountered something like `BITSET16.Foo`, there is no way to determine that `BITSET16` is imported by `Bitsets` but not some other modules.
This is a non-issue.
Import aggregators beat the holy crap out of classic Modula-2 where some software libraries needed tens and hundreds of imports and you needed a PhD just to figure out what you needed to import to use the library. It also cluttered the modules to the point where you couldn't see the actual library for the forest of imports.
By contrast, our solution lets practitioners focus on the actual work.
Post by Xin Wang
This kind of implicit may cause confusion for code readers.
There is no danger of any confusion.
Wherever you see BITSET16 by itself, it will be immediately clear that it is a type. When you see it in a qualified name it will also be clear: a lowercase/camelcase name within an expression is a variable or function call, within a statement sequence it is a procedure call; a titlecase name is a constant.
Having said that, exporting variables is discouraged and you should only use that in extreme rare circumstances. The only case where our standard library exports variables is for stdIn, stdOut and stdErr. In general, variables should be hidded in an implementation part and access to them provided by accessor functions and mutator procedures.
That leaves constants, functions and procedures. However, module types will generally bind their constants, functions and procedures to built-in syntax.
For example, the constants of a library defined integer type that define the minimum and and maximum legal values for the type are required to be bound to TMIN and TMAX. Likewise, arithmetic functions are required to be bound to arithmetic operators.
So, you are not going to see any identifier being used other than the type identifier.
Instead of INTEGER64.MinValue, you will see TMIN(INTEGER64). Instead of INTEGER64.MaxValue, you will see TMAX(INTEGER64). Instead of INTEGER64.Add(i, j), you will see i+j, instead of INTEGER64.div(i, j) you will see i DIV j.
Likewise for collection types. Instead of RealPQ.Capacity, you will see TLIMIT(RealPQ) and instead of NameTable.forIterator(table, key, NameTable.ForLoopBodyProc), you will see FOR key IN table DO ... etc etc.
In any event, in any software development environment you need to have a certain grasp of the standard library.
As for third party libraries, it is the responsibility of the library authors to follow name conventions and structure them in such a way that they are easily discoverable and usable.
Still, I think `Bitsets.BITSET16` is clearer than `BITSET16` if only type identifier get heavily used.

Besides that, re-export seems a lot like Modula-2's original unqualified export, which Oberon and M2R10 are trying to get rid of.
trijezdci
2016-06-07 03:55:19 UTC
Permalink
Post by Xin Wang
Still, I think `Bitsets.BITSET16` is clearer than `BITSET16` if only type identifier get heavily used.
So you would also want

Cardinals.CARDINAL, Cardinals.LONGCARD, Cardinals.SHORTCARD, Cardinals.LONGLONGCARD,

Integers.INTEGER, Integers.LONGINT, Integers.SHORTINT, Integers.LONGLONGINT,

Reals.REAL, Reals.LONGREAL, Reals.SHORTREAL, Reals.LONGLONGREAL ???


In fact, you could no longer use SHORTCARD, LONGLONGCARD, SHORTINT, LONGLONGINT, SHORTREAL and LONGLONGREAL because these are ALIAS types.

What they are ALIAS types of is dependent on the architecture.

On one architecture SHORTINT may be an alias for INTEGER, on another architecture it may be an alias of INTEGER16. The whole point of using those alias types is not to have to care what their implementations actually are.

But following your suggestion, you would need to know and import their actual implementation:

Instead of ...

IMPORT SHORTINT;

... you would then need ...

IMPORT INT16, Integers.SHORTINT;

But in other scenarios this approach might no longer be portable then, so you would need to clutter all your modules with conditional compilation pragmas

<* IF TSIZE(INTEGER)*8 = 16 *>

TYPE SHORTINT = ALIAS OF INTEGER;

<* ELSE *>

IMPORT INT16;

TYPE SHORTINT = ALIAS OF INT16;


Worse still, instead of having one convenient import for all IO for the predefined types

IMPORT IOSupport;

... you'd then need something like this ...

IMPORT IOSupport.CharIO, IOSupport.CharArrayIO.CharBareArrayIO, IOSupport.CharArrayIO.CharFlexArrayIO, IOSupport.CharIO, IOSupport.CharArrayIO.CharBareArrayIO;

... which defeats the whole purpose of having an aggregate import facility in the first place, because it would be easier to import every module one by one.

IMPORT CharIO, CharBareArrayIO, CharFlexArrayIO, RealIO, RealBareArrayIO;

Also, this would be confusing because only CHAR and UNICHAR need separate IO extensions for bare arrays and regular arrays, the latter representing character strings and need to be formatted as such.

VAR a : BARE ARRAY 3 OF CHAR; a := {"a", "b", "c"};
WRITE(a) => CharBareArrayIO.Write(stdOut, a) => {"a", "b", "c"}

but

VAR s : ARRAY 80 OF CHAR; s := "foobar";
WRITE(s) => CharFlexArrayIO.Write(stdOut, s) => foobar


Other types only need a single IO extension for arrays, because the output format will always be a list of values. And since regular arrays are passing compatible with BARE open array formal types, the compiler will use the bare array IO if no binding for regular array IO is present.
Post by Xin Wang
Besides that, re-export seems a lot like Modula-2's original unqualified export
No, its more like C's #include, only that the "include" is limited to imports of modules.
Xin Wang
2016-06-07 05:36:37 UTC
Permalink
Sorry, but I may not quite get your point, following are some random thoughts.
Post by trijezdci
Post by Xin Wang
Still, I think `Bitsets.BITSET16` is clearer than `BITSET16` if only type identifier get heavily used.
So you would also want
Cardinals.CARDINAL, Cardinals.LONGCARD, Cardinals.SHORTCARD, Cardinals.LONGLONGCARD,
Integers.INTEGER, Integers.LONGINT, Integers.SHORTINT, Integers.LONGLONGINT,
Reals.REAL, Reals.LONGREAL, Reals.SHORTREAL, Reals.LONGLONGREAL ???
I may need some of those types, but rarely all of them, so I can just import what I needed directly without using those aggregate modules.
Post by trijezdci
In fact, you could no longer use SHORTCARD, LONGLONGCARD, SHORTINT, LONGLONGINT, SHORTREAL and LONGLONGREAL because these are ALIAS types.
What they are ALIAS types of is dependent on the architecture.
On one architecture SHORTINT may be an alias for INTEGER, on another architecture it may be an alias of INTEGER16. The whole point of using those alias types is not to have to care what their implementations actually are.
Instead of ...
IMPORT SHORTINT;
... you would then need ...
IMPORT INT16, Integers.SHORTINT;
But in other scenarios this approach might no longer be portable then, so you would need to clutter all your modules with conditional compilation pragmas
<* IF TSIZE(INTEGER)*8 = 16 *>
TYPE SHORTINT = ALIAS OF INTEGER;
<* ELSE *>
IMPORT INT16;
TYPE SHORTINT = ALIAS OF INT16;
Reguarding to ALIAS type, I found some code identitical to above example in _STANDARD_LIBRARY/SHORTINT.def, if I can `IMPORT SHORTINT` to use this ALIAS type, I think it will be no difference to `IMPORT Integers` and use `Integers.SHORTINT` to reference this type.
Post by trijezdci
Worse still, instead of having one convenient import for all IO for the predefined types
IMPORT IOSupport;
... you'd then need something like this ...
IMPORT IOSupport.CharIO, IOSupport.CharArrayIO.CharBareArrayIO, IOSupport.CharArrayIO.CharFlexArrayIO, IOSupport.CharIO, IOSupport.CharArrayIO.CharBareArrayIO;
... which defeats the whole purpose of having an aggregate import facility in the first place, because it would be easier to import every module one by one.
IMPORT CharIO, CharBareArrayIO, CharFlexArrayIO, RealIO, RealBareArrayIO;
Yes, I agree with this point, according to this point aggregate import seems have no much use.
Post by trijezdci
Also, this would be confusing because only CHAR and UNICHAR need separate IO extensions for bare arrays and regular arrays, the latter representing character strings and need to be formatted as such.
VAR a : BARE ARRAY 3 OF CHAR; a := {"a", "b", "c"};
WRITE(a) => CharBareArrayIO.Write(stdOut, a) => {"a", "b", "c"}
but
VAR s : ARRAY 80 OF CHAR; s := "foobar";
WRITE(s) => CharFlexArrayIO.Write(stdOut, s) => foobar
Other types only need a single IO extension for arrays, because the output format will always be a list of values. And since regular arrays are passing compatible with BARE open array formal types, the compiler will use the bare array IO if no binding for regular array IO is present.
I think its programmer's duty to specify which version to use explicitly.
Post by trijezdci
Post by Xin Wang
Besides that, re-export seems a lot like Modula-2's original unqualified export
No, its more like C's #include, only that the "include" is limited to imports of modules.
It seems confusing to use one concept "module" to support two facilities which are not similar.
trijezdci
2016-06-07 12:49:48 UTC
Permalink
Post by Xin Wang
Sorry, but I may not quite get your point, following are some random thoughts.
Post by trijezdci
Integers.INTEGER, Integers.LONGINT, Integers.SHORTINT, Integers.LONGLONGINT,
I may need some of those types, but rarely all of them, so I can just import what I needed directly without using those aggregate modules.
Then don't use them. Just because you choose not to use something offered by the standard library doesn't mean it shouldn't be offered.

The whole point of modular languages is to hide unnecessary detail at each abstraction level. Aggregator modules help do exactly that.
trijezdci
2016-06-05 06:29:49 UTC
Permalink
As for nesting of modules within modules, which is called local modules in classic Modula-2, this feature has been removed. Note that Wirth had also removed local modules in Oberon.

I'd like to take the opportunity to point out that M2 R10 is very much a parallel re-imagination of Oberon. Most of the design decisions are the result of tackling the same issues that Wirth and Gutknecht were tackling when they created Oberon out of classic Modula-2. In some cases we came to the same conclusion and our solutions are the same or similar. Such was the case with local modules. In other cases we came to different conclusions and different solutions. Our "Oberon" is more Modula-2 than it is Oberon.

Amongst the issues were:

(1) octal literals

outdated, horrible syntax => removed.

(2) operator synonyms

mutiple syntax for the same concept is generally bad design, either AND, OR and NOT, or &, | and ~, but not both and not inconsistenly mixed either. However, | is used for case labels, so the only possible solution was to remove & and ~. Nice side effect: & became available for the concatenation operator.

(3) enumerations

unqualified enumerated values can cause name conflicts => enumerated values are qualified with their type identifier.

(4) qualified versus unqualified import

both Oberon and R10 do not support unqualified import anymore, both provide a means to establish an alias name for a qualified name, however, in R10 this alias must match the unqualified name, it cannot be used to rename it.

(5) local modules

causes clutter, violates the principle of keeping modules small, requires significant implementation effort for little return => removed.

(6) variant records

unsafe and fragile => replaced with extensible records.

(7) FOR loop

limited added value in relation to WHILE and REPEAT loops, loop variant can be threatened, value of loop variant on loop exit uncertain => replaced with FOR IN loop, loop variant immutable and local to loop body.

(8) array indices

both Oberon and R10 have zero-based array indices, in R10 this change allowed the introduction of negative index values to address values from the end of the array backwards.

(9) lack of arrays with variable-length semantics

here Oberon provides dynamic open arrays, we provide static arrays with fixed and variable length semantics and a low-level building block (indeterminate record type) to aid the construction of dynamic open arrays as opaque ADTs.

(10) lack of use of built-in syntax with abstract data types

Oberon provides a language extension (by ETHZ) to use infix operators with library defined numeric types for math applications. We provide a general facility to allow any user defined abstract data type to use built-in syntax, not only operators, but also predefined functions, the FOR loop etc.
Loading...