With a new programming language coming out every other day, it may be worthwhile to take a step back and point out past mistakes of past languages and try to learn from them. I consider Delphi, using Objective Pascal as the language, to be a great case study since it does (did?) so many things poorly.
It may be the first toolset/language many people were exposed to and remember it as the productive and easy-to-use complete toolset with rapid GUI development with drag-and-drop. But with it lacking basic toolsets such as decent containers, lacking a proper layout system, aging IDE, poor documentation, missing MVC concepts, etc, I find that its internal toolset these days is extremely prohibitive for making any slightly non-trivial applications without having to manually create another entirely new set of libraries. Even the maintenance of legacy applications utilizing it is bothersome since they, in my personal experience, have generally relied almost exclusively on the drag-and-drop functionality and the base tools and eventually may have resulted a difficult to maintain hodgepodge of ui, models, and everything else logic.
Most of my comments are aimed mainly at the libraries and the toolkit that is shipped, but the underlying language itself also needs to be discussed and the so many things it does poorly. With that in mind, it is worth pointing out that to this very day there remain some avid proponents of the language/framework, and there is even a Lazarus open-source project, which I presume from the name, means to bring it back from the dead. The goal of this post is therefore not to argue whether Delphi is good or bad, but to strictly restrict to many of its deficiencies with the hope that they are not repeated in other languages and frameworks.
Language
The primary goal of a programming language is to let the developer express ideas clearly without causing a hinderance with inconsistencies and poor design choices. I find Objective Pascal to be riddled with inconsistencies and unexpected behavior.
- Case insensitive language syntax
Not only does this unnecessary behavior not accomplish anything helpful, it serves as a hinderance. In any given codebase, one is bound to find instances of
myVariable
which will sometimes be written asMyVariable
,Myvariable
,myvariable
ormyVariable
.Consider perhaps a more subtle situation. If there is a class named
Widget
, then instances of the classwidget
cannot be created since to the compiler it is the same asWidget
. This leads to the naming convention where classes in Delphi are typically preceeded with aT
. For example, instead ofWidget
its "appropriate" name would beTWidget
: an unnecessary soft restriction. -
Function forward declaration signature must exactly match
implementation, including the argument names.
That means that this is not valid
procedure foo(param1 : double; paramb : double); implementation procedure foo(param1 : double; param2 : double); // INVALID! begin ... end;
It is a compile time error. Of course the benefit(?) of this is that a change in the function signature is forced to be propagated down to function declaration and implementation. That may be considered a feature. But then one may wonder why the language does not enforce case sensitivity.
-
Inconsistent semicolons syntax
There are too many unnecessary interruptions in the natural flow. Consider the following snippet:
procedure foo();
begin
if (conditionHolds) then
doThis() // Can't have semicolon here
else
doSomethingElse(); // must use semicolon here
if (conditionHolds) then
doThis(); // Must use semicolon here
if (conditionHolds) then begin
doSomethingElse(); // Must use semicolon here
end;
// Valid. Case insensitive
dosomethingelse();
end;
It's not that difficult to commit to memory when a semicolon
is not needed, but the change in the flow is inconsistent
and inconvenient. Imagine changing a then
block to a then begin
block. Then one would
also need to add a semicolon, and vice versa. So it may be
more consistent to avoid if then
blocks entirely and use if (condition) then begin
block
instead. But that is just too verbose.
One may be tempted to argue that c
-like
languages have an almost similiar behavior for online if
conditions.
But the two are different. Notice the then
directive.
I will just name a couple here, but there are many more.
sqr
is the function name for squaring its
argument. It was deemed a good idea to distinguish function
and procedure
, prefer then begin
over
a shorter {
or :
because it is
considered more readable (arguable), but it makes me
wonder if there was ever any considerations put into
having sqrt
and sqr
function names that are actually
inverse functions of each other.
This is of course not an isolated incidence. For instance,
there is a imDontCare
property. I
sometimes struggle to give variables good names, but imDontCare
seems overly lazy. One may understand that perhaps they
"don't care" about specific restrictions imposed with
imDontCare
, but what exactly does it do?
On the same topic, then begin
is not more
readable than {
or :
for
developers. Even if indentation by itself is not a
sufficient enough to decifer the flow subconciously with a
glance, the developer is not reading a novel, and even if
they were, they would never need to read the entirety of then begin
to
understand that something is about to begin following an
if
statement. Incidentally, the dfm
syntax
used by UI components in Delphi uses a yaml
-like syntax
abscent of then
/begin
blocks entirely.
array of datatype
,
unless they are aliased.
function foo() : array of Double; // invalid!
type ArrayDouble = array of Double;
function foo(): ArrayDouble; // ok
If both Type1 = Widget
, and Type2 = Widget
are
declared, then the compiler treats them as two different
types. Even though they are identical.
The language was created with the aim of being an improvement
over c
, yet it made it virtually impossible to move
away from c89
-esque restriction by requiring that
all variables are placed in a var
or const
block. Consider the snippet below as
an example:
function foo();
var
i : Integer; // for a for-loop
M : Integer;
begin
// Must be initialized down here
M := bar();
// What is type of M? Need to look up.
// Any for-loop must have its index counter defined somewhere else
for i := 0 to M do begin
DOSTuff(); // case insensitive
end;
end;
To add on to this, inline array initialization cannot be
performed outside the var
block.
result
or worse,
assignining to the function name.
I assume that the implicit result
variable
was put in to avoid having to declare the returning type
in its own var
block (see above). But that is
hardly productive; if performing an early function
termination, there is a need for an extra exit;
. See
for example below
function foo(i : Integer) : Boolean;
begin
if (i < 0) then begin
// or assign to the implicit foo variable for even more inconsistencies in code
result := False;
exit; // can't just return and exit in one line
end;
Dostuff(); // case insensitive
// not a guaranteed final value
result := True;
result := False;
end;
This is akin to writing an essay just to say "no".
String
type whose index starts at 1.
Principle of single responsibility is violated through out. The container classes are containers that are also spaceships.
record
items
are returned by value.
This behavior is inconsistent with other built-in
containers. The only way to change the data is to
overwrite the entire
record
item.
...
var myList : TList<MyRecordType>;
var myRec : MyRecordType;
...
myList[0].item = 4; // Invalid!
// Copy the the entire record
myRec = myList[0];
// Modify the relevant part
myRec.item = 4;
// Overwrite existing record with new data
myList[0] = myRec; // Ok!
record
s cannot have destructors
record
s cannot have default constructors with
zero arguments.
To summarize: record
types are hardly useful.
What object pascal calls "class" instance is no different
than a raw pointer in c
or c++
. That means that they need to be
allocated (i.e. create()
them, though there is no
uniform constructor syntax), and the developer is
responsible for managing them throughout their
lifetimes. There are hacks to try to emulate object
lifetime managenet using RAII concepts but they are far
from elegant.
// ...
var
widget : TWidget; // See notes on naming convention
begin
// ...
// `widget` is a actually just a pointer and must be allocated
// widget.foo(); // expect a segfault here! widget is null
// valid: case insensitive
widget = twidget.CreatE(); // Must put it on the heap
widget.foo(); // Ok... Maybe
// ...
Free(widget); // Might have to wrap everything in a try-block
// to avoid leaking
end;
properties
cannot reference member
variables that are declared on lines proceeding them.
It is the job of the compiler to look at the class as a single entity, not what has been defined up to that point. This makes working with classes feel very uncomfortable and unnatural.
What this effectively forces you to do is to have the privates before public declaration. This seems counterintuitive since no one reads from bottom to top. Or the developer is forced to try to skip private members before looking at the interface that the class actually exposes.
The convention is to use a Create()
function and designate it as the constructor, but it's not
enforced.
{
. In other words,
one cannot have have a
{
in a {}
comment block.
Most of the language seems to have been designed with the
goal of making it easier to write compilers, rather than
making it easier for the developer actually freely express
ideas in an expressive way. This is another example of
this: rather than escaping {
in a comment
block, they are disallowed.
Consider for example
// Signature
function foo() : Integer;
// ...
// Valid! `a` is assigned to the value of `foo`, but what is `foo`?
// Must rely on additional info.
a = foo;
// Also valid!
b = foo();
// ...
Perhaps these can be caught with a linter. But this behavior is totally unnecessary.
Libraries, IDE, and 3rd party vendors
-
The cursor can be placed anywhere on the
IDE's text editor.
This would be great if it was being used for creating the circa 1990s ASCII art. But trying to write code in an editor that blatantly disregards file contents is frustrating.
-
IDE does not respect its current monitor in debug
mode.
There does not seem to be a way to force the IDE to respect your choice of which monitor to use. If the IDE is forced back to its original window after it changes monitors, it consistently results in the "solitaire effect" of other windows:
- Official examples look like they were hastily put
together over night by an intern.
Examples are a great way to showcase a language, library, or toolkit. But Delphi's built-in examples are akin to using bubble sort as the example of sorting an array — it is hardly elegant and it is unlikely to be used in production. The examples mostly involve using their limiting toolset as-is or poor practices. As an example, for a list-view with images, virtually everything is hard coded, and there is a complete mixture of data and UI logic.
- A project 'unit' created through Delphi's IDE by default encourages poor practice such as exporting globals from a unit
-
Poorly designed and poorly documented official libraries
As an example,
Vcl.Forms
contains 16590 lines, and 70 total lines of comments. Higher comment line count most certainly does not necessarily imply better documentation, but the existing comments are generally not helpful. - Embarrassingly inconsistent, incomplete, or useless
documentations.
One of the main things I look for in a library is how well documented it is. Delphi misses by a long shot. Some small examples:
-
TList<T>.Assign
is listed in docs, but not actually implemented (as of this writing) -
DateString
is listed as a member function ofTDateString
, but does not actually exist (as of this writing) -
The actual description for
Val
data member in the official documentation forTDateTime
isThis is Val, a member of class TDateTimeBase
-
Another
example for a member function:
This is CopyImage, a member of class TSpeedButton.
-
This is
not an isolated case.
This is Find, a member of class TStringArray.
This is OpenRefCount, a member of class TClipboard.
- Or another one which made even our most senior engineer specializing in Delphi scatch his head wondering where file modes are actually defined.
-
-
Official VCL library seem to only provide a minimal
abstraction over WinAPI.
As a consequence of this, when doing anything even slightly non-trivial, the end user of VCL needs to to some extent understand implementation details of WinAPI.
- For GUI applications, its main target platform is MS Windows. Everything else is an afterthought.
-
Despite the main vendor about VCL being complete and
mature, VCL is effectively abandoned in favor of
"FireMonkey".
No mature GUI library lacks a half-decent layout system, have what are understood as bugs in the community with no aims to fix them.
- As an incentive to buy the latest Delphi bundle, sloppy
third party libraries such as 'tchart' are included.
TChart serves a great role as highlighting poor software design and architecture sold commercially. Despite the virtual and abstract base classes, it is rather cumbersome to try to extend functionality without inheritance and overwriting most of the functionality. Principle of least astonishment is routinely violated, and exhibits wads of inconsistencies and unexpected behavior. For example:
- Markers on scatter series are called "Pointer"s
- Panning is called "Scroll" in the context of specifying mouse buttons, but it's called "panning" within the context of changing its mode
- Defaults to sorting data passed to series plots.
- Scrolling the mouse wheel in a plot actually scrolls the container window up and down.
Overwriting these behaviors involves trying to tame a massive superclass, and ocassionally might run into cases where what is meant to accomplish actually cannot be accomplished using the exposed API.
- Does not provide a native multicast system
-
Unusual function signatures.
As an example, the
FormatDateTime()
function takesformat
as first parameter, and thedatetime
as second. Of course, then one may think that perhapsFormatDateTime()
is a "procedure" where thedatetime
object is being modified. But that's not the case. -
Questionable functions/procedures.
As an example
-
DateTimeToString
is a procedure -
DateTimeToStr
is a function
-
-
Unusual default behaviors.
For example, scrolling with mouse in a
TSringGrid
(and its parent classes) moves the selection up and down rather than scrolling the view. This behavior is independent of whether mouse was clicked in the view container outside the table view. Also, as mentioned previously, Delphi has no concept of a View. -
Uncaught exceptions are displayed to the end user in form of
error message boxes.
This means that, if ignored, the end user may for example receive an error message box complaining about fixed row count.
-
All dependencies must be explicitely typed in the "main"
file for them to show up in the IDE.
Even if the dependencies are included in the project configuration file, they won't be listed in the IDE unless they have all been individually listed in the main file.
-
"ui" files must be included as a comment (?) for them to
show as dependencies in the IDE. For example,
uses AppMainWindow in 'AppMainWindow.pas' { AppMainWindow };
-
Built in functions for reading files do not open
files in read-only mode
And despite what the some documentation may indicate, as of this writing, there is no actual way to change this unusual behavior. See for example
TStringList.LoadFromFile()
andTFile.ReadAllLines()
.
These have real consequences leading to, for example, some of
the community or the
vendor itself
not knowing how to accomplish basic tasks without using their
drag-n-drop and GUI, or countless times top answers on
StackOverflow regarding non-gui related tasks being answered
involving Forms
.