[HN Gopher] Features of PL/I not realized in a modern language
       ___________________________________________________________________
        
       Features of PL/I not realized in a modern language
        
       Author : vitplister
       Score  : 44 points
       Date   : 2021-11-26 17:10 UTC (5 hours ago)
        
 (HTM) web link (minnie.tuhs.org)
 (TXT) w3m dump (minnie.tuhs.org)
        
       | throwawayboise wrote:
       | Used PL/1 in one of my first programming classes at university.
       | Never before or since, but I don't recall it being a bad language
       | to work in.
        
         | jrd259 wrote:
         | PL/1 was the systems programming language for Multics.
         | Certainly not a bad language to work with.
        
           | pjmlp wrote:
           | And plenty of other systems as well.
        
         | reidacdc wrote:
         | PL/I was the software-engineering language in my undergraduate
         | computer science classes also, although it was beginning to be
         | displaced by Pascal in the introductory courses, and C/C++ in
         | the advanced courses. (Mid-1980s, in case that is not already
         | clear.) It hung on because, we were told, it was still in
         | demand in industry, particularly at telephone companies.
         | 
         | I did a group project with it, and the language was large
         | enough that we found that each member of the group seemed to
         | have learned a slightly different subset of it.
        
           | throwawayboise wrote:
           | Similar timeframe. Used PL/1, Pascal, and Modula-2 in three
           | successive semesters IIRC.
        
       | manbart wrote:
       | I think 2,3,5 and 7 apply to Rexx as well, another IBM created
       | language. I wouldn't call it modern however
        
         | npsimons wrote:
         | > I wouldn't call it modern however
         | 
         | I wonder how the claim stands up if we remove that "modern"
         | adjective. Or perhaps consider that "modern" languages don't
         | have some of these features as they turned out to be "harmful"
         | (see Dijkstra's "GOTO considered harmful") - at least one reply
         | in that thread calls out one of these "missing features" as
         | very dangerous, sort of like how some C++ projects will forbid
         | features (private inheritance, etc) for ease of maintenance.
        
       | adrian_b wrote:
       | There are many other features of PL/I besides those mentioned
       | there, which are not encountered in modern languages.
       | 
       | Because PL/I was designed by IBM for their computers and
       | operating systems, there were no concerns for portability, so the
       | language definition included a large amount of features that are
       | left unspecified in standards for languages like C/C++, because
       | they are available only for certain operating systems or
       | computers, so the standard must be restricted to specify only the
       | minimum features that are available everywhere.
       | 
       | For example the C++ threads are limited only to the features
       | provided by the POSIX pthreads, even if most operating systems
       | have additional features for multi-threading. PL/I on the other
       | hand, already in 1965 had more powerful features than the C++
       | threads.
       | 
       | One feature of PL/I that is completely absent in all modern
       | languages is what I consider to be the right way for error
       | handling.
       | 
       | Instead of being forced to test the return value of a function to
       | determine what error might have happened, which is a redundant
       | operation, as this was already tested once in the invoked
       | function, and which clutters the code, obscuring the processing
       | of the normal path, the PL/I method was to group the error
       | handlers at the end and pass the labels of the error handlers to
       | the invoked function as alternative return labels.
       | 
       | While this looks visually very similar to a _try_ block with
       | exception handlers, the hardware implementation in PL /I is far
       | more efficient than in modern languages, because there are no
       | conditional branches and no extra code to determine where is the
       | appropriate error handler, just the return, which is done anyway
       | when the invoked function finishes, jumps to different addresses
       | in case of errors, instead of returning to the point of
       | invocation.
       | 
       | PL/I also had exceptions, but those were used for really
       | exceptional conditions, like numeric overflow, illegal memory
       | accesses etc., not for correctable error conditions, like a
       | mistyped user input or a file that cannot be found, which are
       | best handled at the point where a function has been invoked,
       | because only there you have complete information about the
       | intention of the failed operation.
        
         | p_l wrote:
         | PL/I model was inspiration for Common Lisp condition system,
         | which handles both recoverable (continuable) and non-
         | recoverable conditions (akin to exceptions). Continuable
         | exceptions are also a thing in Windows NT, though rarely seen
         | in practice.
         | 
         | All of those hark back to PL/I error handling, possibly through
         | Project MAC (which was probably bigger PL/I user than anything
         | from IBM for a time)
        
         | PaulDavisThe1st wrote:
         | > For example the C++ threads are limited only to the features
         | provided by the POSIX pthreads, even if most operating systems
         | have additional features for multi-threading.
         | 
         | What facilities are you thinking of?
         | 
         | > PL/I on the other hand, already in 1965 had more powerful
         | features than the C++ threads.
         | 
         | Given that the term "threads" originates in the 65/66/67 era,
         | this is an interesting claim.
        
           | adrian_b wrote:
           | The term "thread" has become fashionable only after 1990.
           | 
           | Nobody used the terms "thread" or "multi-threading" when PL/I
           | was designed. Everybody used the terms "task" and "multi-
           | tasking", with the same meaning.
           | 
           | So in PL/I you have "tasks" for what are now called
           | "threads".
           | 
           | The most annoying feature of the C++ thread support library,
           | which is inherited from the POSIX pthreads specification, is
           | the pathetic operation _join_ which waits for the termination
           | of a designated thread.
           | 
           | This is almost never what you want. The most frequent case is
           | to wait for the termination of anyone of a group of threads,
           | which cannot be done with _join_.
           | 
           | Already in 1965, PL/I had a _wait_ that could do anything
           | that can be done with _WaitForMultipleEvents
           | /WaitForSingleEvent_, i.e. it could wait for any or all of
           | any combinations of a set of events, e.g. thread
           | terminations.
           | 
           | This is such a basic feature that I cannot understand how it
           | could have been omitted from the POSIX pthreads and in
           | consequence also from the C++ thread support library. The
           | other features of pthreads/C++ that can be used to implement
           | such a functionality, e.g. condition variables, also suck
           | badly in comparison with a good _wait_ for events.
        
             | spacechild1 wrote:
             | > This is almost never what you want. The most frequent
             | case is to wait for the termination of anyone of a group of
             | threads, which cannot be done with join.
             | 
             | I also have to say that this doesn't match my experience at
             | all. Typically, my threads run for the whole duration of
             | the program/object and I only need to join them when the
             | program/object is done. In fact, this pattern is so common
             | that C++20 added std::jthread which automatically joins the
             | thread in the destructor.
             | 
             | I literally never had to wait for "the termination of
             | anyone of a group of threads". For you it's apparently the
             | most frequent case. This only teaches us not to make too
             | broad assumptions. The world of programming is large.
        
             | PaulDavisThe1st wrote:
             | It's clear that you know a lot about this stuff, but I
             | think maybe not quite enough.
             | 
             | The Linux kernel retains the term "task" for "execution
             | context", and the term "thread" is used exclusively in user
             | space purely because of programmer conventions (pthreads,
             | specifically). This makes a limited amount of of sense
             | because "thread" has also come to mean things that are very
             | definitely not full execution tasks (user-space and/or so-
             | called "green" threads). This overloading of the
             | terminology in user-space is not reflected in kernels, but
             | allows for developers to play with alternate
             | implementations without changing their fundamental
             | terminology. After all, there some things where user-space
             | threads are precisely what you want (and these absolutely
             | do not correspond to kernel tasks).
             | 
             | > This is almost never what you want.
             | 
             | I would beg to differ. I've been writing multithreaded code
             | in C++ for nearly 30 years, and I don't remember any time
             | that I wanted to join on a group of threads. My current
             | project (20 years old) is heavily multithreaded (typically
             | 20-60 threads) and there is nowhere that a thread primitive
             | to join on a group of threads would make our lives easier.
             | The only context where I could even think of using it might
             | be to wait on a thread pool, but in general this tends to
             | be unnecessary and if it is, can be trivially handled
             | without a multi-join.
             | 
             | > Already in 1965, PL/I had a wait that could do anything
             | that can be done with
             | WaitForMultipleEvents/WaitForSingleEvent,
             | 
             | WaitForMultipleEvents/WaitForSingleEvent are not part of
             | the C or C++ language, but Windows system calls. While
             | their semantics are very powerful and useful (which
             | obviously would thus apply to PL/1's _wait_ ), their
             | absence cannot be laid at the feet of C or C++: no Unix-
             | like OS has had this operation, ever (it is almost possible
             | with contemporary Linux).
             | 
             | You could make the argument that it's not part of pthreads
             | because this operation was never a part of posix, and so
             | pthreads could not build on it (you can use those calls on
             | Windows even in a single-threaded task, and that would be
             | true in a POSIX system if the call existed). So it really
             | has almost nothing to do with pthreads at all, other than
             | that one could imagine a pthread wanting to block on "wait-
             | for-something-anything".
        
               | monocasa wrote:
               | Linux exports the concept of threads in it's user facing
               | ABI. You can see this in "Thread Group ID" and "Thread
               | ID" values.
        
               | adrian_b wrote:
               | > The Linux kernel retains the term "task" for "execution
               | context"
               | 
               | This has historical roots from the first UNIX versions
               | written for DEC computers. Even if the UNIX authors
               | usually preferred the term "process", in the DEC
               | documentation for their computers and operating systems
               | the term "task" was always used for "process". So the
               | term was also used in UNIX in various places.
               | 
               | > I don't remember any time that I wanted to join on a
               | group of threads
               | 
               | This is not surprising, because there are many kinds of
               | multi-threaded applications and many styles of
               | programming them.
               | 
               | I do not doubt that what you say is correct for you
               | applications, but my experience happened to be opposite.
               | I have never encountered a case when I wanted to join a
               | single thread, but I have encountered a lot of cases when
               | I wanted to join any of a group of threads (e.g. for
               | keeping a number of active threads matching the number of
               | available cores) and also a less number of cases when I
               | wanted to join all of a group of threads, typically at
               | the end of an operation. The latter case is less
               | important, because it can be done by repeating a _join_
               | with a single thread, even if that is much less efficient
               | than a _wait_ that waits for all.
               | 
               | > are not part of the C or C++ language, but Windows
               | system calls
               | 
               | This is precisely what I have already said in my first
               | post, i.e. that standardized languages like C/C++ are
               | forced to specify only the minimal features that are
               | available on all operating systems, so they cannot
               | include _WaitForMultipleEvents_ , while PL/I was free of
               | such portability concerns, so it could specify more
               | powerful features.
        
               | PaulDavisThe1st wrote:
               | > This is precisely what I have already said in my first
               | post, i.e. that standardized languages like C/C++ are
               | forced to specify only the minimal features that are
               | available on all operating systems, so they cannot
               | include WaitForMultipleEvents, while PL/I was free of
               | such portability concerns,
               | 
               | I think you missed my point. WaitForMultipleEvents is not
               | part of a thread API on any platform. It's a part of the
               | platform API, and is used by single-threaded and multi-
               | threaded code. There's no reason for pthreads (or any
               | other thread API) to represent this system call, because
               | the system call either exists, and can be used directly,
               | or does not exist, and cannot be used.
               | 
               | In essence, you're really just noting that POSIX (not
               | pthreads) never had a wait-for-just-about-anything API.
               | That's a legitimate complaint, just not very relevant for
               | multithreaded programming.
               | 
               | > This is not surprising
               | 
               | Well, given that you said _" This is almost never what
               | you want._", I'd count it as least a little surprising.
               | My point was that multi-join is not "almost never what
               | you want", but has always been "useful in certain
               | contexts". I have never come across a multi-join API that
               | blocks until all threads have completed (they typically
               | return when any of the specified threads completes), and
               | so the difference in efficiency for this version of
               | multi-join is essentially identical to a loop+single-
               | join.
               | 
               | >This has historical roots from the first UNIX versions
               | written for DEC computers.
               | 
               | I don't see much evidence for this claim. task_t exists
               | in early versions of AIX and Mach, and the terminology
               | was already common in Multics (as you know). I don't
               | think that Linux' use of task_t has any relationship to
               | the Ultrix use, but maybe you have some specific insight
               | here?
        
         | gnufx wrote:
         | Is the PL/I alternate return fundamentally different from
         | Fortran's (or FORTRAN's in those days)?
        
         | pjc50 wrote:
         | > the PL/I method was to group the error handlers at the end
         | and pass the labels of the error handlers to the invoked
         | function as alternative return labels.
         | 
         | The idea of an alternative return address - that is, allow the
         | calling convention to pass more than on return instruction
         | pointer - seems both simple to implement and really useful. I
         | guess in the modern gotoless world the trouble is structuring
         | the syntax around the call.
        
           | drran wrote:
           | It depends on which part of code will clear stack after call.
           | If caller is responsible to clear stack, like in C, to
           | support variable arguments, then called function must return
           | to the original point of call.
           | 
           | If called function is responsible to clear stack, like in
           | Pascal or Forth, then yep, multiple return points are easy to
           | implement and useful to implement something like switch
           | operator. I used it in Pascal, when I was in school. Just pop
           | old return address from stack and push new one.
        
             | adrian_b wrote:
             | You are right, but the ancient _cdecl_ function call
             | convention used by C for variable argument lists is really
             | obsolete and it should never be used in any new programming
             | language implementation, even if it has survived until
             | today in various ABI specifications.
             | 
             | Like for the C null-terminated strings, there are many
             | alternative ways to implement variable argument lists and
             | all of them have already been used many years before the
             | first C compilers, e.g. passing a hidden parameter count or
             | implementing transparently for the programmer any _printf_
             | -like functions as _vprintf_ -like functions.
             | 
             | With the alternative implementations, the stack should
             | always be freed in the invoked function, which also enables
             | various other important features, e.g. tail-call
             | optimization.
        
           | adrian_b wrote:
           | The alternative return addresses were declared in the
           | function prototype as parameters having the type _label_.
           | 
           | Then you just passed them during function call, e.g. "handle
           | = open_file(input_file_name, open_flags,
           | FailedToOpenInputFile);"
           | 
           | The error handlers at the end of the body of the invoking
           | function were just labelled appropriately.
           | 
           | Writing a label like _FailedToOpenInputFile:_ before an error
           | handler is certainly much simpler and more obvious than
           | strange workarounds for exception identification, like
           | inventing a dedicated data type for each possible error, as
           | in C++.
        
       | ptx wrote:
       | > _4. A named array is normally passed by reference, as in F(A).
       | But if the argument is not a bare name, as in F((A)), it is
       | passed by value._
       | 
       | Visual Basic (VB6/VBA) does exactly this, with the same syntax
       | (except without the outer parentheses if the call is a
       | statement).
        
         | kergonath wrote:
         | Fortran does it as well, I believe. The reason is that (A) is
         | an expression whose value is that of A, and not a variable.
         | This includes Fortran 2018; I am not arguing whether this is
         | modern or not.
        
           | gnufx wrote:
           | Fortran actually passes by value-return, or whatever you call
           | it, not by reference. The call may still be compiled by
           | reference, of course, and it used to be fun to change the
           | value of a number in the days when it wasn't in read-only
           | storage.
        
           | throwawayboise wrote:
           | It seems quite reasonable and intuitive to me.
        
       | bigbillheck wrote:
       | > 7. Astonishingly complete set of implicit data conversions.
       | E.g. if X is floating-point and S is a string, the assignment X =
       | S works when S = "2" and raises an exception (not PL/I
       | terminology) when S = "A".
       | 
       | This seems like an extraordinarily bad idea.
        
         | layer8 wrote:
         | It's like implicit numeric conversions in C, just extended to
         | string types.
        
           | zokier wrote:
           | Its not like the implicit numeric conversions in C were
           | particularly good idea either
        
             | emmelaich wrote:
             | It was a mistake in C++ too, fixed with the _explicit_
             | keyword.
        
         | jrd259 wrote:
         | Perhaps a bad idea, but at least it was rigorously specified. I
         | note that that Javascript and perl have same ability. See also
         | https://www.destroyallsoftware.com/talks/wat
        
           | layer8 wrote:
           | Javascript and Perl are different in that they allow a
           | variable to change it's type by reassignment (i.e. they would
           | allow the X = S assignment for S == "A"), which PL/1
           | apparently doesn't. Weak/strong typing and dynamic/static
           | typing are different axes.
        
         | drfuchs wrote:
         | And all the implicit conversion rules between pairs of types
         | couldn't fit on a single-page chart; the official IBM manual
         | included an honest-to-goodness fold-out bound right in. Fun
         | times.
        
         | ptx wrote:
         | It does. It sounds like the Unicode handling in Python 2, where
         | your program would still appear to work if it confused bytes
         | with codepoints (through implicit decoding/encoding), until you
         | get a non-ASCII input somewhere and everything blows up with a
         | UnicodeDecodeError somewhere far from where the problem is.
        
       | npsimons wrote:
       | Upvoted simply out of controversy and curiosity.
       | 
       | It's been a long time, and I've not delved deeply in to a wide
       | swath of languages (recently), but surely the features he's
       | citing are in other languages? Hell, even the list of "features
       | only Lisp has" keeps shrinking by the decade. Some of these PL/I
       | "features" feel like things you could trivially implement in a
       | library in other languages (ie, not have to implement half of
       | PL/I to get those features). Some of it sounds simply like
       | syntactic sugar, and not actual features.
       | 
       | What am I missing here?
        
       | Someone wrote:
       | Another feature: keywords aren't reserved words. For an example
       | of where that can lead to see https://multicians.org/proc-
       | proc.html
       | 
       | The thinking behind that was that the set of keywords would grow
       | over time, and that you couldn't expect any programmer to know
       | all of them.
       | 
       | There likely also are few programmers who do know all of them, as
       | there are hundreds (see
       | https://www.kednos.com/pli/docs/reference_manual/6291pro_042...
       | for those of one implementation)
        
         | curtisf wrote:
         | Java has been using this approach with new reserved identifiers
         | like `var` and `record` in the interest of backwards
         | compatibility.
         | 
         | JavaScript has done something similar with its `await`, `let`,
         | etc
        
       | teddyh wrote:
       | Point 7 simply means that PL/I is very weakly typed. However,
       | weak typing seems to now be unused in all modern languages;
       | strong typing seems to be the current norm. (Not to be confused
       | with static/dynamic typing, which is orthogonal.)
        
       | the_only_law wrote:
       | > Bit strings of arbitrary length, with bitwise Boolean
       | operations plus substr and catenation
       | 
       | This one actually piqued my interest in PL/I recently. Closest
       | thing I've seen in modern languages is bitstrings in the BEAM.
        
         | p_l wrote:
         | I believe both Ada and Common Lisp also share this ability,
         | though Ada has nice support in its type system for doing things
         | like mapping a portion of mmio register as bitstring that gets
         | further defined as new type (whether enum, or integer, or
         | string etc.)
        
         | throwawayboise wrote:
         | Yes I also thought of Erlang bitstrings when I read that.
        
       ___________________________________________________________________
       (page generated 2021-11-26 23:01 UTC)