Wednesday, April 19, 2006

An Introduction to Programming Languages.bku.yang摘引



previous latest addition here








Introduction




Target Audience



This is an attempt to summarize some of the basic ideas behind
programming languages. It was originally written for "people who
know one language and are wondering about learning another", and
hasn't strayed far from that aim.



It is not a detailed, scholarly exploration of all
programming languages, nor does it describe the latest developments
(or even cover all the basics) in computer science - I simply do not
know enough to attempt that. Instead, I have tried to write something
that is clear, unbiased, and useful to someone with no knowledge of
the theory of computing.



If you're not that interested in actually writing code and are
looking for a more general article, focusing on the "philosophical"
aspects of programming languages, then you will be much happier with this book review.



Initial Questions



  • Why are there so many different programming languages?

  • What are the differences between them?

  • What are the advantages of particular languages?

  • Can a single language have all the "good bits" of other
    languages?

  • Do some "good bits" force a language to also have "bad bits"?

  • What makes a feature good or bad?

  • Is there a perfect language?

  • Is there a perfect language for a particular task?

  • What changes can we expect in the near future?



Route Map



I don't try to answer the questions above directly. Instead I look at
the various different (and rather arbitrary) families of
languages, discuss their main features, and then try to compare them
with others.


The examples I give focus on the languages I know best, but I try to
cover a wide range of different topics.


Finally, I include some links so that you can continue your own research.



Disclaimer



Be warned: I am no expert. I don't have a formal education in
computing, so this is restricted to what I have picked up "along the
way". The links at the end give other sources of information. If you
spot an error, or have an improvement, please contact me. Thanks.


I'm currently (late 2004) editing and revising this document in an
attempt to improve it a little. Please bear with me.






Language Features




Introduction



This section looks at different "features" or "tricks" that languages
provide. Presenting them in this way is misleading, because it can give
the impression that language design involves selecting which features
to implement, and then writing a compiler when, in fact, language
development is a complex process, that requires balance, compromise,
and is affected by the environment and history in which it is
implemented.



But we have to break the subject up somehow, and this seems a
reasonable way to start.



Types



Different pieces of information in a program may have different
types. For example, a language may treat a string of
characters and number in different ways (dividing a string by 3.14 may
not be possible, even if the string, when printed, is "200"). A
language like this has at least two types - one for strings and one for
numbers.



I can think of one language, Tcl, where
everything is a string, but most languages have many types.



At first, types seem fairly simple. They are a way of dividing up
things into different groups. But it can soon get much more complicated
- what if one group is "like" another group in certain ways? What is
the type of a function?



Types will reappear throughout this section.



Static and Dynamic
Typing



Types can be static or dynamic. Languages like Lisp or Python have many
different types, but when you look at a piece of code there is nothing
that forces a variables to "be" (or to "point to") a piece of data of a
particular type. In other languages, like ML or
Eiffel, a certain variable will always be
connected with a value of a certain, fixed type. The first group of
languages (where the type of everything is unknown when the program is
compiled) has dynamic types, the second group has static types.



Dynamic types are good because the program source can be more flexible
and compact (which might be particularly useful for prototyping a
system, for example). Static types are good because they allow certain
errors in programs to be detected earlier (a compiler for a statically
typed language may also be able to make extra optimisations using the
extra information available, but this depends on details of particular
languages and compilers).



My own view is that at computing projects become larger, static typing
becomes more important. I would not like to work on a project with many
other programmers using a dynamically typed language, and I choose to
use dynamically typed languages, usually, when doing projects of my
own.



In some languages (e.g. ML) the interpreter or
compiler can often work out the type associated with a variable by
itself, which saves the programmer a lot of effort.



Strong and Weak Typing



Types can be weak or strong. The languages mentioned
above are all strongly typed, which means that at any point in the
program, when it is running, the type of a particular chunk of data is
known.



Since a dynamically typed language does not have complete type
information at compile time it must, if it strongly typed, keep track
of the type of different values as it runs. Typically values are
boxed together with information about their type - value and
type are then passed around the program together.



It might seem that a strong, statically typed language would not need
to do this and so could save some memory (as type information is
available when the program is compiled). In practice, however, I
believe that they still do so - possibly because of
polymorphism (see below).



Unlike the languages mentioned so far, C has weak
typing - some variables can point to different types of data, or even
random areas of memory, and the program cannot tell what type of object
is being referred to. Depending on the context within the program, the
variable is assumed to point to some particular type, but it is quite
possible - and a common source of confusing bugs - for this assumption
to be incorrect (some type checking is done by a C
compiler, but not as much as in a language designed to have rigorous
compile time checking, like those described as statically typed above).



Java is strongly, but not statically, typed -
classes can be converted (cast) and, if the types are not
compatible (related through inheritance - see below), a run time error
will occur. Apart from this (significant) exception the Java type
system can be considered static - one description is "compromised
strong static typing".



When strong static typing is enforced (even if only partially, as in
Java) it can be difficult to write generic algorithms - functions that
can act on a range of different types. Polymorphism allows
"any" to be included in the type system. For example, the types of a
list of items are unimportant if we only want to know the length of the
list, so in ML a function can have a type that
indicates that it takes lists of "any" type and returns an integer.



Another solution to the problem of over-restrictive types is to use
inheritance from OOP (see below) to group data
together. Yet another approach, used in C++, is
templates - a way of describing generic routines which are
then automatically specialised for particular data types (generic
programming
and parameterised classes).



Object Oriented Programming



Many language support object oriented programming. In OOP data and functions are grouped together in
objects (encapsulation). An object is a particular
instance of a class. Each object can contain
different data, but all objects belonging to a class have the same
functions (called methods). So you could have a program with
many email objects, containing different messages, but they would all
have the same functionality, fixed by the email class. Objects often
restrict access to the data (data hiding).



Classes are a lot like types - the exact relationship between types and
classes can be complicated and varies from language to language.



Via inheritance, hierarchies of objects can share and modify
particular functions. You may have code in one class that describes the
features all emails have (a sender and a date, for example) and then,
in a sub-class for email containing pictures, add functions
that display images. Often in the program you will refer to an email
object as if it was the parent (super-class) because it will
not matter whether the email contains a picture, or sound, or just
text. This code will not need to be altered when you add another
sub-class of email objects, containing (say) electronic cash.



Sometimes you may want an action on a super-class to produce a result
that depends on what sub-class it "really is". For example, you may
want to display a list of email objects and want each sub-class (text,
image, etc) to display in a different colour. In many languages it is
possible for the super-class to have functions that sub-classes change
to suit their own purposes (polymorphism, implemented by the
compiler using a technique called dynamic binding). So each
email sub-class may supply an alternative to the default, printing
function, with its own colour.



In many OO languages it is possible to find out
what class an object is (run time type information) and even
what functions are connected with it (introspection /
reflection). Others, like C++ have
little run time information available (at least in the standard
language - individual libraries of objects can support RTTI
with their own conventions).



There are at least three approaches to OO
languages:




Methods in Classes



Many languages follow Smalltalk in
associating functions (methods) with classes. The methods form
part of a class definition and the language implementation will have
(this is a low-level detail hidden from the programmer) a
vtable for each class which links methods to their
implementations. This indirection is necessary to allow
polymorphism, but introduces a performance penalty. In some languages
(C++, at least), only some methods, marked as
virtual by the programmer, are treated in this way.



Multi-Methods Separate
from Classes



Some languages (e.g. common Lisp / CLOS) allow
functions to specialise on the class of any variable that they are
passed (multi-methods). Functions cannot be associated with one
class because different versions of the function may exist for many
different combinations of classes.



Prototypes



Other OO languages do away with classes
completely (e.g. Self). Prototype-based
languages create new objects using an existing object as an example
(prototype). Apart from solving some problems with dynamic
object creation, this approach also encourages delegation
(function calls are passed to other objects) rather than inheritance.



More Object Oriented
Programming



Inheritance may be restricted to a single parent (a car is a type of
vehicle, and can use methods defined for vehicles - single
inheritance
(e.g. Modula 3)) or not (a car
is a type of vehicle and a type of polluter, and can use methods
defined for both parents - multiple inheritance (e.g. C++)).



Java has a compromise between single and
multiple inheritance - it allows multiple inheritance of
interfaces (something like class types - classes can be
written to implement an interface), but only single
inheritance of classes.




When sub-classing objects, it is possible to redefine methods. In most
(contravariant) languages the argument to a more specialised
functions must be the same, or a more general class. This allows
methods on objects to be called whether the object is the declared
class or a subclass (e.g. C++, Java, Sather). But in
other (covariant) languages (e.g. Eiffel) the argument to the subclass must be
more specialised than the argument to the method in the parent. This
implies run-time errors or compiler checking, but may be more useful -
specialised classes often require specialised arguments.



One approach to software design involves specifying certain conditions
that objects must satisfy (design by contract). Conditions
might be specified before (pre-conditions), throughout
(invariants) or after (post-conditions) invoking a
method. Some languages (Eiffel led the way
here) provide support for this within the language.



Declarative
Languages



Languages can be classified as functional,
procedural or logical.



In procedural languages it is common for the same variable to keep
changing value as the program runs. These are a common source of errors
and are avoided in declarative (functional or
logical) languages.



If you are not used to declarative programming, it is difficult to see
how a program can work without variables that change value, so the
statement above sounds suspiciously like "programs are a common source
of errors". Part of the problem is that so many different ideas are
covered by the word "variables" and only some of these are eliminated
from declarative programs.



First, I am not saying that variables are not used at all - giving a
name to a value is very useful in all languages.



Second, I am not saying that a variable as it appears statically in the
source code always has one value. The return value of a function can be
different each time it is called, for example. Such variables are
created each time the function is used and destroyed again when the
function ends (the value that they return is not destroyed, but it is
not possible to assign a value to the variable inside the function
definition from outside).



Returning to declarative languages, we can now say more precisely what
they avoid - variables do not change value inside their dynamic scope.
This means no global variables that are updated to reflect how the
program has progressed, and no loop counters, but still allows named
values and different values inside different scopes (e.g. each time a
function is called, it can accept different arguments and return
different values).



One way of avoiding state variables is by passing functions
around. Instead of evaluating a certain function and then passing the
result to another part of the program, the function itself is passed
for evaluation when required. Functions that can be passed like data
are called first-class functions; a related term is higher
order functions
which describes functions that take functions as
arguments, or return functions as results.



For practical reasons many functional languages
do allow for some static data (especially for input/output - languages
which are strictly functional are termed pure).



Logical languages (Prolog, Mercury) are
collections of logical statements and questions about the relationships
they describe (although at first sight it may not seem possible to
program in this way). I'm afraid I can't say much more about them
because I've not used them much.



The advantage of declarative languages, apart from avoiding problems
with state variables, is that programs written in them are closer to
the program specification. Programming, therefore, is at a higher level
than in the imperative languages.



Functional Programming



One facility that enables functional
programming is closure - "wrapping up some state". For
example, to make a function that add 2 to numbers it is possible to
make a (nameless) function that contains the addition function and the
value (state) 2 (the nameless function is called a lambda
expression
) - this can then be used to add 2 to other numbers.
Although that example is trivial, it is also possible to encapsulate
more complex actions.



When defining a closure you must worry about the scope of variables. If
a closure includes a variable, is the value of that variable set when
the closure is used, or when it is defined? Most modern languages store
the value when the closure is defined (static scope). In
contrast, early versions of Lisp, for example, were dynamically
scoped
- variables took values that depended on where the closure
was used.



To avoid having variables that count loop iterations functional languages encourage recursion
- where a function calls itself repeatedly with (hopefully) simpler
arguments. The most efficient way of doing this (which also avoids
running out of memory if the routine recurses many times) is tail
recursion
- the routine is called again only at the very end of
the function, when everything has been calculated, allowing the routine
to be restarted without worrying about over-writing variables.



Other features of some functional languages
include currying (a function with some arguments already
supplied), functions that return several values at once, and pattern
matching (to assign values from multiple returns).



Functional languages can be divided into two groups, depending on
whether or not they use lazy evaluation. If a language is
lazy, then functions are not evaluated until they are actually needed
inside an expression, while non-lazy (eager/strict evaluation)
languages evaluate functions as soon as they are passed as arguments.
Laziness leads to some elegant solutions for problems involving
infinite series and seems (to me, at least) more consistent with the
idea of functional programming. However, because the order in which
functions are used is not very clear to the programmer, the speed and
efficiency of programs written in lazy languages can be difficult to
understand.



Without permanent state there is less need for objects in functional languages, but types are still
important - new types can be defined (constructed types) and
are operated on using pattern matching (you might define a
complex number type as having two real numbers, pattern matching lets
you refer to the numbers separately). Haskell (which is the "standard" functional
language in some respects) has a concept of type classes that
allows functions to operate on related types.



Flexible Syntax



Some languages allow operator overloading where functions can
be defined that correspond to operators (e.g. +, -). This allows
operations on different types to be expressed using a natural syntax
(addition of matrices is an obvious example).



In languages like Tcl and Lisp, the distinction between language and data is
very flexible (dynamic). It is easy for the input to the
program to be a program, or a program fragment. In other languages the
division is rigid (static). When language and data are similar
it is often possible to change the syntax of the language quite easily,
while stricter languages only allow extensions in certain pre-defined
ways.



Where code and data are strongly separated it may still be possible (in
strongly typed languages) for code to enquire about the structure of
the program, and even to create further code (Java, Python).



Compilation



A computer's CPU does not process a high level language directly - the
program must be translated to machine code. This can be done
once, as a distinct step before the program is run
(compilation) to produce an executable, or the
translation be done dynamically, when the program is run
(interpretation). It is also common for a program to be
compiled to an intermediate representation (byte code or
p-code) which can be interpreted more efficiently. More
compilation before the program is run implies less overhead and greater
speed later; the compiler can also analyse more of the code and so
perform static optimisations. Some interpreters are
sophisticated enough to analyse the running program and make
dynamic optimisations - these use information not available to
a static compiler, but static compilation still gives the best
performance in most cases.



Some languages must be compiled, others are interpreted. Many offer
both options (it can be useful to develop and test using an
interpreter, then compile for maximum performance once the program is
working, for example). Some languages produce separate programs while
"programming" others means extending the (monolithic) language /
interpreter until it does what you require (Smalltalk, Forth).
This last approach can be flexible enough to allow programs to be
modified with new code while they are still running.



Threads



In a multi-threaded program the same code is "run" more than
once at the "same" time. If there is only a single processor then in
practice the different threads takes turns to execute.



Having several threads running at once can be useful. For example, a
graphical display can remain responsive (using one thread) while the
program is doing some calculations (using another thread). But it can
also introduce problems. For example, two threads may update data at
"the same time". With complex data it is possible for corruption to
occur. Mechanisms are needed to prevent this, but the same mechanisms
can introduce further problems (in my experience with multi-threaded
Java code, thread problems are the main source of run-time errors).



Taking the idea of threads further, it's possible for programs
to be distributed - separate parts of the process running on
different machines, communicating across the network.



Some languages have no explicit support for threads (C, Common Lisp), or
include only basic features (Java). Others have multiple threads as a
key idea (Oz, Occam, Erlang - the
designer of Erlang argues that concurrency
oriented
programming is an important concept, for more
information see the talk via the previous link.).



Error Handling



When an error occurs in a a program, how is it handled? Many languages
implement various forms of exception handling - an error
forces a function to return (and the function from which it is called
to return...) until a special section of code is met that deals with
the error.



This can cause subtle errors with programs that do not have automatic
memory management (see below) - especially in C++.



Data Structures



Some languages include complex structures (lists, tables, etc) within
the core language, others have standard libraries that are separate
from the core language but considered part of the standard language
implementation. Recently, libraries that support Internet applications
have been popular (following Java's success -
Rebol is an extreme example). Many languages also include libraries to
help implement graphical user interfaces (GUI).



Some libraries are provided as closed, read-only packages. others have
source available; often the code can be modified and re-used (for more
information check out open source, FSF, gnu).



When the program is stopped, how are data stored? Does the language
have persistent storage (some kind of database that keeps information a
program is not running)?



In a related vein, how is memory for data structures managed? Memory
used to store a particular piece of data must not be recycled if the
contents are still being used (but leaving the contents alone when they
are not needed can quickly use all the available memory). This may not
be a difficult problem in functional languages,
but in procedural languages the extent (in time) of a piece of data is
not always obvious. Some languages avoid this problem by having the
size of all data items known at run-time (Fortran 77), others provide
routines that automatically manage the memory (garbage collection -
GC
), and the rest leave it to the programmer to explicitly request
and release memory (C, C++).



Development
Environment



What is the development environment like? Is it command-line based or
is there a graphical interface? Is there a builder to create your own
GUI? If so, how does it structure the interface - is it hierarchical or
flat, do you have to modify the generated code? Does the environment
support a particular development methodology?






Learning Languages




Why Learn a new Language?



Although there is some evidence that things are changing, it is still
fair to say that the selection of languages used in software
development is fairly static. So there is little pressure to learn a
new language to keep your job.



But I believe learning a new language can make you a better programmer.



Ignoring the fun involved, and the practical advantages of having more
than one tool available for the next problem, there are two basic
reasons why you might want to learn a new language:




  • By "moving up" (learning Java if you normally
    use C++, for example) you learn lessons that
    transfer directly back to the language you normally use.


  • By "moving across" (learning a language with a very different set of
    core ideas) you can widen your perspective and learn new approaches
    to problems that may seem difficult in you "usual" language.



Moving Up



As an example of the first case, let's look at the arguments for
learning Java if you program in C or C++.



It is very difficult to write a program in Java
without beginning to understand how to use classes and objects - this
is in contrast to C++, where you can continue
using the same style of programming you used with C. So learning Java will
teach you about OOP. Aside from your CV,
understanding the principles behind OOP will
help you become a better programmer.



It is possible to carry back what you have learnt and write C code in an OO manner (of
course, if you have already been doing this, much of OOP seems little more than hype). This usually
means using structures which contain pointers to functions that take
the structure as an argument. If you have met this style, you'll
understand what I mean; if you haven't, you should once you have used
an OO language (Python's syntax is particularly useful in laying
bare the bones of OOP).



Even if you use C++ as it was intended, you can
still learn from Java - a more complete class
hierarchy, extensive libraries, introspection, and platform
independence are all areas of the language that can change your
perspective.



A similar case can be made for learning C++ if
you know C, but you must discipline yourself to do
things the "OOP way". And of course, if you know
only Java you can learn a lot by looking at C (not just understanding, from your errors, why Java
is designed as it is, but also learning more about machine level
details).



Moving Across



Now the second case, "moving across". Looking at programming from a new
angle gives insights that remain useful when you return to your
original viewpoint. Different languages express the same
patterns (algorithms, approaches, solutions) in different
ways: by looking at a pattern with a different emphasis it is possible
to understand the pattern more completely. Sometimes a pattern is much
clearer in one language than another, but once it has been seen in its
simple form the more version can become much clearer. Patterns rely
partly on language features and partly on convention - the balance
shifts between languages and can illuminate the use of apparently
arbitrary conventions.




As a simple example, consider recursion. As a new programmer in C, in my first job, I knew nothing about recursion
(except that the concept existed). In my spare time I was playing with
ML, where it is normal to write recursive
function calls (and difficult to loop in the "usual" - procedural -
way). I understood the arguments against recursion in C and understood why recursion (at least tail
recursion) was more efficient in other languages. At first I found it
difficult to write in ML, but slowly, with
practice, I learnt to recognise where recursion naturally worked. Not
long afterwards I had a problem at work, writing in C, that was difficult to solve neatly. My first
attempt was ugly and contained a bug. But I recognised that it would be
much simpler with recursion. The recursive code was clear, as simple as
possible, and worked perfectly.



A slightly more complex example might be covariant and contravariant OO languages. When I have the time, I'll be trying
out Eiffel because as I wrote this, and tried
to understand the difference (see what I wrote here), I realised that it changed a whole pile of
assumptions I had unconsciously made about OOP.



As a final example, look at the emphasis in functional languages on moving programs closer to
specifications. Learning to use such a language will raise questions
about the development process.



How to Learn a New
Language



How do you learn a new language? The fastest way is when you are forced
to do so (I learnt the only other natural language I know apart from
English - Spanish - when i had to live with my Chilean mother-in-law
for three months). But if you're lucky enough to be learning by choice,
you are probably doing it in your spare time and you won't do that
unless you are enjoying yourself - so choose an interesting project.



Choosing what you are going to write in your new language is more
important than choosing the language. Choose the language to suit the
project or, better, choose both together. For example, if you want to
write something that will look good then don't choose a language with
no support for graphics.



Learn a little about the language before you start and try and find a
solution that will play to the language's new features. If you are
using OOP for the first time, for example, try
and think how your project can be split into objects (I can remember
doing this and wondering how all these objects would actually turn into
a working program; where was the mysterious transition into running
code? - in retrospect, it just, well, happens...). If you are looking
at functional programming, maybe a numerical
project would be a good start (I chose cryptography) (this suggestion
does not imply that functional languages are
only useful for numerical code, just that most textbooks seem to
feature numerical examples - in my limited experience - making it
easier to start in that direction).



At the same time, be honest with yourself. Don't be too ambitious -
don't pick too difficult a project and (maybe) don't pick too exotic a
language. The second point is debatable. With any language you will
learn something new: it doesn't have to be a huge intellectual leap
into the unknown. You are going to be more productive in a language
that has some familiar ideas, and you can lean on that part of the
language to get going. On the other hand, if you enjoy new ideas, maybe
you will be happier with something completely different.



It should be clear now that i don't agree with a poster to
comp.lang.Lisp who claimed good programmers can learn a new language in
a week. Of course, it is possible to understand the basic concepts -
when i tried ML (see the example above) i quickly understood how to
write recursive routines, but it took more than a week of practice
before i could look at a problem and feel the solution.



Support is also important. If you intend to post questions to Usenet,
is there an appropriate newsgroup? And is it tolerant of newbie
questions? Personally, i like books - the best impetus for me is
finding a good book on computing that uses a particular language in the
examples (Abelson and Sussman, or Norvig, are two clear examples).



A note about asking for information on newsgroups: people seem to vary
widely in how precisely they talk about languages. At one end of the
spectrum there are people who tend to rely on a "subconscious" (or at
least "sub-language") intuition and happily mis-use terminology to "get
the idea across". At the other end are people who are very precise.
Both, no doubt, will give conflicting advice on how to learn and,
sometimes, apparently conflicting answers to questions. You have to
learn to recognise different styles and read them in the context of the
poster.



I am at the "sub-language" / relaxed terminology end of this spectrum,
so don't trust everything you read here (I wrote it partly to find out
just what I knew, and to make myself learn more of the terminology).
From my writing style and the introduction I hope that it is clear that
my aim is to convey general ideas, not precise details (I think this is
useful, but it probably annoys the hell out of language-lawyer pedants
:-).



Finally, don't be afraid to change direction. I'v

No comments: