Peter N Lewis wrote:
Does New as a function return nil or does it still give a runtime error?
If I want to allocate memory, but handle it gracefully if it fails, what are the recommended approaches?
You could also trap runtime errors, using `AtExit', or use the `Trap' unit (which does this internally). This comes somewhat closer to exception, though it's not quite the same. Indeed, this may be the best solution available. It's a bit code to write, but it uses `New' and has no disadvantages in this regard.
Ok, it's an interesting approach, but I'd have to write that code for each type, which is still pretty ugly.
I just checked, and if New returns nil (using the UndocumentedReturnNil for example), the compiler will blindly try to Initialize the returned data, and bus error.
I'm not sure if this counts as a bug, since presumably UndocumentedReturnNil is only suppose to be for BP compatibility, and with BP compatibility there would be no Schema and thus no initialization. Unless perhaps BP has objects, in which case, New(obj) which returned UndocumentedReturnNil would probably have the same bus error issue.
It's a shame, as this would be the best solution (documenting UndocumentedReturnNil and allowing GetMem to return it).
I'd love to start using New and Schema and such, but the best solution seems to be the Trap technique and that's some ugly code to have any place you want to use New and not crash out.
I guess I can stick to using the system NewPtr routine and manually calling Initialize, but that doesn't mesh will with Schema.
I did think of another hack, which was to pre-allocate a chunk of memory larger than the largest New command, and then have GetMem return that for failure, then rather than testing for nil, I could test of that safe return value. It's ugly, but probably the simplest solution that provides clean New code. Peter.
Peter N Lewis wrote:
Peter N Lewis wrote:
Does New as a function return nil or does it still give a runtime error?
If I want to allocate memory, but handle it gracefully if it fails, what are the recommended approaches?
You could also trap runtime errors, using `AtExit', or use the `Trap' unit (which does this internally). This comes somewhat closer to exception, though it's not quite the same. Indeed, this may be the best solution available. It's a bit code to write, but it uses `New' and has no disadvantages in this regard.
Ok, it's an interesting approach, but I'd have to write that code for each type, which is still pretty ugly.
I just checked, and if New returns nil (using the UndocumentedReturnNil for example), the compiler will blindly try to Initialize the returned data, and bus error.
I'm not sure if this counts as a bug, since presumably UndocumentedReturnNil is only suppose to be for BP compatibility, and with BP compatibility there would be no Schema and thus no initialization. Unless perhaps BP has objects, in which case, New(obj) which returned UndocumentedReturnNil would probably have the same bus error issue.
It's a shame, as this would be the best solution (documenting UndocumentedReturnNil and allowing GetMem to return it).
I'd love to start using New and Schema and such, but the best solution seems to be the Trap technique and that's some ugly code to have any place you want to use New and not crash out.
Yes, trapping each single allocation is quite some work. The same would hold for exceptions, BTW. If you'd add an exception handler for each single allocation, this will get a bit ugly. Often, AFAIK, exception handlers are used around somewhat larger blocks. You can do the same, of course, with Trap. (That's what I do in one of my programs, to basically handle all runtime errors globally.)
With a nil-return approach you'd really have to check each allocation, of course ...
I think checking for nil before initialization would not be that hard to do in the compiler. The main drawback I see would be that it would add some code to all programs because the compiler can't know whether the allocator that's active at runtime may return nil. (And no, I don't want to add a compiler option. This would create quite an ugly mess of compile-time settings and runtime behaviour interdependence and just invite hard to find bugs.) OTOH, the check would, of course, only be necessary if the type needs initialization at all (which the compiler knows), so most allocations are not affected -- only files, schemata and objects ATM. So perhaps it's not that bad. Such things are not allocated and initializalized in tight inner loops usually, anyway.
And then there's a problem with allocation in the RTS. I don't think there are so many places, so I could probably take care of all of them and check for nil-returns. The question is, of course, what to do then. Unless there's a reasonable default behaviour that can be done without allocating memory (rarely, I'd suppose), I think the best and safest thing to do is raise a runtime error. And there we are again ...
I did think of another hack, which was to pre-allocate a chunk of memory larger than the largest New command, and then have GetMem return that for failure, then rather than testing for nil, I could test of that safe return value. It's ugly, but probably the simplest solution that provides clean New code.
It's a kludge. I think it would work, but I wouldn't like to keep it permanently ... ;-)
Frank
Frank Heckenbach wrote:
... snip ...
It's a kludge. I think it would work, but I wouldn't like to keep it permanently ... ;-)
You could arrange that the standard procedure new returns NIL for failure, and that the compiler generates all calls with a following "if p = NIL THEN trap;". This would duplicate the standard specified action, but allow for a compile time switch (or pseudo comment) to inhibit the trap action generation. Then the user can install whatever action he desires.
It also makes it easy for the normal trap action to report file:linenumber data.
CBFalconer wrote:
Frank Heckenbach wrote:
... snip ...
It's a kludge. I think it would work, but I wouldn't like to keep it permanently ... ;-)
You could arrange that the standard procedure new returns NIL for failure, and that the compiler generates all calls with a following "if p = NIL THEN trap;". This would duplicate the standard specified action, but allow for a compile time switch (or pseudo comment) to inhibit the trap action generation. Then the user can install whatever action he desires.
OTOH, it produces more runtime code.
It also makes it easy for the normal trap action to report file:linenumber data.
Yes, but we can also get this information via a backtrace and addr2line, so that's not the most important thing.
Frank
Got the following from "Borland Pascal with Objects - Language Guide"
function MaxAvail "Returns the size of the largest contiguous free block in the heap, indicating the size of the largest dynamic varible that can be allocated at the time of the call to MaxAvail."
function MemAvail "Returns the number of free bytes of heap storage available."
Also on another page there is a blurb on how to make New and GetMem return nil instead of aborting the program.
Russ
Russell Whitaker wrote:
Got the following from "Borland Pascal with Objects - Language Guide"
function MaxAvail "Returns the size of the largest contiguous free block in the heap, indicating the size of the largest dynamic varible that can be allocated at the time of the call to MaxAvail."
function MemAvail "Returns the number of free bytes of heap storage available."
Also on another page there is a blurb on how to make New and GetMem return nil instead of aborting the program.
These are typical Borland kludges, that don't and can't port to modern systems with virtual memory. New has to adhere to standard specifications, the other three are extension and can be constructed to do anything.
For example, under Windoze or *ix the action of new is likely to depend on a C malloc implementation (whose action is predetermined) and which is in turn very likely to depend on the system provided sbrk. Ultimately new/malloc failure depends on sbrk failure.
Yes, all those objections can be beaten, on a particular system. But one of the objectives of Pascal is portability. That is why we have standards such as ISO7185 and ISO10206, and why failure to observe them is very poor practice.
CBFalconer wrote:
Russell Whitaker wrote:
Got the following from "Borland Pascal with Objects - Language Guide"
function MaxAvail "Returns the size of the largest contiguous free block in the heap, indicating the size of the largest dynamic varible that can be allocated at the time of the call to MaxAvail."
function MemAvail "Returns the number of free bytes of heap storage available."
Also on another page there is a blurb on how to make New and GetMem return nil instead of aborting the program.
These are typical Borland kludges, that don't and can't port to modern systems with virtual memory.
I'd like to second this. Even if MaxAvail returns a size greater than what you want to allocate, a subsequent allocation is not guaranteed to succeed. (Note, even in the BP description, "at the time", not shortly afterwards ...)
These functions just do not make much sense on multitasking systems. ISTR even the FPC people, who are very close to Borland otherwise, discourage their use.
Checking an allocation aftwards is a better thing to do. Unfortunately, GPC doesn't yet offer the best possible capabilities to do that ...
Frank
At 5:05 +0200 24/7/05, Frank Heckenbach wrote:
Peter N Lewis wrote:
I'd love to start using New and Schema and such, but the best solution seems to be the Trap technique and that's some ugly code to have any place you want to use New and not crash out.
With a nil-return approach you'd really have to check each allocation, of course ...
True, although I'm pretty used to that, and if you are going to have anything other than "program dies on allocation failure" memory handling, then you have to have *some* code. The question is what is reasonable and simple and clear.
I think checking for nil before initialization would not be that hard to do in the compiler. The main drawback I see would be that it would add some code to all programs because the compiler can't know whether the allocator that's active at runtime may return nil. (And no, I don't want to add a compiler option. This would create quite an ugly mess of compile-time settings and runtime behaviour interdependence and just invite hard to find bugs.) OTOH, the check would, of course, only be necessary if the type needs initialization at all (which the compiler knows), so most allocations are not affected -- only files, schemata and objects ATM. So perhaps it's not that bad. Such things are not allocated and initializalized in tight inner loops usually, anyway.
It sounds reasonable to me. I'd even be happy with this applying only to the New as function case if that would help, ie:
New(s,57) gives a runtime error on failure, and so the compiler knows that it need not bother checking.
s := New(String,57) potentially returns nil, so the compiler checks.
I'm not sure if that is reasonable. It makes a certain kind of sense in some respects - New as a procedure has no return value and thus nothing to report, New as a function returns a pointer (even for reference objects anyway) and therefore can return a success/failure result.
And then there's a problem with allocation in the RTS. I don't think there are so many places, so I could probably take care of all of them and check for nil-returns. The question is, of course, what to do then. Unless there's a reasonable default behaviour that can be done without allocating memory (rarely, I'd suppose), I think the best and safest thing to do is raise a runtime error. And there we are again ...
If there is no way to return an error as an alternative to runtime failure, then runtime failure is the only option.
I checked and there are about 20 in units and about 15 in rts. Most look like they have not much choice but to runtime error, while some, like LocaleConv for example, look like they could easily pass the failure on up the line.
I guess by having the two forms of New (function and pointer) behave as described above, it might work quite well in that you can easily make the decision in the rts or in user code - if you don't want to (or cannot) deal with the error, then call New as a procedure; if you are willing to deal with the possible memory allocation failure, then call New a a function and honour the possibly nil result.
I did think of another hack, which was to pre-allocate a chunk of memory larger than the largest New command, and then have GetMem return that for failure, then rather than testing for nil, I could test of that safe return value. It's ugly, but probably the simplest solution that provides clean New code.
It's a kludge. I think it would work, but I wouldn't like to keep it permanently ... ;-)
True, but actually, way back in the days of the first Mac, when we had 128K memory, the heap manager had a concept of a "grow zone" which basically did just this. At the start, you allocated a spare chunk of memory, enough so that no critical systems would fail (eg putting up a dialog to tell the user that all hope was lost), and then if the heap manager was going to fail a memory request, if released this block of memory and retried the request, and then later in your main loop, you detected that the memory had been released and warned the user/saved files/gracefully quit/whatever. You still had to be careful for any big allocations (eg reading in a file), but you didn't have to be paranoid about every possible rts allocation.
So it is not an idea completely without precedent. Peter.
Peter N Lewis wrote:
... snip ...
New(s,57) gives a runtime error on failure, and so the compiler knows that it need not bother checking.
s := New(String,57) potentially returns nil, so the compiler checks.
I'm not sure if that is reasonable. It makes a certain kind of sense in some respects - New as a procedure has no return value and thus nothing to report, New as a function returns a pointer (even for reference objects anyway) and therefore can return a success/failure result.
To start with, new is a standard PROCEDURE, not a function. The constants following the argument specify variants and allow assignment of space for a particular variant of the actual type. You can only do the sort of thing you recommend if you override the level 0 predeclaration of new, which in turn makes memory allocation unavailable to the remainder of the program. In your example above string would have had to be declared similarly to:
TYPE string = RECORD ..... CASE integer OF 57: ( somevariant ); 22: ( othervariant); END;
which is one of the reasons case variants must be the last portions of records.
At 6:34 -0400 24/7/05, CBFalconer wrote:
Peter N Lewis wrote:
New(s,57) gives a runtime error on failure, and so the compiler knows that it need not bother checking.
s := New(String,57) potentially returns nil, so the compiler checks.
I'm not sure if that is reasonable. It makes a certain kind of sense in some respects - New as a procedure has no return value and thus nothing to report, New as a function returns a pointer (even for reference objects anyway) and therefore can return a success/failure result.
To start with, new is a standard PROCEDURE, not a function.
New (builtin) is both a procedure and a function. From the documentation:
New (Under construction.) Synopsis procedure New (var P: any Pointer ); or procedure New (var P: Pointer to a variant record; tag fields ); or procedure New (var P: Pointer to a schema; discriminants ); or procedure New (var P: Pointer to an object; constructor call ); or function New (any Pointer type ): same type ; or function New (variant record Pointer type ; tag fields ): same type ; or function New (schema Pointer type ; discriminants ): same type ; or function New (object Pointer type ; constructor call ): same type ;
This program compiles:
program peterT2;
type StringPtr = ^String; var s: StringPtr; begin New( s, 57 ); s := New( StringPtr, 57 ); end.
The constants following the argument specify variants and allow assignment of space for a particular variant of the actual type.
Yes, sort of. The constants can also be Schema Discrimenants, as in my example (except I should have used StringPtr as the type, not String).
You can only do the sort of thing you recommend if you override the level 0 predeclaration of new, which in turn makes memory
I'm not talking about overriding the New builtin. It is not possible to write a procedure with the semantics of the builtin New in Pascal (this has always seemed a weakness of Pascal to me, that it includes procedures like WriteLn and New that cannot actually be written in the language, but that is more a philosophical question).
Enjoy, Peter.
Peter N Lewis wrote:
At 6:34 -0400 24/7/05, CBFalconer wrote:
Peter N Lewis wrote:
New(s,57) gives a runtime error on failure, and so the compiler knows that it need not bother checking.
s := New(String,57) potentially returns nil, so the compiler checks.
I'm not sure if that is reasonable. It makes a certain kind of sense in some respects - New as a procedure has no return value and thus nothing to report, New as a function returns a pointer (even for reference objects anyway) and therefore can return a success/failure result.
To start with, new is a standard PROCEDURE, not a function.
New (builtin) is both a procedure and a function. From the documentation:
That is simply wrong. See section 6.7.5.3 of ISO 10206.
... snip ...
I'm not talking about overriding the New builtin. It is not possible to write a procedure with the semantics of the builtin New in Pascal (this has always seemed a weakness of Pascal to me, that it includes procedures like WriteLn and New that cannot actually be written in the language, but that is more a philosophical question).
I am sure you are talking about the apparent variadic nature of writeln, which does not exist. writeln is a simple function, with some compiler shortcuts applied. First, the compiler can recognize that the first parameter is not a file, and supply the identifier 'output'. That is the only parameter to writeln, the others are again the result of compiler shortcuts, specified in the standard.
Assuming the first parameter is not a file, so the default above applies, "writeln(a);" is expanded to read "write(a); writeln". This is coupled with the equivalent expansion of "write(a, b);" to "write(a); write(b);". There are equivalents for read and readln.
You are perfectly free to replace the functions read, write, readln, writeln with your own. Since the parameterization of those replacements is not known in advance, the handy default and expansion mechanisms can not be expected to apply.
No philosophy is required. The actual mechanism, and even the further expansion of read and write to use get, put, and f^, are clearly spelled out in the standard. The fact that Borland ignored these requirements, to no advantage, has caused great difficulty in the past and done great damage to the language.
Proper use of the standard mechanism allows almost anything to be attached to the file system with clear semantics. This includes such things as complete screens, LANs, pipes, etc. No need for so-called CRT units, although they may be convenient. I can speak with confidence because I did exactly that over 25 years ago. That does not, of course, mean that that file system code can necessarily be written in Pascal.
CBFalconer wrote:
I am sure you are talking about the apparent variadic nature of writeln, which does not exist. writeln is a simple function, with some compiler shortcuts applied. First, the compiler can recognize that the first parameter is not a file, and supply the identifier 'output'. That is the only parameter to writeln, the others are again the result of compiler shortcuts, specified in the standard.
Assuming the first parameter is not a file, so the default above applies, "writeln(a);" is expanded to read "write(a); writeln". This is coupled with the equivalent expansion of "write(a, b);" to "write(a); write(b);". There are equivalents for read and readln.
I agree here.
Proper use of the standard mechanism allows almost anything to be attached to the file system with clear semantics. This includes such things as complete screens, LANs, pipes, etc. No need for so-called CRT units, although they may be convenient.
I don't agree here. The Pascal file system only allows for sequential read or write access (ISO 7185), or record-based random access (ISO 10206). A CRT unit does much more than this.
To start with, coordinates are two-dimensional. Of course, you could compute a sequential offset yourself, but this doesn't exactly sound like a high-level language to me.
Then, there are "attributes" (e.g., character color). Of course, you could define a record containing attribute and character as fields and create a pseudo-file of those. That's not very convenient to use -- writing strings has to be done char by char. Also, while it can be efficient on direct memory-mapped graphics (i.e., basically, Dos), it becomes harder and less efficient on most other terminals (where the backend would have to compare attributes, generate control sequences etc.).
How do you handle the cursor position? Placing it at the current position as set by SeekWrite is (a) ISO 10206, and (b) not always useful as the cursor would jump too much.
Similarly, how would you handle automatic scrolling etc.? Writing the last (lower-right) character) should often not result in scrolling. Writing the next character would be logical, but conceptually be out of range.
If you want control over the cursor shape etc., you'd need quite some tricks to stick this into the file model.
On the input side, there's an essential difference to regular files. Regular files can be at EOF or not at EOF. In the first case, no data can be read until the file is reset (or similar) again. CRT input can also be waiting for input. Defining this state as EOF doesn't work. E.g., one could be in the middle of entering a number. EOF would mean the number is finished, but in fact the user might type more digits. This issue doesn't apply to CRT only, but also to other forms of interactive input such as pipes, networks etc. For pipes, e.g., the difference between EOF (pipe writing end closed) and waiting for data is very essential.
Furthermore, you'd have to encode function keys as char sequences. Actually, BP's CRT unit does this, but it's not really convenient when you want to handle function keys. Offering a larger type which can hold function keys and regular chars, as our CRT unit does, is really nicer to use.
Frank
Frank Heckenbach wrote:
CBFalconer wrote:
... snip ...
Proper use of the standard mechanism allows almost anything to be attached to the file system with clear semantics. This includes such things as complete screens, LANs, pipes, etc. No need for so-called CRT units, although they may be convenient.
I don't agree here. The Pascal file system only allows for sequential read or write access (ISO 7185), or record-based random access (ISO 10206). A CRT unit does much more than this.
To start with, coordinates are two-dimensional. Of course, you could compute a sequential offset yourself, but this doesn't exactly sound like a high-level language to me.
Just as an example, one of my systems in bygone years did screen updates. The file was defined as a FILE OF RECORD .... where the record included a 24x80 array of char and a "cursorlocation" x y pair. The main program could examine and alter these fields in f^ as it desired, and then execute "put(f)", which did the actual transmission.
There was then no difference in the code running an external terminal on a serial line, and that running a memory mapped display. There was a great difference in the drivers, which were mapped into the actual file system via a set of tables for such things as open, close, put, get, status. The actual operation could be asynchronous, and handled by interrupts in a serial drive, none of which complication was needed in a local memory driver. That meant a difference in throughput to the actual program, but the apparent speed was virtually unaffected.
If significant variations in the end device existed, they could be reported by invariant fields in f^, such as MAXX and MAXY. After a put to the serial device completion could be detected by examining a 'busy' field. Etc. The details are unimportant, the point is that all this can be mapped into the standard Pascal file system.
CBFalconer wrote:
To start with, coordinates are two-dimensional. Of course, you could compute a sequential offset yourself, but this doesn't exactly sound like a high-level language to me.
Just as an example, one of my systems in bygone years did screen updates. The file was defined as a FILE OF RECORD .... where the record included a 24x80 array of char and a "cursorlocation" x y pair. The main program could examine and alter these fields in f^ as it desired, and then execute "put(f)", which did the actual transmission.
There was then no difference in the code running an external terminal on a serial line, and that running a memory mapped display. There was a great difference in the drivers, which were mapped into the actual file system via a set of tables for such things as open, close, put, get, status.
Close and status (what does this do?) are non-standard, BTW ...
As I said, this sounds rather inefficient to me. The driver would have to partly undo your work (you want to write a string, you map it to character writes, possibly in different lines, perhaps interspersed with attributes, then the driver recoginizes sequential writes, with the same attributes if applicable, and turns it back into a sequential string output.
Another factor is that non-memory mapped I/O behaves more nicely if buffered. Some things are easier to program when you can overwrite characters with the user only seeing the end result (e.g., clearing a window, drawing a border, printing a title over the border ...) EP has a record-granularity `Update' routine, but nothing on a bigger scale.
The actual operation could be asynchronous, and handled by interrupts in a serial drive, none of which complication was needed in a local memory driver. That meant a difference in throughput to the actual program, but the apparent speed was virtually unaffected.
If significant variations in the end device existed, they could be reported by invariant fields in f^, such as MAXX and MAXY.
So every program would first have to read, and possibly store these values (unless it's hard-coded to 24x80 which I hope isn't the case), and then do manual offset computations? Sorry, but that's what I'd expect to do in assembler code, not even in C, let alone Pascal ...
After a put to the serial device completion could be detected by examining a 'busy' field.
Which can require a busy-waiting loop if you want to wait for it. In some situations one needs something more advanced, such as select/poll in Unix. Unfortunately, standard Pascal I/O doesn't provide any means for this, so we need some extensions.
Etc. The details are unimportant, the point is that all this can be mapped into the standard Pascal file system.
Almost anything can be mapped to anything (Turing machines etc.), but I'm not convinced it's very useful ...
Frank
Frank Heckenbach wrote:
CBFalconer wrote:
... snip ...
Just as an example, one of my systems in bygone years did screen updates. The file was defined as a FILE OF RECORD .... where the record included a 24x80 array of char and a "cursorlocation" x y pair. The main program could examine and alter these fields in f^ as it desired, and then execute "put(f)", which did the actual transmission.
There was then no difference in the code running an external terminal on a serial line, and that running a memory mapped display. There was a great difference in the drivers, which were mapped into the actual file system via a set of tables for such things as open, close, put, get, status.
Close and status (what does this do?) are non-standard, BTW ...
Close is automagically called when the file variable goes out of scope. Status allows the runtime system to implement interrupt driven code. Just as examples. open is called via the reset and/or rewrite functions.
As I said, this sounds rather inefficient to me. The driver would have to partly undo your work (you want to write a string, you map it to character writes, possibly in different lines, perhaps interspersed with attributes, then the driver recoginizes sequential writes, with the same attributes if applicable, and turns it back into a sequential string output.
Depends on what you want. The point is that the Pascal file system has the capability of mapping many things in a system independant manner.
CBFalconer wrote:
As I said, this sounds rather inefficient to me. The driver would have to partly undo your work (you want to write a string, you map it to character writes, possibly in different lines, perhaps interspersed with attributes, then the driver recoginizes sequential writes, with the same attributes if applicable, and turns it back into a sequential string output.
Depends on what you want. The point is that the Pascal file system has the capability of mapping many things in a system independant manner.
I don't deny that it has the capability, but IMHO this doesn't mean much, and it's not always a good solution. I guess we have to agree to disagreee here.
Frank
At 10:08 -0400 24/7/05, CBFalconer wrote:
Peter N Lewis wrote:
At 6:34 -0400 24/7/05, CBFalconer wrote:
To start with, new is a standard PROCEDURE, not a function.
New (builtin) is both a procedure and a function. From the documentation:
That is simply wrong. See section 6.7.5.3 of ISO 10206.
If you wish to contradict my statements when I quote both the GPC documentation, and a sample program that GPC compiles, you are free to do so but I cannot see it being overly helpful.
I'm not talking about overriding the New builtin. It is not possible to write a procedure with the semantics of the builtin New in Pascal (this has always seemed a weakness of Pascal to me, that it includes procedures like WriteLn and New that cannot actually be written in the language, but that is more a philosophical question).
I am sure you are talking about the apparent variadic nature of writeln, which does not exist.
My statement is that Pascal includes procedures like New and WriteLn that cannot be themselves written in Pascal, and that I consider this a weakness. There are three parts to that statement:
1. Pascal includes procedures like New and WriteLn. 2. Procedures like New and WriteLn cannot be written in Pascal. 3. I consider this a weakness.
It seems you have chosen to argue against 2. Now perhaps it is unclear as to what I mean by "like", but I would have thought it was fairly clear that a procedure "like" New would be one that could take any of the possible inputs New can take (same syntactic behaviour) and produce the same result (same semantic behavior).
Clearly you can write code that has the same semantic behaviour as WriteLn by expanding it sufficiently into multiple calls to multiple different procedures for each possible type. But that is not "like" WriteLn in that it is not even close to syntactically the same.
You can also write code that has the same semantic behaviour as New(StringPtr,57), as shown by Frank in one of his very helpful answers on how to pack strings together. But again you'd have to write a procedure for each possible type.
I would contend, with a definition of "like" that is "has the same (or even close) syntactic behaviour and same semantic behaviour", that points 1 and 2 are undisputable. Point 3 is clearly undisputable - subject to change perhaps, but undisputable.
No philosophy is required.
The philosophy I refer to is that things that look like functions or procedures in the language should be possible to be written in the language. I consider this a worthy goal and hence a weakness in the language. The problem can be solved in a number of ways:
1. Support types as first class objects and support variable numbers of parameters. 2. Dont support procedures that cannot be written in the language.
Neither of these are standard Pascal of course, because as I said, this is a philosophical point. It is about language design, not Pascal in particular, except for my consideration that this is a weakness in Pascal, for which each person can have their own opinion.
Clearly I don't believe that 2 would be an improvement to Pascal, as the reason for my bringing up this topic is to try to find a way where I can safely use New (and hence get all of the nice Schema advantages with simplicity) in cases where memory allocation might fail.
As for 1, it is well outside the scope of Pascal. Certainly, it could be added to Pascal, and its addition, if well thought out, could make for a benefit to programmers, but obviously I'm not suggesting we rush off and do this. Indeed, I cannot see ever doing this with Pascal.
That leaves Pascal, as it is, with its weaknesses intact. I still consider Pascal to be about the best programming language there is, but that does not mean I don't consider some of its design decisions to be less than optimal. Peter.
Peter N Lewis wrote:
Clearly you can write code that has the same semantic behaviour as WriteLn by expanding it sufficiently into multiple calls to multiple different procedures for each possible type. But that is not "like" WriteLn in that it is not even close to syntactically the same.
In fact it's only syntactically different. WriteLn (with several arguments) is just a syntactic shortcut, as defined in the standards. Semantically it's the same as a series of simple procedure calls.
The philosophy I refer to is that things that look like functions or procedures in the language should be possible to be written in the language.
I consider this a worthy goal and hence a weakness in the language. The problem can be solved in a number of ways:
- Support types as first class objects and support variable numbers
of parameters.
That's *not* what WriteLn is. It's a common misunderstanding which Chuck tried to rectify. This may be the core of the disagreement here.
The built-in WriteLn is not a magical procedure that takes any number of arguments, plus type information ("types as first class objects") and acts on these.
Though a compiler could implement it like this if it's equivalent to the real semantics, which is not quite easy to achieve due to some hairy details. Also, this approach is generally likely to be less efficient than the standard way. (I should know as GPC tried to do this until last year, and indeed, it failed on some of those hairy details, and it had a little runtime overhead for dispatching, as well as some rather hairy runtime code.)
Sure, a complete, type-safe, variadic routine implementation could emulate WriteLn, but in a less efficient way and with much, much more effort, both for the language designer, the implementor and the end-user. So, when this is usually implied by asking for being able to reimplement "something like WriteLn", this would be huge overkill for lesser results.
To actually do something like WriteLn's standard semantics, one would need a way to specify those in a program. I have seen no real suggestion how this could even look like. These would be things like:
: For n>=1, write(f,p 1 ,...,p n ) shall access the textfile and establish a : reference to that textfile for the remaining execution of the statement. : For n>=2, the execution of the statement shall be equivalent to : : begin write(ff,p 1 ); write(ff,p 2 ,...,p n ) end : : where ff denotes the referenced textfile.
: Writeln(f,p 1 ,...,p n ) shall access the textfile and establish a : reference to that textfile for the remaining execution of the statement. : The execution of the statement shall be equivalent to : : begin write(ff,p 1 ,...,p n ); writeln(ff) end : : where ff denotes the referenced textfile.
The closest thing to these I remember having seen would be variadic macros in the GCC C preprocessor (maybe also in standard C meanwhile). Though not quite the same, because of usual macro issues such as quoting and multiple evaluation, something in this direction might be a way to actually implement something *like* WriteLn in user code.
Not that I have any intentions to add this in GPC.
BTW, IME, the very large majority of possible applications for requested "varargs" are one of these groups:
- A finite number of cases, which can also be solved with overloading, sometimes even with default parameter values.
- Any number of arguments of the same type (e.g., `Concat' for n string values). Actually, rather rare; can't remember another example right now.
- Yet another form of Write[Ln] or Read[Ln] (the latter much rarer). Such routines are somewhat common in C code (often for error reporting etc.). Usually, though, this boils down to passing the arguments around, ultimately to a predefined routine such as [f|s]printf.
In Pascal we can get a very similar effect by using the standard Write[Ln] routine and (possibly) redefining the output of a (pseudo-)file. (In this case I concur with this suggestion of Chuck's.) Both GPC and BP, and probably other dialects, provide some mechanisms (though slightly different) to do this, e.g. called TFDD (text file device drivers -- not to be confused with OS drivers). Another possibility in Extended Pascal is to use WriteStr and then pass a simple string around.
In both cases, we simply use the special syntax of Write[Ln|Str], also including the `: Width' specifiers which I'm leaving out of the discussion otherwiese for now, and just redefine the target of the text, which is actually the purpose. On the target side, things are much simpler, since the text is already formatted, so number and type of arguments don't matter anymore.
Therefore, I don't think it's reasonable to add a huge framework, just to be able to define one's own WriteLn-(un)like procedure which in the end just passes its arguments on to the real WriteLn ...
- Dont support procedures that cannot be written in the language.
That would be a purist approach (IMHO overly so). C did so with varargs, leading to several weaknesses (again, IMHO) in turn: Varargs functions, predefined as well as user-defined ones, are not type-safe, and printf etc. are more clumsy and dangerous to use for that reason and in principle somewhat less efficient because of runtime parsing.
I've seen other overly purist approaches (such as OO languages where everything, including simple numbers, are objects). IMHO, these may be interesting concepts, but not so useful in practice. So I think Wirth chose a good compromise here.
That leaves Pascal, as it is, with its weaknesses intact. I still consider Pascal to be about the best programming language there is, but that does not mean I don't consider some of its design decisions to be less than optimal.
I agree with the general statement, but not WRT WriteLn (more to things such as lack of `endif' and a few other issues) ...
Frank
CBFalconer wrote:
Peter N Lewis wrote:
... snip ...
New(s,57) gives a runtime error on failure, and so the compiler knows that it need not bother checking.
s := New(String,57) potentially returns nil, so the compiler checks.
I'm not sure if that is reasonable. It makes a certain kind of sense in some respects - New as a procedure has no return value and thus nothing to report, New as a function returns a pointer (even for reference objects anyway) and therefore can return a success/failure result.
To start with, new is a standard PROCEDURE, not a function. The constants following the argument specify variants and allow assignment of space for a particular variant of the actual type. You can only do the sort of thing you recommend if you override the level 0 predeclaration of new, which in turn makes memory allocation unavailable to the remainder of the program.
There are two issues here:
1. `New' applied to schemata, including `String', is an ISO 10206 standard feature. (It uses a similar syntax as `New' for variant records, but there are no conflicts.) Of all the various suggestions to allocate variables with dynamic size at runtime, that's the cleanest one I know. Please note that ISO 7185 has no way to do this at all. It has conformant array parameters (in level 1), but in the (ultimate) caller, the arrays have to have a compile-time known size. Do you suggest abandoning dynamically-sized types completely?
2. `New' as a function is a GPC (and in other contexts, namely objects, also a BP) extension. That's basically just syntactic sugar that doesn't add a lot, but also doesn't hurt much ...
If you want to criticize nonstandard features (2.) so harshly, you might want to attention not to attack standard features (1.) as well ...
Frank
Frank Heckenbach wrote:
CBFalconer wrote:
Peter N Lewis wrote:
... snip ...
New(s,57) gives a runtime error on failure, and so the compiler knows that it need not bother checking.
s := New(String,57) potentially returns nil, so the compiler checks.
I'm not sure if that is reasonable. It makes a certain kind of sense in some respects - New as a procedure has no return value and thus nothing to report, New as a function returns a pointer (even for reference objects anyway) and therefore can return a success/failure result.
To start with, new is a standard PROCEDURE, not a function. The constants following the argument specify variants and allow assignment of space for a particular variant of the actual type. You can only do the sort of thing you recommend if you override the level 0 predeclaration of new, which in turn makes memory allocation unavailable to the remainder of the program.
There are two issues here:
`New' applied to schemata, including `String', is an ISO 10206 standard feature. (It uses a similar syntax as `New' for variant records, but there are no conflicts.) Of all the various suggestions to allocate variables with dynamic size at runtime, that's the cleanest one I know. Please note that ISO 7185 has no way to do this at all. It has conformant array parameters (in level 1), but in the (ultimate) caller, the arrays have to have a compile-time known size. Do you suggest abandoning dynamically-sized types completely?
`New' as a function is a GPC (and in other contexts, namely objects, also a BP) extension. That's basically just syntactic sugar that doesn't add a lot, but also doesn't hurt much ...
If you want to criticize nonstandard features (2.) so harshly, you might want to attention not to attack standard features (1.) as well
I have to plead ignorance here. So I withdraw my objection to (1.) and hope to remember it in the future. I still consider (2.) a wide open gateway for errors. I am atavistic enough to consider the Modula typed procedures a vast step backward :-)
CBFalconer wrote:
Peter N Lewis wrote:
... snip ...
New(s,57) gives a runtime error on failure, and so the compiler knows that it need not bother checking.
s := New(String,57) potentially returns nil, so the compiler checks.
- `New' as a function is a GPC (and in other contexts, namely objects, also a BP) extension. That's basically just syntactic sugar that doesn't add a lot, but also doesn't hurt much ...
Ahh. So since New is a GPC extension, as well as a BP extension where it looks like New can return nil, perhaps it is quiet reasonable to allow New as a function to return nil? I would have no problem using New as a function when I wanted to ensure I could defend against memory allocation failures. Peter.