Ignoring the length part (as some objects would have their own length
and some would not but that difference is immaterial here) how would a
'tag' system be laid out in memory and how would one use that to locate
a method?
I am a long way from implementing OO-type objects but I have in mind a
layout for them and something Dmitry mentioned in another thread
suggested that there may be a better way.
My intended model is to have each object's data preceded by a pointer to
a table of its methods. To illustrate, let's say the source code had a structure 'point' with two fields (x and y) and which referred to two
methods (plot and clear).
Each 'point' would have the coordinates and the two methods mentioned
such that if we had a point P then
P.x would refer to the x coordinate
P.plot() would invoke the plot method
My idea is to implement that as two parts. Each object would have a read-write part as
pointer to method table
x
y
and the pointer therein would point to a read-only 'method table' which
had the records and some other fields, such as
number of objects pointing at this table
pointer to the type
pointer to procedure 'plot'
pointer to procedure 'clear'
Sorry if that looks a bit complicated. It's about as short as I can
think to make it. Basically, the method table would contain a count of
the number of objects which were pointing at it, a pointer to the type definition, and a pointer to each method.
I chose it that way to make it fast to dispatch one of the procedures:
follow one pointer to get to the method table then invoke the routine
pointed to by the pointer at the designated offset. (The routine would
be supplied a pointer to the object.)
To get to base info about the type would also be possible by following
the pointer to the type.
Opinions on the scheme?
I am a long way from implementing OO-type objects but I have in mind a
layout for them and something Dmitry mentioned in another thread
suggested that there may be a better way.
My intended model is to have each object's data preceded by a pointer to
a table of its methods. To illustrate, let's say the source code had a structure 'point' with two fields (x and y) and which referred to two
methods (plot and clear).
Each 'point' would have the coordinates and the two methods mentioned
such that if we had a point P then
P.x would refer to the x coordinate
P.plot() would invoke the plot method
My idea is to implement that as two parts. Each object would have a read-write part as
pointer to method table
x
y
and the pointer therein would point to a read-only 'method table' which
had the records and some other fields, such as
number of objects pointing at this table
pointer to the type
pointer to procedure 'plot'
pointer to procedure 'clear'
Sorry if that looks a bit complicated. It's about as short as I can
think to make it. Basically, the method table would contain a count of
the number of objects which were pointing at it, a pointer to the type definition, and a pointer to each method.
I chose it that way to make it fast to dispatch one of the procedures:
follow one pointer to get to the method table then invoke the routine
pointed to by the pointer at the designated offset. (The routine would
be supplied a pointer to the object.)
To get to base info about the type would also be possible by following
the pointer to the type.
Opinions on the scheme?
However Dmitry said: "You do not need length, because it can be deduced
from the tag. Then tag is more universal than a pointer to the table.
Pointer do not work with multiple-dispatch, for example."
Ignoring the length part (as some objects would have their own length
and some would not but that difference is immaterial here) how would a
'tag' system be laid out in memory and how would one use that to locate
a method?
On 2021-08-29 20:24, James Harris wrote:
Ignoring the length part (as some objects would have their own length
and some would not but that difference is immaterial here) how would a
'tag' system be laid out in memory and how would one use that to
locate a method?
You must consider the following inheritance variants:
On 29/08/2021 19:24, James Harris wrote:
I am a long way from implementing OO-type objects but I have in mind a
layout for them and something Dmitry mentioned in another thread
suggested that there may be a better way.
My intended model is to have each object's data preceded by a pointer
to a table of its methods.
However Dmitry said: "You do not need length, because it can be
deduced from the tag. Then tag is more universal than a pointer to the
table. Pointer do not work with multiple-dispatch, for example."
I haven't implemented this stuff in static code: classes with methods
and with inheritance, so can't speak directly from experience.
I have played with it however, and I have implemented it for dynamic code.
The first thing that strikes me, if I understood correctly, is that each instance of a Point needs to include not only the data x,y, but also a pointer.
With apparently, exactly the same pointer in each of millions of
instances. Which all means that if you allocate an array of such points,
you can't just initialise them with all-zeros to (0,0), but need to specifically initialise that pointer field in each one.
So, why is that actually necessary? Is it because of inheritance, where
any instance P could be of the base class, or of a derived class which
could have its own sets of methods, and this information is not known
until runtime?
I can illustrate that with this example in dynamic code:
record point =
var x,y
proc plot(&self) =
println "POINT",self.x,self.y
end
end
record dirvec(point) = # inherit from Point
proc plot(&self) =
println "DIRVEC",self.x,self.y
end
end
proc start=
a:=point(10,20)
b:=dirvec(30,30)
a.plot()
b.plot()
end
The output here is:
POINT 10 20
DIRVEC 30 30
Each instance (A and B), contains values for its X and Y fields. But no pointers to method tables. But they both have, because it is dynamic, is
a type tag: a index into tables of types.
That is used to search (again because its dynamic), a table of entries
for the 'plot' attribute (which can be used in a different other types
too), which matches the types of A and B.
I would have expect a static language to convert those last the lines to:
point.plot(a)
dirvec.plot(b)
I guess it depends on how A and B would be declared, and whether, if
they have those two different types, either can still be passed to a
function taking a Point type, and possibly one taking a Dirvec type.
(I think I ran into trouble trying this in static code, not just because
of methods, but because the two types instances would have the normal
fields at different offsets, and could be different types; say Point has int32 X,Y, but Dirvec defines float64 Y,Z and inherits int32 X.
But maybe I'm just not understanding how OOP is supposed to work.)
On 29/08/2021 20:32, Dmitry A. Kazakov wrote:
On 2021-08-29 20:24, James Harris wrote:
Ignoring the length part (as some objects would have their own length
and some would not but that difference is immaterial here) how would
a 'tag' system be laid out in memory and how would one use that to
locate a method?
You must consider the following inheritance variants:
...
Thanks for the information, Dmitry. I might come back to you about some
of that. But for this thread I was wondering how the memory might be
laid out under the tag model that you mentioned. What form does a tag
take and how does the runtime get from the tag to a method?
If it depends on which model is in use then what about single
inheritance and the Full MI model you mentioned?
On 29/08/2021 23:04, Bart wrote:
So, why is that actually necessary? Is it because of inheritance,
where any instance P could be of the base class, or of a derived class
which could have its own sets of methods, and this information is not
known until runtime?
It's not because of inheritance. It's to support polymorphism.
Without OO there would be no 'method table pointer' (MTP). Instead, each function called to act on an object would know the object's type and,
hence, where its fields are.
With OO the object could be one of a number of different types as long
as they all have the same conceptual set of functions (in the same
internal order), e.g. they both have a plot() function or they both have
an add() function. So you could have
pixel_point P
float_point F
Both types could have a plot() function. As long as it had the same
interface then the function could be invoked by
P.plot()
F.plot()
The key benefit, AISI, is that the routine which calls plot would work
with either type of point. The call wouldn't care what form the point
took; it would be polymorphic.
Each instance (A and B), contains values for its X and Y fields. But
no pointers to method tables. But they both have, because it is
dynamic, is a type tag: a index into tables of types.
So your form of a tag is an index into a table? Maybe that's how
Dmitry's 'tag' would be implemented.
I have doubts about indexing into a table rather than pointing to the
method table directly. It would probably require at least one extra
level of dereferencing to find a method.
On 29/08/2021 23:04, Bart wrote:
On 29/08/2021 19:24, James Harris wrote:
I am a long way from implementing OO-type objects but I have in mind
a layout for them and something Dmitry mentioned in another thread
suggested that there may be a better way.
My intended model is to have each object's data preceded by a pointer
to a table of its methods.
...
However Dmitry said: "You do not need length, because it can be
deduced from the tag. Then tag is more universal than a pointer to
the table. Pointer do not work with multiple-dispatch, for example."
...
I haven't implemented this stuff in static code: classes with methods
and with inheritance, so can't speak directly from experience.
I have played with it however, and I have implemented it for dynamic
code.
The first thing that strikes me, if I understood correctly, is that
each instance of a Point needs to include not only the data x,y, but
also a pointer.
With apparently, exactly the same pointer in each of millions of
instances. Which all means that if you allocate an array of such
points, you can't just initialise them with all-zeros to (0,0), but
need to specifically initialise that pointer field in each one.
So, why is that actually necessary? Is it because of inheritance,
where any instance P could be of the base class, or of a derived class
which could have its own sets of methods, and this information is not
known until runtime?
It's not because of inheritance. It's to support polymorphism.
Without OO there would be no 'method table pointer' (MTP). Instead, each function called to act on an object would know the object's type and,
hence, where its fields are.
With OO the object could be one of a number of different types as long
as they all have the same conceptual set of functions (in the same
internal order), e.g. they both have a plot() function or they both have
an add() function. So you could have
pixel_point P
float_point F
Both types could have a plot() function. As long as it had the same
interface then the function could be invoked by
P.plot()
F.plot()
The key benefit, AISI, is that the routine which calls plot would work
with either type of point. The call wouldn't care what form the point
took; it would be polymorphic.
Of course, whether that's worth the complexity of the implementation is debatable but it does allow a lot of source code to be simpler.
I can illustrate that with this example in dynamic code:
record point =
var x,y
proc plot(&self) =
println "POINT",self.x,self.y
end
end
record dirvec(point) = # inherit from Point
proc plot(&self) =
println "DIRVEC",self.x,self.y
end
end
proc start=
a:=point(10,20)
b:=dirvec(30,30)
a.plot()
b.plot()
end
The output here is:
POINT 10 20
DIRVEC 30 30
Each instance (A and B), contains values for its X and Y fields. But
no pointers to method tables. But they both have, because it is
dynamic, is a type tag: a index into tables of types.
So your form of a tag is an index into a table? Maybe that's how
Dmitry's 'tag' would be implemented.
I have doubts about indexing into a table rather than pointing to the
method table directly. It would probably require at least one extra
level of dereferencing to find a method.
That is used to search (again because its dynamic), a table of entries
for the 'plot' attribute (which can be used in a different other types
too), which matches the types of A and B.
I would have expect a static language to convert those last the lines to:
point.plot(a)
dirvec.plot(b)
Why not
a.plot()
b.plot()
?
I guess it depends on how A and B would be declared, and whether, if
they have those two different types, either can still be passed to a
function taking a Point type, and possibly one taking a Dirvec type.
(I think I ran into trouble trying this in static code, not just
because of methods, but because the two types instances would have the
normal fields at different offsets, and could be different types; say
Point has int32 X,Y, but Dirvec defines float64 Y,Z and inherits int32 X.
But maybe I'm just not understanding how OOP is supposed to work.)
Dmitry's reply about various different interpretations of how OOP can
work shows there is no single right answer. Indeed, OOP may not be the
best solution, at all.
On 30/08/2021 12:23, James Harris wrote:
On 29/08/2021 23:04, Bart wrote:
I can illustrate that with this example in dynamic code:
That code is not dynamic polymorphism - the types are all statically
known at compile time. Dynamic polymorphism would be if you also had:
proc doubleplot(&x) =
x.plot();
x.plot();
end
proc start =
a := point(10, 20)
b := dirvec(30, 30)
doubleplot(&a)
doubleplot(&b)
end
i.e., the function "doubleplot" is compiled without knowing whether it
will be calling the "point" version of "plot", or the "dirvec" version.
There is also compile-time polymorphism, handled by templates or other generic coding mechanisms.
On 2021-08-30 11:47, James Harris wrote:
On 29/08/2021 20:32, Dmitry A. Kazakov wrote:
On 2021-08-29 20:24, James Harris wrote:
Ignoring the length part (as some objects would have their own
length and some would not but that difference is immaterial here)
how would a 'tag' system be laid out in memory and how would one use
that to locate a method?
You must consider the following inheritance variants:
...
Thanks for the information, Dmitry. I might come back to you about
some of that. But for this thread I was wondering how the memory might
be laid out under the tag model that you mentioned. What form does a
tag take and how does the runtime get from the tag to a method?
You must also consider type tests. E.g. is A a descendant of B, unrelated?
If it depends on which model is in use then what about single
inheritance and the Full MI model you mentioned?
In the most general case a dispatching table belongs to the method, not
to the type. The tag would denote an N-th coordinate (plane) of the multidimensional index in the table.
E.g. in the case of full dispatch: Print (Device, Shape), Print is a
method of two hierarchies.
Now, of course, because the same tag must be used to index a multitude
of tables, it cannot be a dense index. So, go figure.
Another issue to consider is whether tables are contiguous. Consider the following scenario:
The type A is declared in the shared library libA. So you can put the dispatching table there? Not so fast. Let there be the library libB,
that derives some B from A. That must change the dispatching tables for
A upon loading of libB. When unloading libB you must roll the changes back.
On 30/08/2021 12:36, Dmitry A. Kazakov wrote:
On 2021-08-30 11:47, James Harris wrote:
On 29/08/2021 20:32, Dmitry A. Kazakov wrote:
On 2021-08-29 20:24, James Harris wrote:
Ignoring the length part (as some objects would have their own
length and some would not but that difference is immaterial here)
how would a 'tag' system be laid out in memory and how would one
use that to locate a method?
You must consider the following inheritance variants:
...
Thanks for the information, Dmitry. I might come back to you about
some of that. But for this thread I was wondering how the memory
might be laid out under the tag model that you mentioned. What form
does a tag take and how does the runtime get from the tag to a method?
You must also consider type tests. E.g. is A a descendant of B,
unrelated?
Interesting question...!
If it depends on which model is in use then what about single
inheritance and the Full MI model you mentioned?
In the most general case a dispatching table belongs to the method,
not to the type. The tag would denote an N-th coordinate (plane) of
the multidimensional index in the table.
That sounds weird. If the dispatch table belongs to the method what
would it be indexed by?
When you talk about a tag do you mean something that might, if it cannot
be optimised any better, result in a series of three or even more
lookups executed at run time before the required method is found?
Another issue to consider is whether tables are contiguous. Consider
the following scenario:
The type A is declared in the shared library libA. So you can put the
dispatching table there? Not so fast. Let there be the library libB,
that derives some B from A. That must change the dispatching tables
for A upon loading of libB. When unloading libB you must roll the
changes back.
It's rare that a good idea is so complex or has so many semantically different implementations. I do wonder whether all this complexity
should be replaced by something simpler.
My current scheme is simpler but it doesn't necessarily have
substitutability which is something I gather you believe to be
important, even from your interesting question, above.
On 2021-08-30 22:25, James Harris wrote:
On 30/08/2021 12:36, Dmitry A. Kazakov wrote:
In the most general case a dispatching table belongs to the method,
not to the type. The tag would denote an N-th coordinate (plane) of
the multidimensional index in the table.
That sounds weird. If the dispatch table belongs to the method what
would it be indexed by?
By the type, of course. It is pretty straightforward. Consider
Surface Shape
/ \ / \
PDF SVG Ellipse Rectangle
Draw | PDF | SVG |
----------+-----+-----+
Ellipse | ptr | ptr |
----------+-----+-----+
Rectangle | ptr | ptr |
----------+-----+-----+
When you talk about a tag do you mean something that might, if it
cannot be optimised any better, result in a series of three or even
more lookups executed at run time before the required method is found?
Yes. You need some unique type identifier, short enough to fit in a
register.
On 30/08/2021 22:13, Dmitry A. Kazakov wrote:
On 2021-08-30 22:25, James Harris wrote:
On 30/08/2021 12:36, Dmitry A. Kazakov wrote:
...
In the most general case a dispatching table belongs to the method,
not to the type. The tag would denote an N-th coordinate (plane) of
the multidimensional index in the table.
That sounds weird. If the dispatch table belongs to the method what
would it be indexed by?
By the type, of course. It is pretty straightforward. Consider
Surface Shape
/ \ / \
PDF SVG Ellipse Rectangle
Draw | PDF | SVG |
----------+-----+-----+
Ellipse | ptr | ptr |
----------+-----+-----+
Rectangle | ptr | ptr |
----------+-----+-----+
That solution looks as though the shapes would have to know how to draw themselves on different surfaces. As such I doubt it would scale well as
new shapes and new types of surface were added. But maybe it's purely illustrative of multiple dispatch.
it's done, the address of the drawing function (the ptr in the above
table) would need to be found from a key of three parameters
(shape type, surface type, function name)
whereas single dispatch would find a function based on two parameters
such as
(shape type, function name)
Is that correct?
On 30/08/2021 22:13, Dmitry A. Kazakov wrote:
On 2021-08-30 22:25, James Harris wrote:
On 30/08/2021 12:36, Dmitry A. Kazakov wrote:
...
In the most general case a dispatching table belongs to the method,
not to the type. The tag would denote an N-th coordinate (plane) of
the multidimensional index in the table.
That sounds weird. If the dispatch table belongs to the method what
would it be indexed by?
By the type, of course. It is pretty straightforward. Consider
Surface Shape
/ \ / \
PDF SVG Ellipse Rectangle
Draw | PDF | SVG |
----------+-----+-----+
Ellipse | ptr | ptr |
----------+-----+-----+
Rectangle | ptr | ptr |
----------+-----+-----+
That solution looks as though the shapes would have to know how to draw themselves on different surfaces. As such I doubt it would scale well as
new shapes and new types of surface were added. But maybe it's purely illustrative of multiple dispatch. What I take from it is that, however
it's done, the address of the drawing function (the ptr in the above
table) would need to be found from a key of three parameters
(shape type, surface type, function name)
whereas single dispatch would find a function based on two parameters
such as
(shape type, function name)
Is that correct?
When you talk about a tag do you mean something that might, if it
cannot be optimised any better, result in a series of three or even
more lookups executed at run time before the required method is found?
Yes. You need some unique type identifier, short enough to fit in a
register.
If the tag is the unique identifier you mention how would the compiled
code get from the tag to the correct routine?
My intention was for the tag to be the address of the dispatch table so
that functions (as long as they were at known offsets) could be accessed quickly but I don't think you liked that.
On 2021-09-03 14:27, James Harris wrote:
On 30/08/2021 22:13, Dmitry A. Kazakov wrote:
On 2021-08-30 22:25, James Harris wrote:
On 30/08/2021 12:36, Dmitry A. Kazakov wrote:
...
In the most general case a dispatching table belongs to the method,
not to the type. The tag would denote an N-th coordinate (plane) of
the multidimensional index in the table.
That sounds weird. If the dispatch table belongs to the method what
would it be indexed by?
By the type, of course. It is pretty straightforward. Consider
Surface Shape
/ \ / \
PDF SVG Ellipse Rectangle
Draw | PDF | SVG |
----------+-----+-----+
Ellipse | ptr | ptr |
----------+-----+-----+
Rectangle | ptr | ptr |
----------+-----+-----+
That solution looks as though the shapes would have to know how to
draw themselves on different surfaces. As such I doubt it would scale
well as new shapes and new types of surface were added. But maybe it's
purely illustrative of multiple dispatch. What I take from it is that,
however it's done, the address of the drawing function (the ptr in the
above table) would need to be found from a key of three parameters
(shape type, surface type, function name)
whereas single dispatch would find a function based on two parameters
such as
(shape type, function name)
Is that correct?
Yes, and the surface parameter is then class-wide, i.e. each function
must have to deal with any shape (any instance from the class =
class-wide).
When you talk about a tag do you mean something that might, if it
cannot be optimised any better, result in a series of three or even
more lookups executed at run time before the required method is found?
Yes. You need some unique type identifier, short enough to fit in a
register.
If the tag is the unique identifier you mention how would the compiled
code get from the tag to the correct routine?
Via the dispatching table indexed by the tag. Tag could not be a dense
index, true, so you would use a hash or binary search instead.
My intention was for the tag to be the address of the dispatch table
so that functions (as long as they were at known offsets) could be
accessed quickly but I don't think you liked that.
Hash functions are pretty quick.
Then again, if you don't repeat C++ and Java error confusing class-wide
and specific types, you would drastically reduce cases when dispatch
ever happens. Most of the code deals with statically known specific
types, ergo, no dispatch (or as some call it, "static dispatch").
Segmented dispatching tables is IMO a bigger problem unless you rebuild affected dispatching each time you load a library or dynamically create
a new type etc.
On 03/09/2021 13:27, James Harris wrote:
OK, the example was clearly a simple one to illustrate some aspects of
how OOP works. But I see such examples using shapes all the time and
find them somewhat annoying.
It gives the impression that OOP can magically solve all the problems
that come up in actual applications, but the reality is always a lot
more complicated.
I'm rather sceptical about what OOP can do, and I have firm views on how
far a language should get involved in matters that should be the concern
of an application.
I also think that, if you really want to use OOP seriously, then you're
quite liable to just end up reinventing C++, as you meet the same
obstacles.)
What I take from it is that, however
it's done, the address of the drawing function (the ptr in the above
table) would need to be found from a key of three parameters
(shape type, surface type, function name)
whereas single dispatch would find a function based on two parameters
such as
(shape type, function name)
Is that correct?
The shape should describe the shape itself and nothing else. If you want
to plot it, then extra information is needed:
p.plot(surface_type)
With perhaps that parameter being optional when there is a sensible
default. So here there is single dispatch based on shape type.
Anything else would be difficult to get your head around. If there are M shape types and N surface types, then you've got MxN methods to write,
but where are you going to write them? Functions are written one after
the other, not in a table!
You might consider introducing overloaded functions, so the plot()
method for Rectangle is split into one each for PDF and SVG. Feature creep.
(My approach as I used was single dispatch - a simple switch - using
common code to reduce complex objects to basic edges, and then using
single dispatch on the 'surface' when drawing those edges.)
Why not say that each surface has to provide a set of primitives that
other programs can use to draw on an instance of that type of surface?
On 03/09/2021 14:39, Dmitry A. Kazakov wrote:
On 2021-09-03 14:27, James Harris wrote:
On 30/08/2021 22:13, Dmitry A. Kazakov wrote:
On 2021-08-30 22:25, James Harris wrote:
On 30/08/2021 12:36, Dmitry A. Kazakov wrote:
...
In the most general case a dispatching table belongs to the
method, not to the type. The tag would denote an N-th coordinate
(plane) of the multidimensional index in the table.
That sounds weird. If the dispatch table belongs to the method what
would it be indexed by?
By the type, of course. It is pretty straightforward. Consider
Surface Shape
/ \ / \
PDF SVG Ellipse Rectangle
Draw | PDF | SVG |
----------+-----+-----+
Ellipse | ptr | ptr |
----------+-----+-----+
Rectangle | ptr | ptr |
----------+-----+-----+
That solution looks as though the shapes would have to know how to
draw themselves on different surfaces. As such I doubt it would scale
well as new shapes and new types of surface were added. But maybe
it's purely illustrative of multiple dispatch. What I take from it is
that, however it's done, the address of the drawing function (the ptr
in the above table) would need to be found from a key of three
parameters
(shape type, surface type, function name)
whereas single dispatch would find a function based on two parameters
such as
(shape type, function name)
Is that correct?
Yes, and the surface parameter is then class-wide, i.e. each function
must have to deal with any shape (any instance from the class =
class-wide).
OK. I am now trying to understand what you mean by 'class wide'.
Are you
saying that each drawing function would have to be passed the target
surface as a parameter - and that makes surface 'class wide'?
In practice, I'd imagine it's best for 'surfaces' to know how to draw on themselves and they would export drawing primitives (such as point,
line, curve, rectangle, ellipse etc) which routines can call.
When you talk about a tag do you mean something that might, if itYes. You need some unique type identifier, short enough to fit in a
cannot be optimised any better, result in a series of three or even
more lookups executed at run time before the required method is found? >>>>
register.
If the tag is the unique identifier you mention how would the
compiled code get from the tag to the correct routine?
Via the dispatching table indexed by the tag. Tag could not be a dense
index, true, so you would use a hash or binary search instead.
Are you thinking that to find each function the executable code would
carry out the hash or binary search at run time (if optimisation didn't remove the need for it)?
Hash functions are pretty quick.
Hash functions are not fast enough to find a call target! I was/am even concerned about the extra cost of following a double pointer. Using a
hash table would be slower still.
Then again, if you don't repeat C++ and Java error confusing
class-wide and specific types, you would drastically reduce cases when
dispatch ever happens. Most of the code deals with statically known
specific types, ergo, no dispatch (or as some call it, "static
dispatch").
I'm not sure I understand that but if you think C++ and Java got it
wrong do you think Ada got it right and what did it do differently?
Segmented dispatching tables is IMO a bigger problem unless you
rebuild affected dispatching each time you load a library or
dynamically create a new type etc.
I presume that means when a large dispatch table reaches the limits of
its allocated memory and has to be split.
On 2021-09-03 16:59, James Harris wrote:
Why not say that each surface has to provide a set of primitives that
other programs can use to draw on an instance of that type of surface?
Because that set is open ended. OOP allows you
1. adding new primitives (new shapes)
2. using primitives with new surfaces (inheritance)
All counter proposals are static and do not scale in practice, which is
why OOP is used so widely in large projects regardless any difficulties. There is simple no other way to do it, economically, there is no other way.
On 03/09/2021 18:27, Dmitry A. Kazakov wrote:
On 2021-09-03 18:43, James Harris wrote:
Unless I am missing something the cartesian M*N would be costly and
so even economically, as you mention,
And its existence is a fact of life. Shapes exist, surfaces exist,
drawing exist.
I cannot agree that there is no other way.
Show the other way to implement and use M*N operations safely.
Say you have an enum type listing M different shapes, and another
listing N different surfaces:
* Create a 2D MxN array H of function pointers
On 03/09/2021 16:28, Dmitry A. Kazakov wrote:
On 2021-09-03 16:59, James Harris wrote:
Why not say that each surface has to provide a set of primitives that
other programs can use to draw on an instance of that type of surface?
Because that set is open ended. OOP allows you
1. adding new primitives (new shapes)
2. using primitives with new surfaces (inheritance)
All counter proposals are static and do not scale in practice, which
is why OOP is used so widely in large projects regardless any
difficulties. There is simple no other way to do it, economically,
there is no other way.
Your example was good in showing multiple dispatch but I'm not so sure
it's a good solution in this case, let alone the only way!
The rights and wrongs of the approach are OT so all I'll say is that M*N
does not scale and that new shapes can be constructed from primitives.
Unless I am missing something the cartesian M*N would be costly and so
even economically, as you mention,
I cannot agree that there is no other way.
On 2021-09-03 18:43, James Harris wrote:
Unless I am missing something the cartesian M*N would be costly and so
even economically, as you mention,
And its existence is a fact of life. Shapes exist, surfaces exist,
drawing exist.
I cannot agree that there is no other way.
Show the other way to implement and use M*N operations safely.
On 2021-09-03 19:44, Bart wrote:
On 03/09/2021 18:27, Dmitry A. Kazakov wrote:
On 2021-09-03 18:43, James Harris wrote:
Unless I am missing something the cartesian M*N would be costly and
so even economically, as you mention,
And its existence is a fact of life. Shapes exist, surfaces exist,
drawing exist.
I cannot agree that there is no other way.
Show the other way to implement and use M*N operations safely.
Say you have an enum type listing M different shapes, and another
listing N different surfaces:
* Create a 2D MxN array H of function pointers
Failed. The requirement included different teams of developers and
different times rows and columns (types) added.
On 2021-09-03 22:06, Bart wrote:
On 03/09/2021 19:01, Dmitry A. Kazakov wrote:
On 2021-09-03 19:44, Bart wrote:
On 03/09/2021 18:27, Dmitry A. Kazakov wrote:
On 2021-09-03 18:43, James Harris wrote:
Unless I am missing something the cartesian M*N would be costly
and so even economically, as you mention,
And its existence is a fact of life. Shapes exist, surfaces exist,
drawing exist.
I cannot agree that there is no other way.
Show the other way to implement and use M*N operations safely.
Say you have an enum type listing M different shapes, and another
listing N different surfaces:
* Create a 2D MxN array H of function pointers
Failed. The requirement included different teams of developers and
different times rows and columns (types) added.
How is that relevant? Presumably all the necessary information (method
definitions or their interfaces) will be visible to the (presumably
AOT) compiler when the application is built?
Nope. An unknown number of dynamically loaded shared libraries may
contain types derived from Shape and/or Surface. The libraries are
loaded during run-time per scanning a list of directories containing DLL plug-ins.
Then who cares who wrote it or when.
You must care. You must scan all code to determine N and M and find all functions for all slots. Then you must recompile everything.
Anyway who are you to say how my application will be organised.
A provider of operations.
The point is that in real projects nobody has control or knowledge about
the types comprising dispatching tables. It is called late bindings for
a reason.
You may have your wet dreams about how you organize *your* application,
but the fact is that nobody develops software this way for longer than
three decades.
On 03/09/2021 19:01, Dmitry A. Kazakov wrote:
On 2021-09-03 19:44, Bart wrote:
On 03/09/2021 18:27, Dmitry A. Kazakov wrote:
On 2021-09-03 18:43, James Harris wrote:
Unless I am missing something the cartesian M*N would be costly and
so even economically, as you mention,
And its existence is a fact of life. Shapes exist, surfaces exist,
drawing exist.
I cannot agree that there is no other way.
Show the other way to implement and use M*N operations safely.
Say you have an enum type listing M different shapes, and another
listing N different surfaces:
* Create a 2D MxN array H of function pointers
Failed. The requirement included different teams of developers and
different times rows and columns (types) added.
How is that relevant? Presumably all the necessary information (method definitions or their interfaces) will be visible to the (presumably AOT) compiler when the application is built?
Then who cares who wrote it or when.
Anyway who are you to say how my application will be organised.
On 03/09/2021 21:25, Dmitry A. Kazakov wrote:
Nope. An unknown number of dynamically loaded shared libraries may
contain types derived from Shape and/or Surface. The libraries are
loaded during run-time per scanning a list of directories containing
DLL plug-ins.
Then any approach using tables attached to class instances is going to
fail too,
if every time someone makes a suggestion, you are going to
think up more unlikely scenarios to sabotage the idea.
Then who cares who wrote it or when.
You must care. You must scan all code to determine N and M and find
all functions for all slots. Then you must recompile everything.
That's what I said you have to do. a For static application which knows
all the shapes and all the surfaces.
Now you are saying you can NEVER do that because SOME applications have shapes and surfaces defined at runtime.
The point is that in real projects nobody has control or knowledge
about the types comprising dispatching tables. It is called late
bindings for a reason.
This is not about types. It's about enumerations.
On 2021-09-03 22:48, Bart wrote:
On 03/09/2021 21:25, Dmitry A. Kazakov wrote:
Nope. An unknown number of dynamically loaded shared libraries may
contain types derived from Shape and/or Surface. The libraries are
loaded during run-time per scanning a list of directories containing
DLL plug-ins.
Then any approach using tables attached to class instances is going to
fail too,
It works perfectly well with C++, Ada and dozens other OOPL. You can try
it yourself. Get a grip, you can have a type T in one library. Derive S
from T in another. Load that library at run-time. Create an object of
the type S. Pass it to a function in the first library and dispatch from
that function to a method overridden in the dynamically loaded library!
if every time someone makes a suggestion, you are going to think up
more unlikely scenarios to sabotage the idea.
What? Plug-ins are unlikely scenario? Do you live under a rock?
Now you are saying you can NEVER do that because SOME applications
have shapes and surfaces defined at runtime.
No they are defined at compile time in the components which know nothing about each other.
On 03/09/2021 22:42, Dmitry A. Kazakov wrote:
On 2021-09-03 22:48, Bart wrote:
On 03/09/2021 21:25, Dmitry A. Kazakov wrote:
Nope. An unknown number of dynamically loaded shared libraries may
contain types derived from Shape and/or Surface. The libraries are
loaded during run-time per scanning a list of directories containing
DLL plug-ins.
Then any approach using tables attached to class instances is going
to fail too,
It works perfectly well with C++, Ada and dozens other OOPL. You can
try it yourself. Get a grip, you can have a type T in one library.
Derive S from T in another. Load that library at run-time. Create an
object of the type S. Pass it to a function in the first library and
dispatch from that function to a method overridden in the dynamically
loaded library!
So my H[shape,surface] table /can/ work?
There's a place for your languages, and there's a place for ones like mine.
On 2021-09-03 16:40, James Harris wrote:
On 03/09/2021 14:39, Dmitry A. Kazakov wrote:
Hash functions are pretty quick.
Hash functions are not fast enough to find a call target! I was/am
even concerned about the extra cost of following a double pointer.
Using a hash table would be slower still.
Faster than anything the user would have to write otherwise, in order to emulate dispatch.
Then performance is never a big concern.
Safety is. User emulations of
dispatch would be nested if-statements, which are not only significantly slower O(n), but inherently error-prone and unmaintainable.
Then again, if you don't repeat C++ and Java error confusing
class-wide and specific types, you would drastically reduce cases
when dispatch ever happens. Most of the code deals with statically
known specific types, ergo, no dispatch (or as some call it, "static
dispatch").
I'm not sure I understand that but if you think C++ and Java got it
wrong do you think Ada got it right and what did it do differently?
If foo and baz are virtual functions of T then
void foo (T& x)
{
x.baz (); -- Dispatches
}
In Ada
procedure Foo (X : T) is
begin
X.Baz; -- Does not dispatch, the type is established to be T
end Foo;
On 03/09/2021 16:46, Dmitry A. Kazakov wrote:
On 2021-09-03 16:40, James Harris wrote:
On 03/09/2021 14:39, Dmitry A. Kazakov wrote:
...
Hash functions are pretty quick.
Hash functions are not fast enough to find a call target! I was/am
even concerned about the extra cost of following a double pointer.
Using a hash table would be slower still.
Faster than anything the user would have to write otherwise, in order
to emulate dispatch.
On the contrary, where the scheme I mentioned would have just two dereferences to get the address of the required function ... your scheme would have multiple dereferences, a function call, a hash calculation,
and possibly collision resolution.
Then performance is never a big concern.
I'm really surprised that such a lookup would even be considered. It
would be much slower than a direct function call.
What is your objection to my proposed solution? To be clear, the idea is
to have the object's pointer to link directly to the method table as
follows.
object: |___ptr___|___field0___|___field1___| etc
|
V
|__type___|__reserved__|__ptr__|__ptr__|__ptr__|__ptr__|
I've shown one dimension but there could be as many as necessary for
multiple dispatch.
Having said all that, IMO the static dispatch that David mentioned would
be significantly faster than either of the above approaches, though I
gather from your point about closures that it can be impossible at
compile time to predict what the type will be.
Then again, if you don't repeat C++ and Java error confusing
class-wide and specific types, you would drastically reduce cases
when dispatch ever happens. Most of the code deals with statically
known specific types, ergo, no dispatch (or as some call it, "static
dispatch").
I'm not sure I understand that but if you think C++ and Java got it
wrong do you think Ada got it right and what did it do differently?
If foo and baz are virtual functions of T then
void foo (T& x)
{
x.baz (); -- Dispatches
}
I didn't know what a virtual function was but AIUI it's a function which
has to be overridden in a descendant class. (?)
If that's right is the issue that Ada prohibits virtual functions?
In Ada
procedure Foo (X : T) is
begin
X.Baz; -- Does not dispatch, the type is established to be T
end Foo;
So T.Baz has to be in T and cannot be overridden in a subclass of T?
like mine.There's a place for your languages, and there's a place for ones
Nobody forces you to implement any feature. The grapes are sour.
[Original DAK post lost]
DAK:
On 2021-09-04 02:49, Bart wrote:
like mine.There's a place for your languages, and there's a place for ones
Nobody forces you to implement any feature. The grapes are sour.
There is a certain amount of satisfaction in having, over exactly 40
years now, used almost exclusively my own languages, implementations and tools, and all self-hosted.
One key to doing that is to be selective about what features I've added.
I've kept things simple, managable and understandable. Which is not
always easy.
Your (DAK's) role seems to be the opposite: turn everything into
something harder, less manageable, and incomprehensible!
On 30/08/2021 11:23, James Harris wrote:
P.plot()
Why, in your example, wouldn't the compiler know the type of P?
Do you have a more elaborate example that shows off the advantages of
OO, or whatever you are trying to do, that would be too clunky in C?
On 2021-09-04 22:19, Bart wrote:
[Original DAK post lost]
DAK:
On 2021-09-04 02:49, Bart wrote:
like mine.There's a place for your languages, and there's a place for ones
Nobody forces you to implement any feature. The grapes are sour.
There is a certain amount of satisfaction in having, over exactly 40
years now, used almost exclusively my own languages, implementations
and tools, and all self-hosted.
One key to doing that is to be selective about what features I've
added. I've kept things simple, managable and understandable. Which is
not always easy.
Your (DAK's) role seems to be the opposite: turn everything into
something harder, less manageable, and incomprehensible!
Right, different priorities. Simplicity for the compiler designer
translates into complexity for the programmer.
On 30/08/2021 13:08, Bart wrote:
On 30/08/2021 11:23, James Harris wrote:
...
P.plot()
...
Why, in your example, wouldn't the compiler know the type of P?
The compiler would know if P were created and manipulated wholly by the current function but P could be a formal parameter which could be one of
a number of types. I can think of two cases. One is inheritance
type A
|
type B
then a parameter declared to be of type A could include a function from either type A or type B. (Or is it the other way round?)
function f(A P)
A given activation of function f could be required to use either A's or
B's version of a function.
The other is a variant
typedef C = int or float
function f(C P)
A given activation of function f could have been passed an int or a float.
The other is a variant
typedef C = int or float
function f(C P)
A given activation of function f could have been passed an int or a
float.
Here it's easy to understand your intention. But possibly more
challenging to implement.
That looks a bit like a Haskell type,
but if you're not implementing
that, would f be a generic function?
Or would C be a variant type along
the lines of C++'s?
Will there be overloads of f()?
Just getting the basics working (eg. A+B if both are of type C) would be challenging here.
On 04/09/2021 22:00, Dmitry A. Kazakov wrote:
On 2021-09-04 22:19, Bart wrote:
[Original DAK post lost]
DAK:
On 2021-09-04 02:49, Bart wrote:
like mine.There's a place for your languages, and there's a place for ones
Nobody forces you to implement any feature. The grapes are sour.
There is a certain amount of satisfaction in having, over exactly 40
years now, used almost exclusively my own languages, implementations
and tools, and all self-hosted.
One key to doing that is to be selective about what features I've
added. I've kept things simple, managable and understandable. Which
is not always easy.
Your (DAK's) role seems to be the opposite: turn everything into
something harder, less manageable, and incomprehensible!
Right, different priorities. Simplicity for the compiler designer
translates into complexity for the programmer.
You might want to look back at the fragments of my language that I've
posted. Such as 'print x'.
On 2021-09-04 23:37, Bart wrote:
On 04/09/2021 22:00, Dmitry A. Kazakov wrote:
On 2021-09-04 22:19, Bart wrote:
[Original DAK post lost]
DAK:
On 2021-09-04 02:49, Bart wrote:
like mine.There's a place for your languages, and there's a place for ones
Nobody forces you to implement any feature. The grapes are sour.
There is a certain amount of satisfaction in having, over exactly 40
years now, used almost exclusively my own languages, implementations
and tools, and all self-hosted.
One key to doing that is to be selective about what features I've
added. I've kept things simple, managable and understandable. Which
is not always easy.
Your (DAK's) role seems to be the opposite: turn everything into
something harder, less manageable, and incomprehensible!
Right, different priorities. Simplicity for the compiler designer
translates into complexity for the programmer.
You might want to look back at the fragments of my language that I've
posted. Such as 'print x'.
I do not need it. But if you ask, print x with the sign as a ternary
number aligned at the right margin of the field of 10 character width,
and do not forget it must be superscript.
On 05/09/2021 08:32, Dmitry A. Kazakov wrote:
On 2021-09-04 23:37, Bart wrote:
On 04/09/2021 22:00, Dmitry A. Kazakov wrote:
On 2021-09-04 22:19, Bart wrote:
[Original DAK post lost]
DAK:
On 2021-09-04 02:49, Bart wrote:
ones like mine.There's a place for your languages, and there's a place for
Nobody forces you to implement any feature. The grapes are sour. >>>>>There is a certain amount of satisfaction in having, over exactly
40 years now, used almost exclusively my own languages,
implementations and tools, and all self-hosted.
One key to doing that is to be selective about what features I've
added. I've kept things simple, managable and understandable. Which
is not always easy.
Your (DAK's) role seems to be the opposite: turn everything into
something harder, less manageable, and incomprehensible!
Right, different priorities. Simplicity for the compiler designer
translates into complexity for the programmer.
You might want to look back at the fragments of my language that I've
posted. Such as 'print x'.
I do not need it. But if you ask, print x with the sign as a ternary
number aligned at the right margin of the field of 10 character width,
and do not forget it must be superscript.
I can manage this:
a:=730
println a
fprintln "|#|", a:"x3 10 jr"
This shows:
730
| 1000001|
I can't do much about superscript; that depends on the capabilities of
the console window I'm using.
On 2021-09-05 12:24, Bart wrote:
730
| 1000001|
Where is the +?
I can't do much about superscript; that depends on the capabilities of
the console window I'm using.
Windows console supports UTF-8. See the code page command CHCP.
On 04/09/2021 21:44, James Harris wrote:
On 30/08/2021 13:08, Bart wrote:
On 30/08/2021 11:23, James Harris wrote:
...
P.plot()
...
Why, in your example, wouldn't the compiler know the type of P?
The compiler would know if P were created and manipulated wholly by
the current function but P could be a formal parameter which could be
one of a number of types. I can think of two cases. One is inheritance
type A
|
type B
then a parameter declared to be of type A could include a function
from either type A or type B. (Or is it the other way round?)
function f(A P)
A given activation of function f could be required to use either A's
or B's version of a function.
I didn't understand that at all. I guess inheritance is not for me.
The other is a variant
typedef C = int or float
function f(C P)
A given activation of function f could have been passed an int or a
float.
Here it's easy to understand your intention. But possibly more
challenging to implement.
That looks a bit like a Haskell type, but if you're not implementing
that, would f be a generic function? Or would C be a variant type along
the lines of C++'s? Will there be overloads of f()?
Just getting the basics working (eg. A+B if both are of type C) would be challenging here.
So perhaps pin down more precisely what features you're thinking of having.
On 04/09/2021 22:35, Bart wrote:
On 04/09/2021 21:44, James Harris wrote:
On 30/08/2021 13:08, Bart wrote:
On 30/08/2021 11:23, James Harris wrote:
...
P.plot()
...
Why, in your example, wouldn't the compiler know the type of P?
The compiler would know if P were created and manipulated wholly by
the current function but P could be a formal parameter which could be
one of a number of types. I can think of two cases. One is inheritance
type A
|
type B
then a parameter declared to be of type A could include a function
from either type A or type B. (Or is it the other way round?)
function f(A P)
A given activation of function f could be required to use either A's
or B's version of a function.
I didn't understand that at all. I guess inheritance is not for me.
Inheritance is maybe not for /anyone/! It encourages composition by tall hierarchies rather than by flattening and simplifying but that's another matter. The model I had in mind for that example is a simple one:
An object of type A has fields Aa, Ab and Ac
An object of type B has all of A's fields and some of its own.
As long as both types of object have A's fields at the same offsets then
code which expects an A can still work on A's fields and doesn't care
whether the object is really of type A or of type B.
...
The other is a variant
typedef C = int or float
function f(C P)
A given activation of function f could have been passed an int or a
float.
Here it's easy to understand your intention. But possibly more
challenging to implement.
Could function f not just use a switch?
switch P.tag
case int
/* int processing */
case float
/* float processing */
end switch
(That switch could either be explicit in the source code or implicit and generated by the compiler.)
That looks a bit like a Haskell type, but if you're not implementing
that, would f be a generic function? Or would C be a variant type
along the lines of C++'s? Will there be overloads of f()?
To answer your questions: in the example, I had in mind C being a
variant type; f could be a simple function and would not have to be overloaded.
I read a nice, simple description the other day:
* Polymorphism is about one function handling multiple types.
* Generics is about producing a custom function for each type.
One could add a variant type (and a switch on the type) to that in order
to achieve the same effect.
Frankly, I'd like the programmer to be able to work purely in terms of
the logic of his algorithm and for the compiler to pick the implementation.
On 09/09/2021 18:25, James Harris wrote:
On 04/09/2021 22:35, Bart wrote:
On 04/09/2021 21:44, James Harris wrote:
type A
|
type B
then a parameter declared to be of type A could include a function
from either type A or type B. (Or is it the other way round?)
function f(A P)
A given activation of function f could be required to use either A's
or B's version of a function.
I didn't understand that at all. I guess inheritance is not for me.
Inheritance is maybe not for /anyone/! It encourages composition by
tall hierarchies rather than by flattening and simplifying but that's
another matter. The model I had in mind for that example is a simple one:
An object of type A has fields Aa, Ab and Ac
An object of type B has all of A's fields and some of its own.
There is also the case where B overrides some fields of A. They might be
of different types and sizes from A's, which makes keeping them at the
same offsets hard, and cause other difficulties in using a value X which
can be of either A or B types.
As long as both types of object have A's fields at the same offsets
then code which expects an A can still work on A's fields and doesn't
care whether the object is really of type A or of type B.
In my dynamic language, none of these matters. The offsets can be
different, as they are picked up at runtime; and the types can be
different anyway.
But I ran into trouble with static code.
The other is a variant
typedef C = int or float
function f(C P)
A given activation of function f could have been passed an int or a
float.
Here it's easy to understand your intention. But possibly more
challenging to implement.
Could function f not just use a switch?
What, for each reference to P inside the body? What about when you have
the expression P+Q+R, each of which has type C; will you need an 8-way switch, or ... ?
I use type-dispatching in dynamic code; I have difficulty seeing how it
could be applied efficiently to static code.
How about looking at an expression such as A:=B+C*D, where these are all variants, and mocking up what generate code might look like. Try also
X[i] where X has variant elements, and i might be variant too.
Or maybe write some C++ code and see what the compiler generates.
Frankly, I'd like the programmer to be able to work purely in terms of
the logic of his algorithm and for the compiler to pick the
implementation.
You might be on the verge of switching to a dynamically typed language!
On 2021-09-03 18:43, James Harris wrote:
On 03/09/2021 16:28, Dmitry A. Kazakov wrote:
On 2021-09-03 16:59, James Harris wrote:
Why not say that each surface has to provide a set of primitives
that other programs can use to draw on an instance of that type of
surface?
Because that set is open ended. OOP allows you
1. adding new primitives (new shapes)
2. using primitives with new surfaces (inheritance)
All counter proposals are static and do not scale in practice, which
is why OOP is used so widely in large projects regardless any
difficulties. There is simple no other way to do it, economically,
there is no other way.
Your example was good in showing multiple dispatch but I'm not so sure
it's a good solution in this case, let alone the only way!
Of course it is. Write a program that renders a list of shapes in a
widget in a way that you would not need to modify each time another developers team creates a new shape or supports another surface.
The rights and wrongs of the approach are OT so all I'll say is that
M*N does not scale and that new shapes can be constructed from
primitives.
You did not thought it through. A set of primitives supported by all
shapes is already a class. This is exactly what a class of shapes is.
Any client of the class expects to find an implementation of any
primitive in any surface.
Remove the class, you will need to write a new client for each surface.
Unless I am missing something the cartesian M*N would be costly and so
even economically, as you mention,
And its existence is a fact of life. Shapes exist, surfaces exist,
drawing exist.
I cannot agree that there is no other way.
Show the other way to implement and use M*N operations safely.
On 09/09/2021 19:10, Bart wrote:
On 09/09/2021 18:25, James Harris wrote:
On 04/09/2021 22:35, Bart wrote:
On 04/09/2021 21:44, James Harris wrote:
...
type A
|
type B
then a parameter declared to be of type A could include a function
from either type A or type B. (Or is it the other way round?)
function f(A P)
A given activation of function f could be required to use either
A's or B's version of a function.
I didn't understand that at all. I guess inheritance is not for me.
Inheritance is maybe not for /anyone/! It encourages composition by
tall hierarchies rather than by flattening and simplifying but that's
another matter. The model I had in mind for that example is a simple
one:
An object of type A has fields Aa, Ab and Ac
An object of type B has all of A's fields and some of its own.
There is also the case where B overrides some fields of A. They might
be of different types and sizes from A's, which makes keeping them at
the same offsets hard, and cause other difficulties in using a value X
which can be of either A or B types.
AIUI in a statically typed language data types are typically not
changed. (They may be in a language which has dynamic typing.) Only the functions will be overrdden, not the data type. So the offsets would not change, and a type B could appear to be a type A.
As long as both types of object have A's fields at the same offsets
then code which expects an A can still work on A's fields and doesn't
care whether the object is really of type A or of type B.
In my dynamic language, none of these matters. The offsets can be
different, as they are picked up at runtime; and the types can be
different anyway.
But I ran into trouble with static code.
Did you try to change the types?
If those are integers I'd have thought /you/ would just expand them all
up ... to 64 bits!
A more typical use than numbers would probably be nodes of different types.
...
I use type-dispatching in dynamic code; I have difficulty seeing how
it could be applied efficiently to static code.
How about looking at an expression such as A:=B+C*D, where these are
all variants, and mocking up what generate code might look like. Try
also X[i] where X has variant elements, and i might be variant too.
1. Convert i to int.
2. Get the address of the element.
What is needed next will be the same as operating on a scalar.
Or maybe write some C++ code and see what the compiler generates.
Why would I want to do that?!
Frankly, I'd like the programmer to be able to work purely in terms
of the logic of his algorithm and for the compiler to pick the
implementation.
You might be on the verge of switching to a dynamically typed language!
Not me!
On 2021-09-04 19:43, James Harris wrote:
On 03/09/2021 16:46, Dmitry A. Kazakov wrote:
On 2021-09-03 16:40, James Harris wrote:
On 03/09/2021 14:39, Dmitry A. Kazakov wrote:
...
Hash functions are pretty quick.
Hash functions are not fast enough to find a call target! I was/am
even concerned about the extra cost of following a double pointer.
Using a hash table would be slower still.
Faster than anything the user would have to write otherwise, in order
to emulate dispatch.
On the contrary, where the scheme I mentioned would have just two
dereferences to get the address of the required function ... your
scheme would have multiple dereferences, a function call, a hash
calculation, and possibly collision resolution.
You did not propose anything for the case of full dispatch. Certainly specialized cases like single dispatch or multi-methods could be handled differently.
The point was, if you do not support this in the language, the
programmer will have to implement it, which is always slower and always
error prone.
Then performance is never a big concern.
I'm really surprised that such a lookup would even be considered. It
would be much slower than a direct function call.
Again, what is the alternative?
GTK is written in C. C has no classes. So GTK emulates them using the
GObject library. Guess, what is faster, C++ implementation of dispatch
or the GObject kludge?
What is your objection to my proposed solution? To be clear, the idea
is to have the object's pointer to link directly to the method table
as follows.
object: |___ptr___|___field0___|___field1___| etc
|
V
|__type___|__reserved__|__ptr__|__ptr__|__ptr__|__ptr__|
Show how this is supposed to work with full multiple dispatch, with multi-methods, with multiple inheritance, with constructors and
destructors, with scalar types.
BTW, there is a book by Bjarne Stroustrup with a detailed discussion regarding implementation of objects and dispatching tables (virtual
tables) in C++.
https://www.stroustrup.com/arm.html
I suggest you read it first in order to get an elementary understanding
of the problematic.
And keep in mind that C++ has single dispatch.
"Virtual function" is C++ term for method.
If that's right is the issue that Ada prohibits virtual functions?
No, Ada's name for virtual function is "primitive operation."
In Ada
procedure Foo (X : T) is
begin
X.Baz; -- Does not dispatch, the type is established to be T >>> end Foo;
So T.Baz has to be in T and cannot be overridden in a subclass of T?
X.Baz does not dispatch in Ada. In C++ X.baz () it does dispatch.
On 03/09/2021 18:27, Dmitry A. Kazakov wrote:
On 2021-09-03 18:43, James Harris wrote:
On 03/09/2021 16:28, Dmitry A. Kazakov wrote:
On 2021-09-03 16:59, James Harris wrote:
Why not say that each surface has to provide a set of primitives
that other programs can use to draw on an instance of that type of
surface?
Because that set is open ended. OOP allows you
1. adding new primitives (new shapes)
2. using primitives with new surfaces (inheritance)
All counter proposals are static and do not scale in practice, which
is why OOP is used so widely in large projects regardless any
difficulties. There is simple no other way to do it, economically,
there is no other way.
Your example was good in showing multiple dispatch but I'm not so
sure it's a good solution in this case, let alone the only way!
Of course it is. Write a program that renders a list of shapes in a
widget in a way that you would not need to modify each time another
developers team creates a new shape or supports another surface.
Sure. I would have surfaces support different interfaces: pixmap, vector
and whatever else was needed, and have a basic set of component shapes
that surfaces could draw on themselves: lines, triangles, rectangles, ellipses, etc, and bit blit. Then widgets could then use those
primitives to do all required drawing of basic and complex shapes.
The rights and wrongs of the approach are OT so all I'll say is that
M*N does not scale and that new shapes can be constructed from
primitives.
You did not thought it through. A set of primitives supported by all
shapes is already a class. This is exactly what a class of shapes is.
Any client of the class expects to find an implementation of any
primitive in any surface.
I don't know what that means but composite shapes can be made up of
primitive ones. I suspect, in fact, that that's how rendering engines
work: a set of primitives that the hardware or firmware knows how to
render and widget which know how to use those primitives to draw
everything they want to.
Remove the class, you will need to write a new client for each surface.
I disagree. As above, I'd use a common interface.
Unless I am missing something the cartesian M*N would be costly and
so even economically, as you mention,
And its existence is a fact of life. Shapes exist, surfaces exist,
drawing exist.
I cannot agree that there is no other way.
Show the other way to implement and use M*N operations safely.
See above.
On 04/09/2021 20:56, Dmitry A. Kazakov wrote:
On 2021-09-04 19:43, James Harris wrote:
On 03/09/2021 16:46, Dmitry A. Kazakov wrote:
On 2021-09-03 16:40, James Harris wrote:
On 03/09/2021 14:39, Dmitry A. Kazakov wrote:
...
Hash functions are pretty quick.
Hash functions are not fast enough to find a call target! I was/am
even concerned about the extra cost of following a double pointer.
Using a hash table would be slower still.
Faster than anything the user would have to write otherwise, in
order to emulate dispatch.
On the contrary, where the scheme I mentioned would have just two
dereferences to get the address of the required function ... your
scheme would have multiple dereferences, a function call, a hash
calculation, and possibly collision resolution.
You did not propose anything for the case of full dispatch. Certainly
specialized cases like single dispatch or multi-methods could be
handled differently.
If by 'full dispatch' you mean selecting a function based on the types
of all of its arguments then I don't remember you proposing a mechanism
for it.
Yes, you spoke about something that could be used in toy
situations where you had two or three parameters and a handful of types
but it would not scale. If you had just five parameters which could be
of a dozen types. You would need a dispatching table of 5^12 or 244
million pointers to who knows how many functions. That is fantasyland.
Then performance is never a big concern.
I'm really surprised that such a lookup would even be considered. It
would be much slower than a direct function call.
Again, what is the alternative?
I have some ideas but I'd need to see some examples of what the problems really are before coming up with a concrete proposal.
GTK is written in C. C has no classes. So GTK emulates them using the
GObject library. Guess, what is faster, C++ implementation of dispatch
or the GObject kludge?
Straw man!
https://en.wikipedia.org/wiki/Straw_man
Your argument reminds me of how a certain graphics system got its
reputation completely trashed (unfairly, I gather) by a slow
implementation.
What is your objection to my proposed solution? To be clear, the idea
is to have the object's pointer to link directly to the method table
as follows.
object: |___ptr___|___field0___|___field1___| etc
|
V
|__type___|__reserved__|__ptr__|__ptr__|__ptr__|__ptr__|
Show how this is supposed to work with full multiple dispatch, with
multi-methods, with multiple inheritance, with constructors and
destructors, with scalar types.
Those are /your/ desires.
I /would/ support hooks such as constructors and destructors
BTW, there is a book by Bjarne Stroustrup with a detailed discussion
regarding implementation of objects and dispatching tables (virtual
tables) in C++.
https://www.stroustrup.com/arm.html
I suggest you read it first in order to get an elementary
understanding of the problematic.
If I ever start to think like Stroustrup I will have failed.
"Virtual function" is C++ term for method.
OK, though I cannot imagine what's virtual about a normal method.
In Ada
procedure Foo (X : T) is
begin
X.Baz; -- Does not dispatch, the type is established to be T >>>> end Foo;
So T.Baz has to be in T and cannot be overridden in a subclass of T?
X.Baz does not dispatch in Ada. In C++ X.baz () it does dispatch.
I am confused by your terms. When you say 'dispatch' do you mean
dispatch at run time aka dynamic dispatch
https://en.wikipedia.org/wiki/Dynamic_dispatch
as opposed to dispatch at compile time or static dispatch
https://en.wikipedia.org/wiki/Static_dispatch
On 29/08/2021 20:32, Dmitry A. Kazakov wrote:
On 2021-08-29 20:24, James Harris wrote:
Ignoring the length part (as some objects would have their own length
and some would not but that difference is immaterial here) how would a
'tag' system be laid out in memory and how would one use that to
locate a method?
You must consider the following inheritance variants:...
Thanks for the information, Dmitry. I might come back to you about some
of that. But for this thread I was wondering how the memory might be
laid out under the tag model that you mentioned. What form does a tag
take and how does the runtime get from the tag to a method?
If it depends on which model is in use then what about single
inheritance and the Full MI model you mentioned?
On 09/09/2021 19:32, James Harris wrote:
On 09/09/2021 19:10, Bart wrote:
On 09/09/2021 18:25, James Harris wrote:
On 04/09/2021 22:35, Bart wrote:
On 04/09/2021 21:44, James Harris wrote:
type A
|
type B
An object of type B has all of A's fields and some of its own.
So the offsets
would not change, and a type B could appear to be a type A.
Where is that written?
Exactly how different can variant types be? Maybe X is an int, and maybe
X is an array of structs. Or an array of Variants. What are the
limitations?
Staying with my X[i] where X's type is array of Variant elements, they
could be ints, floats or strings. And maybe i is a 32-int, or 64-bit
int, or a float.
You need to analyse it each time is is accessed to see if conversion is needed.
Also, Variants could presumably be assigned something else of a
different type? Then you can't even assume it will stay the same type.
Or maybe write some C++ code and see what the compiler generates.
Why would I want to do that?!
To see what limitations it places on the possibilities. Or maybe there
are no limitations and the resulting code is horrendous.
I don't quite understand why you seem to think that dealing with variant types is straightforward.
Frankly, I'd like the programmer to be able to work purely in terms
of the logic of his algorithm and for the compiler to pick the
implementation.
You might be on the verge of switching to a dynamically typed language!
Not me!
If you have a fully flexible Variant type with no limitations, then you
will de facto have a language that supports dynamic typing.
Try the following set of slides. They are part of a compilers course and
show how such objects could be implemented, including how they could be
laid out in memory.
http://openclassroom.stanford.edu/MainFolder/courses/Compilers/docs/slides/12-06-object-layout-annotated.pdf
Keeping the fields is a natural consequence of the "is a" relationship
when using static typing. An object of type B _is_ also an object of
type A so it has to be usable wherever an object of type A is usable
and/or has been declared.
On 09/09/2021 22:04, Bart wrote:
Also, Variants could presumably be assigned something else of a
different type? Then you can't even assume it will stay the same type.
Wouldn't code which handles variants typically include a switch on the
type as in the following?
switch T.type
case int:
/* Handle int */
case array float:
/* Handle array of float */
end switch /* Ignoring other cases */
On 22/09/2021 15:24, James Harris wrote:
On 09/09/2021 22:04, Bart wrote:
Also, Variants could presumably be assigned something else of a
different type? Then you can't even assume it will stay the same type.
Wouldn't code which handles variants typically include a switch on the
type as in the following?
switch T.type
case int:
/* Handle int */
case array float:
/* Handle array of float */
end switch /* Ignoring other cases */
You need to show an example of source code where such types are used,
On 2021-09-22 21:47, Bart wrote:
On 22/09/2021 15:24, James Harris wrote:
On 09/09/2021 22:04, Bart wrote:
Also, Variants could presumably be assigned something else of a
different type? Then you can't even assume it will stay the same type.
Wouldn't code which handles variants typically include a switch on
the type as in the following?
switch T.type
case int:
/* Handle int */
case array float:
/* Handle array of float */
end switch /* Ignoring other cases */
You need to show an example of source code where such types are used,
Come on, is that a joke?
https://docs.microsoft.com/en-us/windows/win32/api/oaidl/ns-oaidl-variant
http://documentation.unified-automation.com/uasdkcpp/1.5.2/html/structOpcUa__Variant.html
https://en.wikipedia.org/wiki/Tagged_union
https://zone.ni.com/reference/en-XX/help/371361R-01/lvhowto/variants/
Next you will ask for examples of loops?
On 22/09/2021 15:24, James Harris wrote:
On 09/09/2021 22:04, Bart wrote:
Also, Variants could presumably be assigned something else of a
different type? Then you can't even assume it will stay the same type.
Wouldn't code which handles variants typically include a switch on the
type as in the following?
switch T.type
case int:
/* Handle int */
case array float:
/* Handle array of float */
end switch /* Ignoring other cases */
You need to show an example of source code where such types are used,
and see that the generated code might be like.
But let's take your example:
type U = int |[]float
U a, b, c
a := b + c
Here, it knows that b and c are both of type U, but doesn't know - at compile-time - whether they are ints or arrays.
So just to do b+c, it will need 2-way dispatch:
int temp
if b.type=c.type=int then # Hey, a chained compare example! temp = b.ivalue+c.ivalue
else
.... # error? Unless "+" is defined for float arrays, array+iny
end
Now to assign result to a:
if a.type=float_array then
// destroy resources, or decr reference count for array
end
a.type = int
a.ivalue = temp
And that's just for one valid combination, and one type of temp, so that
'a' is only ever going to be assigned an int. How about:
type U = int32 | int64 | float32 | float64
U a, b, c, d, e
a = b*c + d*e
All 16 combinations of types should be valid, but do you want to try
writing the switch statements for them?
I don't think this amount of boilerplate is practical to write as inlinecode.
You should however try doing so for example fragments of code to see as
I suggested to see what is involved.
This type of variant means that:
a := a[i]
could be legal. Anything goes.
That Switch example is meaningless without knowing what operation is
being performed in the source code, hence the request for the
corresponding fragment of source code.
On 2021-09-23 00:13, Bart wrote:
That Switch example is meaningless without knowing what operation is
being performed in the source code, hence the request for the
corresponding fragment of source code.
The predefined operations should be:
1. Equality/inequality
2. Access to the constraint (the selector value, read-only)
3. Member access (the variant value, read/write)
4. Aggregate (literal value)
5. Assignment as a whole, IFF variant is definite, otherwise, no
assignment.
On 22/09/2021 21:47, Bart wrote:
On 22/09/2021 15:24, James Harris wrote:
On 09/09/2021 22:04, Bart wrote:
Also, Variants could presumably be assigned something else of a
different type? Then you can't even assume it will stay the same type.
Wouldn't code which handles variants typically include a switch on the
type as in the following?
switch T.type
case int:
/* Handle int */
case array float:
/* Handle array of float */
end switch /* Ignoring other cases */
You need to show an example of source code where such types are used,
and see that the generated code might be like.
Variant types are typically implemented by having a "type" field - an
integer that says which of the allowed types is currently valid. In
C++, this is std::variant. In Pascal, it is a "tagged union" or
"variant record".
In C, you might have :
typedef struct {
enum { T_is_int, T_is_float } type_index;
union {
int int_value;
float float_value;
}
} T;
switch (t.type_index) {
case T_is_int :
// Use t.int_value
break;
case T_is_float :
// Use t.float_value
break;
}
You're assuming it is just a tagged union or sumtype, where all you can
do is write explicit code to extract one of the possible values.
On 23/09/2021 10:14, Dmitry A. Kazakov wrote:
On 2021-09-23 00:13, Bart wrote:
That Switch example is meaningless without knowing what operation is
being performed in the source code, hence the request for the
corresponding fragment of source code.
The predefined operations should be:
1. Equality/inequality
2. Access to the constraint (the selector value, read-only)
3. Member access (the variant value, read/write)
4. Aggregate (literal value)
5. Assignment as a whole, IFF variant is definite, otherwise, no
assignment.
But you can't do A+B?
What do you have to do in a language with YOUR idea of a variant, in
order to evaluate A+B? Assuming it can be done, what goes on behind the scenes?
If you have a variant which can include suitable types, tell me why any
of the following operations, which I generally allow, should not apply
to variants.
On 2021-09-23 11:38, Bart wrote:
On 23/09/2021 10:14, Dmitry A. Kazakov wrote:
On 2021-09-23 00:13, Bart wrote:
That Switch example is meaningless without knowing what operation is
being performed in the source code, hence the request for the
corresponding fragment of source code.
The predefined operations should be:
1. Equality/inequality
2. Access to the constraint (the selector value, read-only)
3. Member access (the variant value, read/write)
4. Aggregate (literal value)
5. Assignment as a whole, IFF variant is definite, otherwise, no
assignment.
But you can't do A+B?
Because it is not defined. If the programmer wants "+", Foo, Boo he can declare and implement them as with any other type.
What do you have to do in a language with YOUR idea of a variant, in
order to evaluate A+B? Assuming it can be done, what goes on behind
the scenes?
Nothing. What you see is what you get.
If you have a variant which can include suitable types, tell me why
any of the following operations, which I generally allow, should not
apply to variants.
Tell me why they should. Days of PL/1 are gone.
On 23/09/2021 11:22, Dmitry A. Kazakov wrote:
On 2021-09-23 11:38, Bart wrote:
On 23/09/2021 10:14, Dmitry A. Kazakov wrote:
On 2021-09-23 00:13, Bart wrote:
That Switch example is meaningless without knowing what operation
is being performed in the source code, hence the request for the
corresponding fragment of source code.
The predefined operations should be:
1. Equality/inequality
2. Access to the constraint (the selector value, read-only)
3. Member access (the variant value, read/write)
4. Aggregate (literal value)
5. Assignment as a whole, IFF variant is definite, otherwise, no
assignment.
But you can't do A+B?
Because it is not defined. If the programmer wants "+", Foo, Boo he
can declare and implement them as with any other type.
Show me. Let's say with a variant that can be an Integer, or a String.
If you have a variant which can include suitable types, tell me why
any of the following operations, which I generally allow, should not
apply to variants.
Tell me why they should. Days of PL/1 are gone.
Most languages allows all those ops, or a big subset.
You seem to be saying that A+B is allowed when A and B are both int types.
But not when A and B are variant types, even if that type is (Int |
Real), not without a lot of work.
Then that's not a Variant type as I understand it. It's just a union or tagged union, a much cruder type.
On 2021-09-23 12:40, Bart wrote:
On 23/09/2021 11:22, Dmitry A. Kazakov wrote:
On 2021-09-23 11:38, Bart wrote:
On 23/09/2021 10:14, Dmitry A. Kazakov wrote:
On 2021-09-23 00:13, Bart wrote:
That Switch example is meaningless without knowing what operation
is being performed in the source code, hence the request for the
corresponding fragment of source code.
The predefined operations should be:
1. Equality/inequality
2. Access to the constraint (the selector value, read-only)
3. Member access (the variant value, read/write)
4. Aggregate (literal value)
5. Assignment as a whole, IFF variant is definite, otherwise, no
assignment.
But you can't do A+B?
Because it is not defined. If the programmer wants "+", Foo, Boo he
can declare and implement them as with any other type.
Show me. Let's say with a variant that can be an Integer, or a String.
function "+" (Left, Right : Variant) return Variant;
If you have a variant which can include suitable types, tell me why
any of the following operations, which I generally allow, should not
apply to variants.
Tell me why they should. Days of PL/1 are gone.
Most languages allows all those ops, or a big subset.
None I know of.
You seem to be saying that A+B is allowed when A and B are both int
types.
But not when A and B are variant types, even if that type is (Int |
Real), not without a lot of work.
Again, what is the reason why?
Then that's not a Variant type as I understand it. It's just a union
or tagged union, a much cruder type.
You confuse a variant type with an ad-hoc class of types.
On 23/09/2021 12:15, Dmitry A. Kazakov wrote:
On 2021-09-23 12:40, Bart wrote:
On 23/09/2021 11:22, Dmitry A. Kazakov wrote:
On 2021-09-23 11:38, Bart wrote:
On 23/09/2021 10:14, Dmitry A. Kazakov wrote:
On 2021-09-23 00:13, Bart wrote:
That Switch example is meaningless without knowing what operation >>>>>>> is being performed in the source code, hence the request for the >>>>>>> corresponding fragment of source code.
The predefined operations should be:
1. Equality/inequality
2. Access to the constraint (the selector value, read-only)
3. Member access (the variant value, read/write)
4. Aggregate (literal value)
5. Assignment as a whole, IFF variant is definite, otherwise, no
assignment.
But you can't do A+B?
Because it is not defined. If the programmer wants "+", Foo, Boo he
can declare and implement them as with any other type.
Show me. Let's say with a variant that can be an Integer, or a String.
function "+" (Left, Right : Variant) return Variant;
So, what does that do?
Does it magically allow A+B?
If not, then what's the point?
(Also, what is the 'Variant' in your example; is it the name of a
specific user type, so N such declarations are needed, or is it a
generic 'variant' so that this declaration suffices for all possible
types?)
Suppose, also, that variant A is (int | real), and variant B is (real | string)'; can I do A+B? (Here A,B represent types.)
But not when A and B are variant types, even if that type is (Int |
Real), not without a lot of work.
Again, what is the reason why?
Why what? Why I should be able to do A+B?
If not, then what is the purpose of a Variant type, other than as a
glorified union.
Then that's not a Variant type as I understand it. It's just a union
or tagged union, a much cruder type.
You confuse a variant type with an ad-hoc class of types.
James' proposal looks to me like an ad-hoc collection of types, within
an umbrella type.
It might also be specifically created to get away from terms like class,
and member and inheritance!
On 2021-09-23 13:34, Bart wrote:
If not, then what is the purpose of a Variant type, other than as a
glorified union.
None, variant is exactly a tagged union AKA variant record.
Then that's not a Variant type as I understand it. It's just a union
or tagged union, a much cruder type.
You confuse a variant type with an ad-hoc class of types.
James' proposal looks to me like an ad-hoc collection of types, within
an umbrella type.
That is not variant, that is an ad-hoc class with inheritance and all
bells and whistles. He may not understand that or be in denial, but that
is his problem.
It might also be specifically created to get away from terms like
class, and member and inheritance!
He has his delusions you have yours.
On 23/09/2021 08:10, David Brown wrote:
On 22/09/2021 21:47, Bart wrote:
On 22/09/2021 15:24, James Harris wrote:
On 09/09/2021 22:04, Bart wrote:
Also, Variants could presumably be assigned something else of aWouldn't code which handles variants typically include a switch on the >>>> type as in the following?
different type? Then you can't even assume it will stay the same type. >>>>
switch T.type
case int:
/* Handle int */
case array float:
/* Handle array of float */
end switch /* Ignoring other cases */
You need to show an example of source code where such types are used,
and see that the generated code might be like.
Variant types are typically implemented by having a "type" field - an
integer that says which of the allowed types is currently valid. In
C++, this is std::variant. In Pascal, it is a "tagged union" or
"variant record".
In C, you might have :
typedef struct {
enum { T_is_int, T_is_float } type_index;
union {
int int_value;
float float_value;
}
} T;
switch (t.type_index) {
case T_is_int :
// Use t.int_value
break;
case T_is_float :
// Use t.float_value
break;
}
No one has yet explained exactly what kind of variant they are talking
about.
You're assuming it is just a tagged union or sumtype, where all you can
do is write explicit code to extract one of the possible values.
Or if you try and use one of those values directly (t.int_value) the
compiler will perform a behind the scenes check that it is an int.
The C++ variant might do a little bit more.
The kind of variant I've used in dynamic code is considerably more
powerful:
for x in (100, 23.4, "cat", 50..60, [7,8,9], (100,110,120)) do println x.type, "\t", x
end
This is the proposed feature in action:
type T = int | real | string
function biggerof (T a, b)=>T =
if a>b then
return a
else
return b
fi
end
On 23/09/2021 14:57, Dmitry A. Kazakov wrote:
On 2021-09-23 15:47, Bart wrote:
This is the proposed feature in action:
type T = int | real | string
function biggerof (T a, b)=>T =
if a>b then
return a
else
return b
fi
end
Type inference, when the interfaces of the class members are somehow
combined = non-starter.
It is impossible to infer semantics from interfaces. No need to discuss.
I don't really know what you're talking about. Such a feature works fine
in dynamic code:
On 2021-09-23 15:47, Bart wrote:
This is the proposed feature in action:
type T = int | real | string
function biggerof (T a, b)=>T =
if a>b then
return a
else
return b
fi
end
Type inference, when the interfaces of the class members are somehow
combined = non-starter.
It is impossible to infer semantics from interfaces. No need to discuss.
On 2021-09-23 17:23, Bart wrote:
On 23/09/2021 15:48, Dmitry A. Kazakov wrote:
On 2021-09-23 16:31, Bart wrote:
On 23/09/2021 14:57, Dmitry A. Kazakov wrote:
On 2021-09-23 15:47, Bart wrote:
This is the proposed feature in action:
type T = int | real | string
function biggerof (T a, b)=>T =
if a>b then
return a
else
return b
fi
end
Type inference, when the interfaces of the class members are
somehow combined = non-starter.
It is impossible to infer semantics from interfaces. No need to
discuss.
I don't really know what you're talking about. Such a feature works
fine in dynamic code:
You just have illustrated the point by providing a broken program
that has a type error at run time.
It has an /error/ at runtime.
Not an error, a type error = the language's type system is broken, as expected.
On 2021-09-23 16:31, Bart wrote:
On 23/09/2021 14:57, Dmitry A. Kazakov wrote:
On 2021-09-23 15:47, Bart wrote:
This is the proposed feature in action:
type T = int | real | string
function biggerof (T a, b)=>T =
if a>b then
return a
else
return b
fi
end
Type inference, when the interfaces of the class members are somehow
combined = non-starter.
It is impossible to infer semantics from interfaces. No need to discuss.
I don't really know what you're talking about. Such a feature works
fine in dynamic code:
You just have illustrated the point by providing a broken program that
has a type error at run time.
On 23/09/2021 15:48, Dmitry A. Kazakov wrote:
On 2021-09-23 16:31, Bart wrote:
On 23/09/2021 14:57, Dmitry A. Kazakov wrote:
On 2021-09-23 15:47, Bart wrote:
This is the proposed feature in action:
type T = int | real | string
function biggerof (T a, b)=>T =
if a>b then
return a
else
return b
fi
end
Type inference, when the interfaces of the class members are somehow
combined = non-starter.
It is impossible to infer semantics from interfaces. No need to
discuss.
I don't really know what you're talking about. Such a feature works
fine in dynamic code:
You just have illustrated the point by providing a broken program that
has a type error at run time.
It has an /error/ at runtime.
On 23/09/2021 16:47, Dmitry A. Kazakov wrote:
On 2021-09-23 17:23, Bart wrote:
On 23/09/2021 15:48, Dmitry A. Kazakov wrote:
On 2021-09-23 16:31, Bart wrote:
On 23/09/2021 14:57, Dmitry A. Kazakov wrote:
On 2021-09-23 15:47, Bart wrote:
This is the proposed feature in action:
type T = int | real | string
function biggerof (T a, b)=>T =
if a>b then
return a
else
return b
fi
end
Type inference, when the interfaces of the class members are
somehow combined = non-starter.
It is impossible to infer semantics from interfaces. No need to
discuss.
I don't really know what you're talking about. Such a feature works
fine in dynamic code:
You just have illustrated the point by providing a broken program
that has a type error at run time.
It has an /error/ at runtime.
Not an error, a type error = the language's type system is broken, as
expected.
The language has one type: a 'variant'. A and B will ALWAYS be variants,
so they can never have the wrong type.
On 23/09/2021 19:02, Dmitry A. Kazakov wrote:
On 2021-09-23 18:05, Bart wrote:
The language has one type: a 'variant'. A and B will ALWAYS be
variants, so they can never have the wrong type.
They do since you infer operations from member types. That cannot work
and it does not.
Properly designed languages inherit interfaces rather than infer them.
Interface lists operations applicable to the type.
This allows to ensure existence of implementations of all operations
at compile time, statically, so that no type error (AKA "operation not
understood") may ever occur.
So tagged unions are no good either. Since you don't know their exact
active type until runtime.
On 2021-09-23 18:05, Bart wrote:
The language has one type: a 'variant'. A and B will ALWAYS be
variants, so they can never have the wrong type.
They do since you infer operations from member types. That cannot work
and it does not.
Properly designed languages inherit interfaces rather than infer them. Interface lists operations applicable to the type.
This allows to ensure existence of implementations of all operations at compile time, statically, so that no type error (AKA "operation not understood") may ever occur.
On 2021-09-23 20:29, Bart wrote:
On 23/09/2021 19:02, Dmitry A. Kazakov wrote:
On 2021-09-23 18:05, Bart wrote:
The language has one type: a 'variant'. A and B will ALWAYS be
variants, so they can never have the wrong type.
They do since you infer operations from member types. That cannot
work and it does not.
Properly designed languages inherit interfaces rather than infer
them. Interface lists operations applicable to the type.
This allows to ensure existence of implementations of all operations
at compile time, statically, so that no type error (AKA "operation
not understood") may ever occur.
So tagged unions are no good either. Since you don't know their exact
active type until runtime.
Why do I need that? Again, all interfaces are manifested and static and
apply whatever value of the selector. See?
You still do not understand basic intertype relationships. The relation between the variant type T having int as a choice is aggregation has-a.
T does not inherit anything from int, so it does not have + of int's interface because it is has-a.
https://en.wikipedia.org/wiki/Has-a
This is what languages having tagged unions provide.
What you unsuccessfully trying to figure out is a very different thing,
is-a.
https://en.wikipedia.org/wiki/Is-a
That is when the type T inherits from int, and maybe from float and
maybe from string. So that an instance of T may play an int, or a float,
or a string.
This is not what the variant type is. This is multiple inheritance and class-wide object which actual tag indicates the actual specific type
int, float, string. That one could inherit + from int, + from float,
nothing from string. Then in a decent language you would have to
reconcile inherited interfaces (like conflicting +'s) and provide implementations, which in the case of + would be a multi-method with all niceties it brings.
That both variant and class-wide objects have similar representations
with a tag indicating the actual choice or the specific type does not
make them same. Your obsession with representations prevents you from
seeing the whole picture and turns everything upside down.
On 23/09/2021 20:12, Dmitry A. Kazakov wrote:
On 2021-09-23 20:29, Bart wrote:
On 23/09/2021 19:02, Dmitry A. Kazakov wrote:
On 2021-09-23 18:05, Bart wrote:
The language has one type: a 'variant'. A and B will ALWAYS be
variants, so they can never have the wrong type.
They do since you infer operations from member types. That cannot
work and it does not.
Properly designed languages inherit interfaces rather than infer
them. Interface lists operations applicable to the type.
This allows to ensure existence of implementations of all operations
at compile time, statically, so that no type error (AKA "operation
not understood") may ever occur.
So tagged unions are no good either. Since you don't know their exact
active type until runtime.
Why do I need that? Again, all interfaces are manifested and static and apply whatever value of the selector. See?
You still do not understand basic intertype relationships. The relation between the variant type T having int as a choice is aggregation has-a.
T does not inherit anything from int, so it does not have + of int's interface because it is has-a.
https://en.wikipedia.org/wiki/Has-a
This is what languages having tagged unions provide.
What you unsuccessfully trying to figure out is a very different thing, is-a.
https://en.wikipedia.org/wiki/Is-a
That is when the type T inherits from int, and maybe from float and
maybe from string. So that an instance of T may play an int, or a float, or a string.
This is not what the variant type is. This is multiple inheritance and class-wide object which actual tag indicates the actual specific type
int, float, string. That one could inherit + from int, + from float, nothing from string. Then in a decent language you would have to
reconcile inherited interfaces (like conflicting +'s) and provide implementations, which in the case of + would be a multi-method with all niceties it brings.
That both variant and class-wide objects have similar representationsAnd yours is an obsession with classes and inheritance and the usual OO nonsense.
with a tag indicating the actual choice or the specific type does not
make them same. Your obsession with representations prevents you from seeing the whole picture and turns everything upside down.
Your links meant nothing to me; I saw it as result of people with too
much time on the hands inventing all this pointless jargon.
There is nothing to stop me calling my type-tags something entirely different, so that my variants are just user data structures, and my interpreter is just another application.
So, are you then going to tell how me I should design my applications?
Have a look at a file system some time. Files are blocks of data with
tags (maybe the extension, maybe an internal signature), which tell you something about the data they contains.
An OS shell program issues commands on files. The command looks at the
file types and decides how to apply the command, which might be between
two files.
Just like my variant system.
Are you going to tell the people who write such file systems that they
got it all wrong too?
On 23/09/2021 20:12, Dmitry A. Kazakov wrote:
On 2021-09-23 20:29, Bart wrote:
On 23/09/2021 19:02, Dmitry A. Kazakov wrote:
On 2021-09-23 18:05, Bart wrote:
The language has one type: a 'variant'. A and B will ALWAYS be
variants, so they can never have the wrong type.
They do since you infer operations from member types. That cannot
work and it does not.
Properly designed languages inherit interfaces rather than infer
them. Interface lists operations applicable to the type.
This allows to ensure existence of implementations of all operations
at compile time, statically, so that no type error (AKA "operation
not understood") may ever occur.
So tagged unions are no good either. Since you don't know their exact
active type until runtime.
Why do I need that? Again, all interfaces are manifested and static
and apply whatever value of the selector. See?
You still do not understand basic intertype relationships. The
relation between the variant type T having int as a choice is
aggregation has-a. T does not inherit anything from int, so it does
not have + of int's interface because it is has-a.
https://en.wikipedia.org/wiki/Has-a
This is what languages having tagged unions provide.
What you unsuccessfully trying to figure out is a very different
thing, is-a.
https://en.wikipedia.org/wiki/Is-a
That is when the type T inherits from int, and maybe from float and
maybe from string. So that an instance of T may play an int, or a
float, or a string.
This is not what the variant type is. This is multiple inheritance and
class-wide object which actual tag indicates the actual specific type
int, float, string. That one could inherit + from int, + from float,
nothing from string. Then in a decent language you would have to
reconcile inherited interfaces (like conflicting +'s) and provide
implementations, which in the case of + would be a multi-method with
all niceties it brings.
That both variant and class-wide objects have similar representations
with a tag indicating the actual choice or the specific type does not
make them same. Your obsession with representations prevents you from
seeing the whole picture and turns everything upside down.
And yours is an obsession with classes and inheritance and the usual OO nonsense.
Your links meant nothing to me; I saw it as result of people with too
much time on the hands inventing all this pointless jargon.
There is nothing to stop me calling my type-tags something entirely different, so that my variants are just user data structures, and my interpreter is just another application.
So, are you then going to tell how me I should design my applications?
Have a look at a file system some time. Files are blocks of data with
tags (maybe the extension, maybe an internal signature), which tell you something about the data they contains.
On 22/09/2021 15:24, James Harris wrote:
On 09/09/2021 22:04, Bart wrote:
Also, Variants could presumably be assigned something else of a
different type? Then you can't even assume it will stay the same type.
Wouldn't code which handles variants typically include a switch on the
type as in the following?
switch T.type
case int:
/* Handle int */
case array float:
/* Handle array of float */
end switch /* Ignoring other cases */
You need to show an example of source code where such types are used,
and see that the generated code might be like.
But let's take your example:
type U = int |[]float
if b.type=c.type=int then # Hey, a chained compare example!
type U = int32 | int64 | float32 | float64
U a, b, c, d, e
a = b*c + d*e
All 16 combinations of types should be valid, but do you want to try
writing the switch statements for them?
I don't think this amount of boilerplate is practical to write as inline code.
You should however try doing so for example fragments of code to see as
I suggested to see what is involved.
This type of variant means that:
a := a[i]
could be legal. Anything goes.
On 22/09/2021 20:47, Bart wrote:
A good example of a variant is in a tokeniser where the token as read
from the input needs to be categorised into a keyword, a string, a name,
a number, a known and possibly composite symbol, a single character, etc.
I cannot at the moment, however, think of a good example of where code
would have to switch on the combination of two or more variants - which
is the type of code you have been showing. A switch (in the source code)
on a single variant would be more normal, I would think.
On 23/09/2021 11:54, Bart wrote:
No one has yet explained exactly what kind of variant they are talking
about.
You're assuming it is just a tagged union or sumtype, where all you can
do is write explicit code to extract one of the possible values.
That is almost exactly what a variant /is/. But you are missing a few operations - you can also read which type is contained in some manner (perhaps by reading a field, perhaps by pattern matching or a special
type of switch, perhaps by trial-and-error and exceptions), and of
course you can assign values to it.
You seem to be mixing up variants with dynamic typing. Perhaps you have
been playing with Visual Basic, and copied MS's misuse of the term.
'Variants' seem to be mean lots of different things.
variant a, b, c
a := b + c
generated this x64 code (not ABI-compliant):
lea D0, [Dframe+-8]
push D0
call [L5] # init a
lea D0, [Dframe+-16]
push D0
call [L5] # init b
lea D0, [Dframe+-24]
push D0
call [L5] # init c
mov D0, [Dframe+-16] # 'push b'
inc word32 [D0] # step ref count
mov D1, [Dframe+-24] # 'push c'
inc word32 [D1] # step ref count
push D0
push D1
call [L6] # addvar(b, c)
lea D1, [Dframe+-8] # &a
push D0
push D1
call [L7] # 'pop 'a'
So it's all done with function calls; little inline code. And it's inefficient.
But that's how /I/ did it. This might not be what James has in mind.
On 2021-09-23 16:31, Bart wrote:
On 23/09/2021 14:57, Dmitry A. Kazakov wrote:
On 2021-09-23 15:47, Bart wrote:
This is the proposed feature in action:
type T = int | real | string
function biggerof (T a, b)=>T =
if a>b then
return a
else
return b
fi
end
Type inference, when the interfaces of the class members are somehow
combined = non-starter.
It is impossible to infer semantics from interfaces. No need to discuss.
I don't really know what you're talking about. Such a feature works
fine in dynamic code:
You just have illustrated the point by providing a broken program that
has a type error at run time.
On 23/09/2021 15:48, Dmitry A. Kazakov wrote:
On 2021-09-23 16:31, Bart wrote:
On 23/09/2021 14:57, Dmitry A. Kazakov wrote:
On 2021-09-23 15:47, Bart wrote:
This is the proposed feature in action:
type T = int | real | string
function biggerof (T a, b)=>T =
if a>b then
return a
else
return b
fi
end
Type inference, when the interfaces of the class members are somehow
combined = non-starter.
It is impossible to infer semantics from interfaces. No need to
discuss.
I don't really know what you're talking about. Such a feature works
fine in dynamic code:
You just have illustrated the point by providing a broken program that
has a type error at run time.
Run-time errors happen.
Consider a parser which reads the keyword "return" and then expects
either an expression or end-of-statement. The /type/ of the following
token returned by the lexer may, instead, be a keyword. The parser would
then have a type error detected at run time.
Neither a program nor a language is 'broken' as long as the error is detected.
On 2021-09-23 13:34, Bart wrote:
https://en.wikipedia.org/wiki/Tagged_union
James' proposal looks to me like an ad-hoc collection of types, within
an umbrella type.
That is not variant, that is an ad-hoc class with inheritance and all
bells and whistles. He may not understand that or be in denial, but that
is his problem.
It might also be specifically created to get away from terms like
class, and member and inheritance!
He has his delusions you have yours.
On 25/09/2021 16:19, James Harris wrote:
On 22/09/2021 20:47, Bart wrote:
A good example of a variant is in a tokeniser where the token as read
from the input needs to be categorised into a keyword, a string, a
name, a number, a known and possibly composite symbol, a single
character, etc.
That one can just be a tagged union (see below).
I cannot at the moment, however, think of a good example of where code
would have to switch on the combination of two or more variants -
which is the type of code you have been showing. A switch (in the
source code) on a single variant would be more normal, I would think.
I think look at some of my follow-up posts over last few days, since
there might be some confusion over what everyone means by 'variant'.
My idea of it is something that can be used exactly like a fixed type,
which is what happens with dynamic languages.
Some might use it to mean tagged union, with variable amounts of
language support.
(For that kind, I use zero language support. Here's how I express your
token example:
global record lexrec =
union
int64 value
real64 xvalue
word64 uvalue
ichar svalue
ref int128 pvalue128
symbol symptr
end
word32 pos: (sourceoffset:24, moduleno:8)
int16 symbol
int16 subcode
end
Which kind of token it represents is dertermined by the 'symbol' field.
It's been optimised to fit into a 16-byte record, something you have
less control over using language-controlled tagged unions.)
On 23/09/2021 13:20, Dmitry A. Kazakov wrote:
That is not variant, that is an ad-hoc class with inheritance and all
bells and whistles. He may not understand that or be in denial, but
that is his problem.
No, there's not necessarily any class or inheritance. With your OO
mindset you may struggle to understand that but it is so, nonetheless.
It might also be specifically created to get away from terms like
class, and member and inheritance!
He has his delusions you have yours.
Anything which isn't OO is a delusion, eh?
On 22/09/2021 23:13, Bart wrote:
...
'Variants' seem to be mean lots of different things.
...
variant a, b, c
a := b + c
generated this x64 code (not ABI-compliant):
lea D0, [Dframe+-8]
push D0
call [L5] # init a
lea D0, [Dframe+-16]
push D0
call [L5] # init b
lea D0, [Dframe+-24]
push D0
call [L5] # init c
mov D0, [Dframe+-16] # 'push b' >> inc word32 [D0] # step ref count
mov D1, [Dframe+-24] # 'push c' >> inc word32 [D1] # step ref count
push D0
push D1
call [L6] # addvar(b, c)
lea D1, [Dframe+-8] # &a
push D0
push D1
call [L7] # 'pop 'a'
So it's all done with function calls; little inline code. And it's
inefficient.
But that's how /I/ did it. This might not be what James has in mind.
I think the key part of that is the bit you've not shown: how the
variants are combined in addvar.
On 23/09/2021 13:20, Dmitry A. Kazakov wrote:
On 2021-09-23 13:34, Bart wrote:
...
https://en.wikipedia.org/wiki/Tagged_union
...
James' proposal looks to me like an ad-hoc collection of types,
within an umbrella type.
I wouldn't go so far as to call it a proposal but what I have in mind is roughly as Bart describes: an arbitrary collection of types where the
types are explicitly specified by the programmer (as with the typedef
below). An object thereof would be implemented by a tagged or
discriminated union as in the above link with the tag being managed by
the compiler.
Code to handle a certain variant would in general use a switch or an
'if' but cases could be combined. For example, the following code
(somewhat artificially) shares code for int and bool.
typedef answer = int # bool # char
function decrement(answer A)
switch A.type
case int
case bool
A = A - 1
case char
A = char(ord(A) - 1)
end switch
end function
The whole switch construct could be omitted if all cases shared source code
On 25/09/2021 17:05, Bart wrote:
(For that kind, I use zero language support. Here's how I express your
token example:
global record lexrec =
union
int64 value
real64 xvalue
word64 uvalue
ichar svalue
ref int128 pvalue128
symbol symptr
end
word32 pos: (sourceoffset:24, moduleno:8)
int16 symbol
int16 subcode
end
Which kind of token it represents is dertermined by the 'symbol'
field. It's been optimised to fit into a 16-byte record, something you
have less control over using language-controlled tagged unions.)
I agree with all that (though your double use of "symbol" looks
strange).
But I thought you were going to come up with an example of
where TWO variants had to be combined in a single dyadic operation.
Also, in your example it looks as though the tag could be changed
without changing the contents of the union. IMO it's better to require
tag and content to change at the same time, if possible, so as to
maintain type safety.
On 2021-09-25 19:59, James Harris wrote:
On 23/09/2021 15:48, Dmitry A. Kazakov wrote:
On 2021-09-23 16:31, Bart wrote:
On 23/09/2021 14:57, Dmitry A. Kazakov wrote:
It is impossible to infer semantics from interfaces. No need to
discuss.
I don't really know what you're talking about. Such a feature works
fine in dynamic code:
You just have illustrated the point by providing a broken program
that has a type error at run time.
Run-time errors happen.
No, errors do not. At run-time happen exceptions. Type error is a bug, constraint check failure is a valid program state. In a properly
designed language type errors are detected at compile time.
Consider a parser which reads the keyword "return" and then expects
either an expression or end-of-statement. The /type/ of the following
token returned by the lexer may, instead, be a keyword. The parser
would then have a type error detected at run time.
Broken language.
Neither a program nor a language is 'broken' as long as the error is
detected.
By whom and when?
On 25/09/2021 19:44, Dmitry A. Kazakov wrote:
On 2021-09-25 20:22, James Harris wrote:
On 23/09/2021 13:20, Dmitry A. Kazakov wrote:
That is not variant, that is an ad-hoc class with inheritance and
all bells and whistles. He may not understand that or be in denial,
but that is his problem.
No, there's not necessarily any class or inheritance. With your OO
mindset you may struggle to understand that but it is so, nonetheless.
There necessarily is, IFF variant acts as its members. That is the
definition of subtyping,
That's interesting. I see that definition at
https://en.wikipedia.org/wiki/Subtyping
but according to
http://archive.adaic.com/standards/83lrm/html/lrm-03-03.html
"The set of values of a subtype is a subset of the values of the base
type" which I thought of much as day-of-month (1-31) might be a subtype
of integer. Am I wrong?
Why the difference in definitions? Is it because OOP has hijacked
familiar terms?
It might also be specifically created to get away from terms like
class, and member and inheritance!
He has his delusions you have yours.
Anything which isn't OO is a delusion, eh?
Anything that contradicts reality is. OO has nothing to do with all
this. Whatever choice you or Bart do it falls into one of well known
and described categories.
It doesn't help communication if OOP appropriated to itself terms like
class, member and inheritance and gave then new meanings. IIRC even you sometimes speak of a class in different terms from an OOP class.
On 25/09/2021 19:31, Dmitry A. Kazakov wrote:
On 2021-09-25 19:59, James Harris wrote:
On 23/09/2021 15:48, Dmitry A. Kazakov wrote:
On 2021-09-23 16:31, Bart wrote:
On 23/09/2021 14:57, Dmitry A. Kazakov wrote:
...
It is impossible to infer semantics from interfaces. No need to
discuss.
I don't really know what you're talking about. Such a feature works
fine in dynamic code:
You just have illustrated the point by providing a broken program
that has a type error at run time.
Run-time errors happen.
No, errors do not. At run-time happen exceptions. Type error is a bug,
constraint check failure is a valid program state. In a properly
designed language type errors are detected at compile time.
You always get hung up on terminology, Dmitry. Whether you call them
errors or exceptions the issue is the same.
Neither a program nor a language is 'broken' as long as the error is
detected.
By whom and when?
In the context, by the executable code at run time.
On 25/09/2021 19:22, James Harris wrote:
On 23/09/2021 13:20, Dmitry A. Kazakov wrote:
https://en.wikipedia.org/wiki/Tagged_union
Code to handle a certain variant would in general use a switch or an
'if' but cases could be combined. For example, the following code
(somewhat artificially) shares code for int and bool.
typedef answer = int # bool # char
function decrement(answer A)
switch A.type
case int
case bool
A = A - 1
case char
A = char(ord(A) - 1)
end switch
end function
The whole switch construct could be omitted if all cases shared source
code
Does this function exist in the user's source code, or is it part of the implmementation which is called with the user code does:
answer X=1234 # X set to int type and value 1234
--X # X ends up as having int type still and value 1233
If the latter, you will also need something to deal with this line:
A = A - 1 # (A additionally needs to be a reference type)
Also, you need something for ord(A). And also, for assignment, although
that is simple enough to do inline, provided that A's existing type
doesn't need a complex destruct routine.
At some point, there must be code that knows where all the components of
A are stored.
On 2021-09-25 20:22, James Harris wrote:
On 23/09/2021 13:20, Dmitry A. Kazakov wrote:
That is not variant, that is an ad-hoc class with inheritance and all
bells and whistles. He may not understand that or be in denial, but
that is his problem.
No, there's not necessarily any class or inheritance. With your OO
mindset you may struggle to understand that but it is so, nonetheless.
There necessarily is, IFF variant acts as its members. That is the
definition of subtyping,
subtypes form a class, also per definition of.
Standard variant does not do this, it is just an aggregation = has-a.
It might also be specifically created to get away from terms like
class, and member and inheritance!
He has his delusions you have yours.
Anything which isn't OO is a delusion, eh?
Anything that contradicts reality is. OO has nothing to do with all
this. Whatever choice you or Bart do it falls into one of well known and described categories.
Delusion is to believe that if you would not call a spade spade then it
might turn into a Lamborghini.
On 25/09/2021 21:57, Bart wrote:
On 25/09/2021 19:22, James Harris wrote:
On 23/09/2021 13:20, Dmitry A. Kazakov wrote:
https://en.wikipedia.org/wiki/Tagged_union
...
Code to handle a certain variant would in general use a switch or an
'if' but cases could be combined. For example, the following code
(somewhat artificially) shares code for int and bool.
typedef answer = int # bool # char
function decrement(answer A)
switch A.type
case int
case bool
A = A - 1
case char
A = char(ord(A) - 1)
end switch
end function
The whole switch construct could be omitted if all cases shared
source code
Does this function exist in the user's source code, or is it part of
the implmementation which is called with the user code does:
answer X=1234 # X set to int type and value 1234
--X # X ends up as having int type still and value 1233
It was meant to be an outline for a function which could be called as
decrement(X)
On 2021-09-26 09:57, James Harris wrote:
On 25/09/2021 19:31, Dmitry A. Kazakov wrote:
On 2021-09-25 19:59, James Harris wrote:
Neither a program nor a language is 'broken' as long as the error is
detected.
By whom and when?
In the context, by the executable code at run time.
In that context consider an aircraft you are sitting in...
On 2021-09-26 09:57, James Harris wrote:
On 25/09/2021 19:31, Dmitry A. Kazakov wrote:
On 2021-09-25 19:59, James Harris wrote:
On 23/09/2021 15:48, Dmitry A. Kazakov wrote:
On 2021-09-23 16:31, Bart wrote:
On 23/09/2021 14:57, Dmitry A. Kazakov wrote:
...
It is impossible to infer semantics from interfaces. No need to
discuss.
I don't really know what you're talking about. Such a feature
works fine in dynamic code:
You just have illustrated the point by providing a broken program
that has a type error at run time.
Run-time errors happen.
No, errors do not. At run-time happen exceptions. Type error is a
bug, constraint check failure is a valid program state. In a properly
designed language type errors are detected at compile time.
You always get hung up on terminology, Dmitry. Whether you call them
errors or exceptions the issue is the same.
No, it is very important distinction. You can always convert errors into exceptions, by weakening contracts of the interfaces. But that has
direct consequences for the user who must deal with a weaker contracts
and this weakness propagates up the inheritance tree, so that the
interface you would never expect raising errors suddenly does this, e.g. arithmetic + subscript errors.
Neither a program nor a language is 'broken' as long as the error is
detected.
By whom and when?
In the context, by the executable code at run time.
In that context consider an aircraft you are sitting in...
You seems to think that having an impossibly strict type system and
using the right jargon for everything somehow magically eliminates all
bugs.
Most bugs IME are not caused by a too-lax type system.
By whom and when?
In the context, by the executable code at run time.
In that context consider an aircraft you are sitting in...
Aeroplanes seemed to manage perfectly well without computers at all!
On 26/09/2021 09:38, Dmitry A. Kazakov wrote:
On 2021-09-26 09:57, James Harris wrote:
On 25/09/2021 19:31, Dmitry A. Kazakov wrote:
On 2021-09-25 19:59, James Harris wrote:
...
Neither a program nor a language is 'broken' as long as the error
is detected.
By whom and when?
In the context, by the executable code at run time.
In that context consider an aircraft you are sitting in...
try
one path
catch exception
other path
Plane keeps flying.
On 23/09/2021 14:55, David Brown wrote:
On 23/09/2021 11:54, Bart wrote:
...
No one has yet explained exactly what kind of variant they are talking
about.
You're assuming it is just a tagged union or sumtype, where all you can
do is write explicit code to extract one of the possible values.
That is almost exactly what a variant /is/. But you are missing a few
operations - you can also read which type is contained in some manner
(perhaps by reading a field, perhaps by pattern matching or a special
type of switch, perhaps by trial-and-error and exceptions), and of
course you can assign values to it.
You seem to be mixing up variants with dynamic typing. Perhaps you have
been playing with Visual Basic, and copied MS's misuse of the term.
That's an interesting comment. What did MS do wrong and how are you
saying a variant should differ from a dynamic type?
In a language which uses dynamic typing one typically doesn't specify
the type of any object, just using something like
var x
That would allow x to be set to any type. AISI with a variant all one
does is limit the types which x can be, e.g.
(int # bool) x ;x can be of type int or type bool
How that's implemented wouldn't matter to the semantics.
On 26/09/2021 09:52, James Harris wrote:
On 25/09/2021 21:57, Bart wrote:
On 25/09/2021 19:22, James Harris wrote:
Code to handle a certain variant would in general use a switch or an
'if' but cases could be combined. For example, the following code
(somewhat artificially) shares code for int and bool.
typedef answer = int # bool # char
function decrement(answer A)
switch A.type
case int
case bool
A = A - 1
case char
A = char(ord(A) - 1)
end switch
end function
The whole switch construct could be omitted if all cases shared
source code
Does this function exist in the user's source code, or is it part of
the implmementation which is called with the user code does:
answer X=1234 # X set to int type and value 1234
--X # X ends up as having int type still and value 1233
It was meant to be an outline for a function which could be called as
decrement(X)
Then this is just user-code.
So where does the variant magic come in? In your example, it appears to
be in the lines A=A-1, and A=char(ord(A)-1.
Suppose we used the trick where the language transpiles into C, and had
two columns where on the left is what the user writes in the language,
and on the right is what is generated. Then your example could have
looked like this:
New Langauge | Generated C
| enum {Void=0, Int, Bool, Char};
|
type answer = |
int # bool # char | typedef struct {int tag, value;} answer;
|
answer A | answer A = {Void, 0};
|
A = 1234 | A.tag = Int;
| A.value = 1234;
| --A | switch (A.tag) {
| case Int: --A.value; break;
| case Char: --A.value; break;
| case Bool:
| if (A.value) --A.value;
| else /* error */;
| break;
| default: /* error */;
| }
It's not a great example, because it's not challenging: the variant
types aren't different enough to warrant the use of a union, and all the branches of the switch can use the same code (I tried to mix it up with Bool).
I'm using here a global set of enums for the types; but if there are 50
types in all (that MS link used a variant of 50), and your variant uses
only 3, you may want to use ordinals 1, 2, 3 instead of 12, 15, 39.
I've also uses an extra type variation: Void, whose tag has the value
zero, and is used when no value has yet been assumed. This allows such variables in .bss for example, to have a stable type.
Anyway, you can extend my example with more operations on A, but I
suggest the type is changed to:
type answer = int # float # string
(Where string is just a char* type, nothing complicated, and it will
never 'own' its data.)
There is probably no point in having different widths of int, since each instance will have the same size anyway. But then maybe you may want
values limited to 255 or 65535.
On 26/09/2021 09:38, Dmitry A. Kazakov wrote:
On 2021-09-26 09:57, James Harris wrote:
On 25/09/2021 19:31, Dmitry A. Kazakov wrote:
On 2021-09-25 19:59, James Harris wrote:
...
Neither a program nor a language is 'broken' as long as the error
is detected.
By whom and when?
In the context, by the executable code at run time.
In that context consider an aircraft you are sitting in...
try
one path
catch exception
other path
Plane keeps flying.
On 2021-09-26 13:04, Bart wrote:
You seems to think that having an impossibly strict type system and
using the right jargon for everything somehow magically eliminates all
bugs.
Type system eliminates type bugs. Broken type system introduces bugs.
Most bugs IME are not caused by a too-lax type system.
Bugs that can be prevented must be.
By whom and when?
In the context, by the executable code at run time.
In that context consider an aircraft you are sitting in...
Aeroplanes seemed to manage perfectly well without computers at all!
Why do you think electric plugs have different shapes. Isn't that an impossibly strict idea not to allow to insert an audio jack into 230V outlets?
On 26/09/2021 12:34, Dmitry A. Kazakov wrote:
On 2021-09-26 13:04, Bart wrote:
You seems to think that having an impossibly strict type system and
using the right jargon for everything somehow magically eliminates
all bugs.
Type system eliminates type bugs. Broken type system introduces bugs.
It creates extra errors. But which ones are genuine?
I made a change
once which meant that 'ref char' and 'ref byte' were incompatible.
(They were different to allow some conveniences, such as printing a 'ref char' type would assume it was a zero-terminated string.)
But then I had a function 'readfile(filename)=>ref byte', and I wanted
to read a text file like this:
ref char sourcecode := readfile("hello.c")
In dynamic code, I can choose from:
sourcecode := readblockfile("hello.c") # returns a raw ptr
sourcecode := readstrfile("hello.c") # ... a string
sourcecode := readtextfile("hello.c") # ... a list of strings
sourcecode := readbinfile("hello.c") # ... a byte-array
What's the type of 'sourcecode'? Who cares! If I try to use it inappropriately, it will tell me.
But look at the flexibility.
Most bugs IME are not caused by a too-lax type system.
Bugs that can be prevented must be.
Not when that consumes so many resources that you will never finish the program.
By whom and when?
In the context, by the executable code at run time.
In that context consider an aircraft you are sitting in...
Aeroplanes seemed to manage perfectly well without computers at all!
Why do you think electric plugs have different shapes. Isn't that an
impossibly strict idea not to allow to insert an audio jack into 230V
outlets?
There are different processes that lead to the various kinds of
standardised connectors.
If exceeded, the fuse blows and the appliance can't be used until
something is fixed.
On 2021-09-26 15:21, Bart wrote:
On 26/09/2021 12:34, Dmitry A. Kazakov wrote:
On 2021-09-26 13:04, Bart wrote:
You seems to think that having an impossibly strict type system and
using the right jargon for everything somehow magically eliminates
all bugs.
Type system eliminates type bugs. Broken type system introduces bugs.
It creates extra errors. But which ones are genuine?
All of them genuine. It is the programmer's choice to use same or
different types for two objects.
I made a change once which meant that 'ref char' and 'ref byte' were
incompatible.
(They were different to allow some conveniences, such as printing a
'ref char' type would assume it was a zero-terminated string.)
But then I had a function 'readfile(filename)=>ref byte', and I wanted
to read a text file like this:
ref char sourcecode := readfile("hello.c")
Reading text file should return texts, not bytes. The type system helped
you to detect a design error of readfile.
In dynamic code, I can choose from:
sourcecode := readblockfile("hello.c") # returns a raw ptr
sourcecode := readstrfile("hello.c") # ... a string
sourcecode := readtextfile("hello.c") # ... a list of strings
sourcecode := readbinfile("hello.c") # ... a byte-array
You can do that in properly typed language:
Source_Code : Garbage := Read ("hello.c");
Source_Code : String := Read ("hello.c");
Source_Code : List_Of_Strings := Read ("hello.c");
Source_Code : Dump := Read ("hello.c");
What's the type of 'sourcecode'? Who cares! If I try to use it
inappropriately, it will tell me.
How does it know if you use it inappropriately without knowing what it
is? How do you know what it is without stating that in the program? You
wrote comments, where I used type.
You used different names, where I
just overloaded Read.
But look at the flexibility.
Flexibility of what exactly?
You are free not to do that and enjoy debugging the flight system while
the plane crushes down...
There are different processes that lead to the various kinds of
standardised connectors.
Yes, like processes of being electrocuted and blocks burnt down.
If exceeded, the fuse blows and the appliance can't be used until
something is fixed.
Or the user dies, nobody to complain...
On 26/09/2021 15:30, Dmitry A. Kazakov wrote:
On 2021-09-26 15:21, Bart wrote:
On 26/09/2021 12:34, Dmitry A. Kazakov wrote:
On 2021-09-26 13:04, Bart wrote:
You seems to think that having an impossibly strict type system and
using the right jargon for everything somehow magically eliminates
all bugs.
Type system eliminates type bugs. Broken type system introduces bugs.
It creates extra errors. But which ones are genuine?
All of them genuine. It is the programmer's choice to use same or
different types for two objects.
I made a change once which meant that 'ref char' and 'ref byte' were
incompatible.
(They were different to allow some conveniences, such as printing a
'ref char' type would assume it was a zero-terminated string.)
But then I had a function 'readfile(filename)=>ref byte', and I
wanted to read a text file like this:
ref char sourcecode := readfile("hello.c")
Reading text file should return texts, not bytes. The type system
helped you to detect a design error of readfile.
In the file system there is no difference between a text file and
non-text file.
What's the type of 'sourcecode'? Who cares! If I try to use it
inappropriately, it will tell me.
How does it know if you use it inappropriately without knowing what it
is? How do you know what it is without stating that in the program?
You wrote comments, where I used type.
You used different names, where I just overloaded Read.
So one uses 'str'; the other 'String'; is there really a lot of difference?
In yours, even if this was allowed:
Write("hello2.c", Read("hello.c"))
what would be the intermediate type used? Here you don't have typed
variables to determinate which overload is to be used.
In mine, types don't really come into it, except in a very casual manner.
readtextfile() returns a List, whose elements happen to be strings, but
they can be anything:
allfiles::=()
forall file in dirlist("*.c") do
allfiles append:=readtextfile(file)
od
writetextfile("$random.c", reverse(allfiles[random(allfiles.bounds)]))
Now allfiles is a list of lists of strings. I choose one element at
random and write that file out, with the lines reversed.
Job done.
Flexibility of what exactly?
See above.
You are free not to do that and enjoy debugging the flight system
while the plane crushes down...
I don't use my software for avionics.
Most software isn't. So you are imposing impossibly high and counter-productive standards on everyone for no reason.
Or the user dies, nobody to complain...
You're ignoring my point, which is that things can go wrong, and you
need to detect and deal with it.
On 2021-09-26 18:59, Bart wrote:
So one uses 'str'; the other 'String'; is there really a lot of
difference?
The difference is manifested vs. inferred types.
In yours, even if this was allowed:
Write("hello2.c", Read("hello.c"))
what would be the intermediate type used? Here you don't have typed
variables to determinate which overload is to be used.
It would flag an error, because it is ambiguous, provided there are many compatible versions of Write and Read designed for a *reason*. You are ignoring the reason, *why*?
Job done.
I have no idea what the job was.
But the implementation is awfully bad.
What happens with formatters HT, LF, CF, VT and separators SOH, STX,
ETX, ETB, EOT of read files?
What about encoding? Where is BOM detection
and processing? Where is re-coding? You seems never actually worked with processing text files, do you?
Flexibility of what exactly?
See above.
Above is only buggy code of unclear purpose.
You are free not to do that and enjoy debugging the flight system
while the plane crushes down...
I don't use my software for avionics.
That is a relief.
You're ignoring my point, which is that things can go wrong, and you
need to detect and deal with it.
No you are ignoring the point that things are different in the
consequences of what happens if something goes wrong and in the costs of prevention. Yes, the building can collapse and the pencil can fall down
from the table. This is called risk analysis and management.
On 26/09/2021 20:11, Dmitry A. Kazakov wrote:
On 2021-09-26 18:59, Bart wrote:
So one uses 'str'; the other 'String'; is there really a lot of
difference?
The difference is manifested vs. inferred types.
<shrug> Who cares? My approach works fine.
In yours, even if this was allowed:
Write("hello2.c", Read("hello.c"))
what would be the intermediate type used? Here you don't have typed
variables to determinate which overload is to be used.
It would flag an error, because it is ambiguous, provided there are
many compatible versions of Write and Read designed for a *reason*.
You are ignoring the reason, *why*?
I'm not. In my version (which you snipped), it's something like:
writestrfile("B", readstrfile("A"))
Reading a string, and writing a string.
Job done.
I have no idea what the job was.
It doesn't matter. It was some silly task reading a bunch of files into memory, and writing a reversed version of one of those.
All without bothering with the minutiae of type systems.
But the implementation is awfully bad. What happens with formatters
HT, LF, CF, VT and separators SOH, STX, ETX, ETB, EOT of read files?
What about them?
A file is just N bytes of data. A text file is a N
bytes separated with CR/CRLF sequences.
What about encoding? Where is BOM detection and processing? Where is
re-coding? You seems never actually worked with processing text files,
do you?
That's true, I've only worked with them since about 1976.
BOM-handling is done at the application level.
I don't bother with UTF8
as it normally looks after itself.
You're ignoring my point, which is that things can go wrong, and you
need to detect and deal with it.
No you are ignoring the point that things are different in the
consequences of what happens if something goes wrong and in the costs
of prevention. Yes, the building can collapse and the pencil can fall
down from the table. This is called risk analysis and management.
OK, so for you, there is no such thing as a range of programming
language levels; some higher, some lower level; some with a strict type system, others more lax. It has to be Ada or nothing.
The fact that dynamic languages can provide huge benefits in
productivity and are more accessible, I guess cuts no ice.
Unfortunately I'm not reimplementing Ada.
On 2021-09-26 23:16, Bart wrote:
On 26/09/2021 20:11, Dmitry A. Kazakov wrote:
On 2021-09-26 18:59, Bart wrote:
So one uses 'str'; the other 'String'; is there really a lot of
difference?
The difference is manifested vs. inferred types.
<shrug> Who cares? My approach works fine.
In yours, even if this was allowed:
Write("hello2.c", Read("hello.c"))
what would be the intermediate type used? Here you don't have typed
variables to determinate which overload is to be used.
It would flag an error, because it is ambiguous, provided there are
many compatible versions of Write and Read designed for a *reason*.
You are ignoring the reason, *why*?
I'm not. In my version (which you snipped), it's something like:
writestrfile("B", readstrfile("A"))
Reading a string, and writing a string.
Write ("B", String'(Read("A")))
Job done.
I have no idea what the job was.
It doesn't matter. It was some silly task reading a bunch of files
into memory, and writing a reversed version of one of those.
In short the example makes no sense and proves or disproves nothing.
All without bothering with the minutiae of type systems.
But the implementation is awfully bad. What happens with formatters
HT, LF, CF, VT and separators SOH, STX, ETX, ETB, EOT of read files?
What about them?
You said "text" files.
A file is just N bytes of data. A text file is a N bytes separated
with CR/CRLF sequences.
Where did you get that?
What about encoding? Where is BOM detection and processing? Where is
re-coding? You seems never actually worked with processing text
files, do you?
That's true, I've only worked with them since about 1976.
BOM-handling is done at the application level.
And your code was not an application? Was it a device driver?
I don't bother with UTF8 as it normally looks after itself.
How? Let the first file were in KOI-8, the second in ITU T.6, the third
in RADIX-50, what would be the encoding of the result?
The fact that dynamic languages can provide huge benefits in
productivity and are more accessible, I guess cuts no ice.
Any dynamic language is a static language that uses single type ANY for everything.
On 27/09/2021 09:10, Dmitry A. Kazakov wrote:
On 2021-09-26 23:16, Bart wrote:
On 26/09/2021 20:11, Dmitry A. Kazakov wrote:
On 2021-09-26 18:59, Bart wrote:
So one uses 'str'; the other 'String'; is there really a lot of
difference?
The difference is manifested vs. inferred types.
<shrug> Who cares? My approach works fine.
In yours, even if this was allowed:
Write("hello2.c", Read("hello.c"))
what would be the intermediate type used? Here you don't have typed
variables to determinate which overload is to be used.
It would flag an error, because it is ambiguous, provided there are
many compatible versions of Write and Read designed for a *reason*.
You are ignoring the reason, *why*?
I'm not. In my version (which you snipped), it's something like:
writestrfile("B", readstrfile("A"))
Reading a string, and writing a string.
Write ("B", String'(Read("A")))
Which is scarily similar to my proposal to use:
writefile("B", readstrfile("A"))
Looks like I'm providing more type annotations in my dynamic language
that Ada does in its static type system!
Job done.
I have no idea what the job was.
It doesn't matter. It was some silly task reading a bunch of files
into memory, and writing a reversed version of one of those.
In short the example makes no sense and proves or disproves nothing.
It does quite a number of things without having to explicitly pin down
exact types:
* Reading files as lists of strings
* Iterating over a list without specifing the list type or the loop
variable type
* Calling a random() function without being hung up on arg or return
type, etc etc
All without bothering with the minutiae of type systems.
But the implementation is awfully bad. What happens with formatters
HT, LF, CF, VT and separators SOH, STX, ETX, ETB, EOT of read files?
What about them?
You said "text" files.
A file is just N bytes of data. A text file is a N bytes separated
with CR/CRLF sequences.
Where did you get that?
Sorry; in my nomenclature,
'strfile' meant a file (nominally a text
file) represented as one string with embedded newline characters.
'textfile' meant a nominal text file separated into multiple strings
with cr or crlf; the newline sequences don't appear in the strings, and
are recreated if the file is written out again.
What about encoding? Where is BOM detection and processing? Where is
re-coding? You seems never actually worked with processing text
files, do you?
That's true, I've only worked with them since about 1976.
BOM-handling is done at the application level.
And your code was not an application? Was it a device driver?
The application decides whether to deal with BOM.
Why, what clever things does ADA do with BOM if all you want to do is
loaded the bytes of "A" into memory and write the same bytes out as "B"?
I don't bother with UTF8 as it normally looks after itself.
How? Let the first file were in KOI-8, the second in ITU T.6, the
third in RADIX-50, what would be the encoding of the result?
And suppose the fourth is in you own private encoding called DAK-3000;
how is an OS shell supposed to implement CopyFile?
Or is possible that ... it doesn't matter?
The fact that dynamic languages can provide huge benefits in
productivity and are more accessible, I guess cuts no ice.
Any dynamic language is a static language that uses single type ANY
for everything.
That's one small part of what makes a dynamic language amenable.
Try using C++ using only one type: std::any, and assume all necessary
support has been added.
It will still be one huge lumbering language. It would be like fitting handle-bars and pedals to a 40-ton truck.
On 2021-09-27 12:03, Bart wrote:
* Iterating over a list without specifing the list type or the loop
variable type
Why anybody need to specify type when iterating a container?
* Calling a random() function without being hung up on arg or return
type, etc etc
You do not know what random returns?
Sorry; in my nomenclature,
Sorry, text files are not your nomenclature.
'strfile' meant a file (nominally a text file) represented as one
string with embedded newline characters.
But not end-of-file characters and other formatting characters etc?
https://en.wikipedia.org/wiki/Text_file
Why, what clever things does ADA do with BOM if all you want to do is
loaded the bytes of "A" into memory and write the same bytes out as "B"?
This has nothing to do with Ada, but with brainless method of writing programs without minimal efforts to understand the problem at hand and validate the solution.
And suppose the fourth is in you own private encoding called DAK-3000;
https://en.wikipedia.org/wiki/DEC_RADIX_50
how is an OS shell supposed to implement CopyFile?
You need not to read file lines (especially when none exist) in order to
copy it.
That's one small part of what makes a dynamic language amenable.
Try using C++ using only one type: std::any, and assume all necessary
support has been added.
It will still be one huge lumbering language. It would be like fitting
handle-bars and pedals to a 40-ton truck.
No, handle-bars and pedals are typed. In a dynamic language the control
panel
On 27/09/2021 12:11, Dmitry A. Kazakov wrote:
On 2021-09-27 12:03, Bart wrote:
Why anybody need to specify type when iterating a container?
My god - you've finally hit on something the programmer does not need to define the exact type of.
But not end-of-file characters and other formatting characters etc?
I don't understand. What on earth do formatting characters have to do
with it? If the file contains a "\t" character, then the resulting
string contain "\t" too.
Imagine some mmap function over the file, which looks like a byte-array
(ie. what my readbinfile routine returns).
https://en.wikipedia.org/wiki/Text_file
That doesn't say anything new.
Why, what clever things does ADA do with BOM if all you want to do is
loaded the bytes of "A" into memory and write the same bytes out as "B"?
This has nothing to do with Ada, but with brainless method of writing
programs without minimal efforts to understand the problem at hand and
validate the solution.
OK, so does clever things does /your solution/ do with BOM? Since you
brought it up.
And suppose the fourth is in you own private encoding called DAK-3000;
https://en.wikipedia.org/wiki/DEC_RADIX_50
I know radix-50. It was used to squeeze 6 characters into 32 bits. Not
sure how that is relevant to the issue at hand.
Should a text-file reading routine need to be aware of DEC's SIXBIT
encoding (similarly used to squeeze 6 characters into 36 bits)?
What you're describing are not text files. Try submitting a ITU T.6
file, whatever that is, to GCC in order to compile it.
Here's MY simple definition of a text file, the kind I read and write:
how is an OS shell supposed to implement CopyFile?
You need not to read file lines (especially when none exist) in order
to copy it.
Exactly. Then you use one of my readblock/readstr/readbinfile routines because the contents of the file do not matter.
You might, for example, want to concatenate multiple files into one file.
That's one small part of what makes a dynamic language amenable.
Try using C++ using only one type: std::any, and assume all necessary
support has been added.
It will still be one huge lumbering language. It would be like
fitting handle-bars and pedals to a 40-ton truck.
No, handle-bars and pedals are typed. In a dynamic language the
control panel
is a regular bicycle.
On 2021-09-27 13:53, Bart wrote:
On 27/09/2021 12:11, Dmitry A. Kazakov wrote:
On 2021-09-27 12:03, Bart wrote:
Why anybody need to specify type when iterating a container?
My god - you've finally hit on something the programmer does not need
to define the exact type of.
The object already exist, thus its type is known. Does that wonder you?
Why?
But not end-of-file characters and other formatting characters etc?
I don't understand. What on earth do formatting characters have to do
with it? If the file contains a "\t" character, then the resulting
string contain "\t" too.
Line start and end has multiple encodings in different systems:
Unix: LF
Windows: CR-LF
OS 9: CR
RSX, VMS: None, variable record format
...
CR-VT is the end of line followed by a number of empty lines down to the
next vertical tabulation stop.
EOT is added to the file end.
Imagine some mmap function over the file, which looks like a
byte-array (ie. what my readbinfile routine returns).
Imagine no such function and return to the subject of text files.
https://en.wikipedia.org/wiki/Text_file
That doesn't say anything new.
Then, why do you ignore them?
In Ada there is a library package Ada.Text_IO responsible for reading
and writing text files. It provides functions Get_Line and Put_Line
handling whatever operating system conventions about text files. This includes all magic cookies, encoding, text formatting characters.
In software design important concepts are code reuse and modularization.
And suppose the fourth is in you own private encoding called DAK-3000;
https://en.wikipedia.org/wiki/DEC_RADIX_50
I know radix-50. It was used to squeeze 6 characters into 32 bits. Not
sure how that is relevant to the issue at hand.
It is 16-bit encoding, not possible to manipulate the way you did.
What you're describing are not text files. Try submitting a ITU T.6
file, whatever that is, to GCC in order to compile it.
Since when GCC became definition of text file?
Here's MY simple definition of a text file, the kind I read and write:
I prefer the actual definition = of the OS at hand.
No, you cannot, the result would be garbage. Concatenate UTF-8 file with UTF-16 and see what happens.
is a regular bicycle.
In a regular bicycle you cannot apply the operation Ring_The_Bell to the pedals of. The real world is strongly typed.
On 27/09/2021 14:58, Dmitry A. Kazakov wrote:
On 2021-09-27 13:53, Bart wrote:
On 27/09/2021 12:11, Dmitry A. Kazakov wrote:
On 2021-09-27 12:03, Bart wrote:
Why anybody need to specify type when iterating a container?
My god - you've finally hit on something the programmer does not need
to define the exact type of.
The object already exist, thus its type is known. Does that wonder
you? Why?
But not end-of-file characters and other formatting characters etc?
I don't understand. What on earth do formatting characters have to do
with it? If the file contains a "\t" character, then the resulting
string contain "\t" too.
Line start and end has multiple encodings in different systems:
Unix: LF
Windows: CR-LF
OS 9: CR
RSX, VMS: None, variable record format
...
CR-VT is the end of line followed by a number of empty lines down to
the next vertical tabulation stop.
You don't know how my readtextfile is implemented.
It is not helpful.
Dynamic code gives you generics for free.
And suppose the fourth is in you own private encoding called DAK-3000; >>>>https://en.wikipedia.org/wiki/DEC_RADIX_50
I know radix-50. It was used to squeeze 6 characters into 32 bits.
Not sure how that is relevant to the issue at hand.
It is 16-bit encoding, not possible to manipulate the way you did.
So it is a binary file format, and not text.
UTF8 on the other hand was specially designed to be processed with
methods that assumed 8-bit text files.
You couldn't for example reverse the order of all the bytes, and expect
the data to still be valid UTF8.
What you don't do is condemn a language's file handling routines simply because the language isn't Ada. For all you know, my language's String
type could accommodate Unicode.
What you're describing are not text files. Try submitting a ITU T.6
file, whatever that is, to GCC in order to compile it.
Since when GCC became definition of text file?
Since when did C source code not count as a text file?
But I seriously doubt that ITU T.6 is a text format.
Here's MY simple definition of a text file, the kind I read and write:
I prefer the actual definition = of the OS at hand.
My choice of words, I choose to use 'text file' as I see fit.
No, you cannot, the result would be garbage. Concatenate UTF-8 file
with UTF-16 and see what happens.
So how does ZIP work?
is a regular bicycle.
In a regular bicycle you cannot apply the operation Ring_The_Bell to
the pedals of. The real world is strongly typed.
Actually, so are dynamic languages.
Is it allowed to discuss the design of such languages here?
On 2021-09-27 16:51, Bart wrote:
You don't know how my readtextfile is implemented.
Oh, let me guess, it is implemented poorly, right?
You couldn't for example reverse the order of all the bytes, and
expect the data to still be valid UTF8.
Why should I?
It does not make sense for text files. What about treating
a text file as a 2D matrix and finding its determinant? The answer is,
do not!
But I seriously doubt that ITU T.6 is a text format.
Whatever.
So how does ZIP work?
There is information freely available on the topic.
Actually, so are dynamic languages.
Good, then your example should not compile because it applies wrong operations to the text file.
On 27/09/2021 17:16, Dmitry A. Kazakov wrote:
On 2021-09-27 16:51, Bart wrote:
You don't know how my readtextfile is implemented.
Oh, let me guess, it is implemented poorly, right?
I'd be interested in what an Ada compiler will do when running on
Windows given a source file with RSX line endings and with EBCDIC or
RADIX_50 or ITU T.6 encoding.
You couldn't for example reverse the order of all the bytes, and
expect the data to still be valid UTF8.
Why should I?
For fun?
If I wanted to sort the characters in a string
So how does ZIP work?
There is information freely available on the topic.
So you're avoiding answering my very simple point: IT DOESN'T MATTER.
Some programs just see files as a lump of data.
Should users be stopped from defining operations on strings that you
don't approve of? Or that you can't think of a use for?
Here's one use for sorting the characters within a string:
println ssort("oriental")
println ssort("relation")
Both display "aeilnort". This means you can test for two strings being anagrams if they sort to the same string.
Now you going to piss all over this by casting aspersions on the quality
of sort routines; on how well it works on UTF8 (or RADIX-50), on all kinds.
On 2021-09-27 19:31, Bart wrote:
So you're avoiding answering my very simple point: IT DOESN'T MATTER.
Some programs just see files as a lump of data.
So what? This has nothing to do with text files unless compression works
on the level of word dictionaries.
Now try
println ssort("восточный")
Now you going to piss all over this by casting aspersions on the
quality of sort routines; on how well it works on UTF8 (or RADIX-50),
on all kinds.
What did it print?
On 27/09/2021 20:54, Dmitry A. Kazakov wrote:
On 2021-09-27 19:31, Bart wrote:
Notice I'm using a string type to contain that data. It works. Who cares
what the data is or whether it is technically a text file or not?
Now try
println ssort("восточный")
Now you going to piss all over this by casting aspersions on the
quality of sort routines; on how well it works on UTF8 (or RADIX-50),
on all kinds.
What did it print?
The specs for ssort() will say it sorts the individual bytes of the
string.
Your approach: it needs to work for 10,000 alphabets;
(For your specific example, if I knock something up (that represents
strings as UCS2 arrays), then it shows "вйноостчы")
On 10/09/2021 08:08, Dmitry A. Kazakov wrote:
On 2021-09-09 21:39, James Harris wrote:
On 03/09/2021 18:27, Dmitry A. Kazakov wrote:
On 2021-09-03 18:43, James Harris wrote:
...
Your example was good in showing multiple dispatch but I'm not so
sure it's a good solution in this case, let alone the only way!
Of course it is. Write a program that renders a list of shapes in a
widget in a way that you would not need to modify each time another
developers team creates a new shape or supports another surface.
Sure. I would have surfaces support different interfaces: pixmap,
vector and whatever else was needed, and have a basic set of
component shapes that surfaces could draw on themselves: lines,
triangles, rectangles, ellipses, etc, and bit blit. Then widgets
could then use those primitives to do all required drawing of basic
and complex shapes.
Who calls these? Your rendering program?
Shapes in the object model would render themselves on a given surface by invoking the primitives which are provided by the surface. IOW each
surface would know how to draw basic shapes on itself and those
primitives could be invoked by the units in the object model.
ObjectModel --> Surface
The ObjectModel would have components of arbitrary complexity. The
surface would provide /basic/ shapes only. (And all types of surface
would support the same interface, i.e. provide the same operations.)
You confuse implementation with means of. Rendering primitives are
means to implement the procedure Draw you would call from your program
for each element from the list shapes. The classic OO way:
for Shape in Shapes_List loop
Shape.Draw (Widget.Get_Redenring Context);
-- Dispatches to the Shape's Draw
end loop;
Your way?
The same except that the way Shape.Draw would draw itself would be by
calling primitives in the target surface.
I know graphics contexts are common but I am not sure they are distinct
from a surface. Assuming they are comparable then your code would become
Surface s
....
for Shape in Shapes_List loop
Shape.Draw(s)
end loop
Then the shape's Draw method would invoke one or more primitives of s.
On 2021-09-09 21:39, James Harris wrote:
On 03/09/2021 18:27, Dmitry A. Kazakov wrote:
On 2021-09-03 18:43, James Harris wrote:
Your example was good in showing multiple dispatch but I'm not so
sure it's a good solution in this case, let alone the only way!
Of course it is. Write a program that renders a list of shapes in a
widget in a way that you would not need to modify each time another
developers team creates a new shape or supports another surface.
Sure. I would have surfaces support different interfaces: pixmap,
vector and whatever else was needed, and have a basic set of component
shapes that surfaces could draw on themselves: lines, triangles,
rectangles, ellipses, etc, and bit blit. Then widgets could then use
those primitives to do all required drawing of basic and complex shapes.
Who calls these? Your rendering program?
You confuse implementation with means of. Rendering primitives are means
to implement the procedure Draw you would call from your program for
each element from the list shapes. The classic OO way:
for Shape in Shapes_List loop
Shape.Draw (Widget.Get_Redenring Context);
-- Dispatches to the Shape's Draw
end loop;
Your way?
On Monday, August 30, 2021 at 4:47:58 AM UTC-5, James Harris wrote:
On 29/08/2021 20:32, Dmitry A. Kazakov wrote:
On 2021-08-29 20:24, James Harris wrote:...
Ignoring the length part (as some objects would have their own length
and some would not but that difference is immaterial here) how would a >>>> 'tag' system be laid out in memory and how would one use that to
locate a method?
You must consider the following inheritance variants:
Thanks for the information, Dmitry. I might come back to you about some
of that. But for this thread I was wondering how the memory might be
laid out under the tag model that you mentioned. What form does a tag
take and how does the runtime get from the tag to a method?
If it depends on which model is in use then what about single
inheritance and the Full MI model you mentioned?
For TMI on tags and tagging, see Representing Type Information in
Dynamically Typed Languages by David Gudeman.
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.39.4394&rep=rep1&type=pdf
On 2021-09-26 12:32, James Harris wrote:
On 26/09/2021 09:38, Dmitry A. Kazakov wrote:
On 2021-09-26 09:57, James Harris wrote:
On 25/09/2021 19:31, Dmitry A. Kazakov wrote:
On 2021-09-25 19:59, James Harris wrote:
...
Neither a program nor a language is 'broken' as long as the error
is detected.
By whom and when?
In the context, by the executable code at run time.
In that context consider an aircraft you are sitting in...
try
one path
catch exception
other path
Plane keeps flying.
in a dive...
Historically Ada used the term subtype for a very narrow set of
subtyping. So, to clarify one would say "Ada subtype", or "Liskov
subtype" etc. Same with C++ classes, which are just types. C++ class
reads "dynamically polymorphic type."
OOP did not do that, in OOPL the terminology is as unsettled as anywhere else.
But, one can agree on different terms, just define them, then it is OK.
If you want to use "class" for "type" like in C++, that is fine, just
give another word for class. Do not say they do not exist because you
used the name...
I have no issue with terms, but with broken semantics.
On 22/10/2021 17:08, Dmitry A. Kazakov wrote:
On 2021-10-22 17:45, James Harris wrote:
I know graphics contexts are common but I am not sure they are
distinct from a surface. Assuming they are comparable then your code
would become
Surface s
....
for Shape in Shapes_List loop
Shape.Draw(s)
end loop
Then the shape's Draw method would invoke one or more primitives of s.
Good, you copied my example. So you do not deny that Draw dynamically
dispatches on the actual type of Shape. q.e.d.
Certainly. I didn't think that point was in dispute.
And as we've discussed, the dynamic dispatch that OO proponents are
normally so keen on is much slooooooower than a function call. They seem
to accept its poor performance without complaint when they want to. ;-)
On 2021-10-22 17:45, James Harris wrote:
On 10/09/2021 08:08, Dmitry A. Kazakov wrote:
On 2021-09-09 21:39, James Harris wrote:
On 03/09/2021 18:27, Dmitry A. Kazakov wrote:
On 2021-09-03 18:43, James Harris wrote:
...
Your example was good in showing multiple dispatch but I'm not so
sure it's a good solution in this case, let alone the only way!
Of course it is. Write a program that renders a list of shapes in a
widget in a way that you would not need to modify each time another
developers team creates a new shape or supports another surface.
Sure. I would have surfaces support different interfaces: pixmap,
vector and whatever else was needed, and have a basic set of
component shapes that surfaces could draw on themselves: lines,
triangles, rectangles, ellipses, etc, and bit blit. Then widgets
could then use those primitives to do all required drawing of basic
and complex shapes.
Who calls these? Your rendering program?
Shapes in the object model would render themselves on a given surface
by invoking the primitives which are provided by the surface. IOW each
surface would know how to draw basic shapes on itself and those
primitives could be invoked by the units in the object model.
ObjectModel --> Surface
The ObjectModel would have components of arbitrary complexity. The
surface would provide /basic/ shapes only. (And all types of surface
would support the same interface, i.e. provide the same operations.)
[...]
You confuse implementation with means of. Rendering primitives are
means to implement the procedure Draw you would call from your
program for each element from the list shapes. The classic OO way:
for Shape in Shapes_List loop
Shape.Draw (Widget.Get_Redenring Context);
-- Dispatches to the Shape's Draw
end loop;
Your way?
The same except that the way Shape.Draw would draw itself would be by
calling primitives in the target surface.
I know graphics contexts are common but I am not sure they are
distinct from a surface. Assuming they are comparable then your code
would become
Surface s
....
for Shape in Shapes_List loop
Shape.Draw(s)
end loop
Then the shape's Draw method would invoke one or more primitives of s.
Good, you copied my example. So you do not deny that Draw dynamically dispatches on the actual type of Shape. q.e.d.
------------------------------------
This example is called "double" or "cascading" dispatch, as opposed to
full multiple dispatch. Instead of dispatching on both Shape and
Surface, Draw dispatches only on Shape. Inside the implementation of
Shape, the primitive rendering operations dispatch on Surface. E.g. an implementation of Draw might look like (note, the second argument is class-wide):
procedure Draw (X : Ellipse; S : in Surface'Class) is
begin
S.Push;
S.Move_Coordinates (X.Center_X, X.Center_Y);
S.Rotate_Coordinates (X.Angle);
S.Draw_Ellipse (X.Width, Y.Height);
S.Pop;
end Draw;
So double dispatch is Shape first, Surface second. The obvious
disadvantage is performance loss as you have to dispatch multiple times.
On 2021-10-23 10:46, James Harris wrote:
On 22/10/2021 17:08, Dmitry A. Kazakov wrote:
On 2021-10-22 17:45, James Harris wrote:
I know graphics contexts are common but I am not sure they are
distinct from a surface. Assuming they are comparable then your code
would become
Surface s
....
for Shape in Shapes_List loop
Shape.Draw(s)
end loop
Then the shape's Draw method would invoke one or more primitives of s.
Good, you copied my example. So you do not deny that Draw dynamically
dispatches on the actual type of Shape. q.e.d.
Certainly. I didn't think that point was in dispute.
No? How then to interpret this sentence a few lines below:
[...]
And as we've discussed, the dynamic dispatch that OO proponents are
normally so keen on is much slooooooower than a function call. They
seem to accept its poor performance without complaint when they want
to. ;-)
1. Where is your solution to drawing a list of shapes without dispatch?
So far you proposed exactly what OO proponents do.
2. It is double dispatch work-around that is slow. Multiple dispatch dispatches just once.
On 23/10/2021 09:59, Dmitry A. Kazakov wrote:
On 2021-10-23 10:46, James Harris wrote:
On 22/10/2021 17:08, Dmitry A. Kazakov wrote:
On 2021-10-22 17:45, James Harris wrote:
I know graphics contexts are common but I am not sure they areGood, you copied my example. So you do not deny that Draw
distinct from a surface. Assuming they are comparable then your
code would become
Surface s
....
for Shape in Shapes_List loop
Shape.Draw(s)
end loop
Then the shape's Draw method would invoke one or more primitives of s. >>>>
dynamically dispatches on the actual type of Shape. q.e.d.
Certainly. I didn't think that point was in dispute.
No? How then to interpret this sentence a few lines below:
Two related topics in one post. Happens all the time.
[...]
And as we've discussed, the dynamic dispatch that OO proponents are
normally so keen on is much slooooooower than a function call. They
seem to accept its poor performance without complaint when they want
to. ;-)
1. Where is your solution to drawing a list of shapes without
dispatch? So far you proposed exactly what OO proponents do.
2. It is double dispatch work-around that is slow. Multiple dispatch
dispatches just once.
Performance is a big subject but why not. First, let's review what has
been proposed here by OO proponents (AIUI):
1. Store a tag with the object.
2. Use the tag and the name of the intended callee to find the right
callee for the types of one or more parameters. The right callee would
be found by, incredibly, sequential or binary search, possibly fronted
with a hash-table lookup.
In this case, if there are very few surfaces I might implement (in
assembly or in the compiler's output) what you might call /static/
dispatch by duplicating the assembly code for shapes (one copy for each surface) and call directly. To invoke the circle routine for surface A:
call surface_A_circle
On 2021-10-23 12:02, James Harris wrote:
On 23/10/2021 09:59, Dmitry A. Kazakov wrote:
On 2021-10-23 10:46, James Harris wrote:
On 22/10/2021 17:08, Dmitry A. Kazakov wrote:
On 2021-10-22 17:45, James Harris wrote:
And as we've discussed, the dynamic dispatch that OO proponents are
normally so keen on is much slooooooower than a function call. They
seem to accept its poor performance without complaint when they want
to. ;-)
1. Where is your solution to drawing a list of shapes without
dispatch? So far you proposed exactly what OO proponents do.
2. It is double dispatch work-around that is slow. Multiple dispatch
dispatches just once.
Performance is a big subject but why not. First, let's review what has
been proposed here by OO proponents (AIUI):
1. Store a tag with the object.
Only class-wide objects need tags.
You refer to a specific
implementation, which is good for objects passed by reference and
unusable for objects passed by value.
2. Use the tag and the name of the intended callee to find the right
callee for the types of one or more parameters. The right callee would
be found by, incredibly, sequential or binary search, possibly fronted
with a hash-table lookup.
Wrong. If restricted to single dispatch the tag could be implemented as
a pointer to the type-specific dispatching vector. The vector keeps a
pointer for each method:
vptr
.----------------.
| Draw |
+----------------+
| Rotate |
+----------------+
| Scale |
+----------------+
| ... |
Dispatch over such object would be just
call *vptr [offset]
where offset is a statically known dense index. E.g. 0 for Draw. No
search, no look-ups.
The drawbacks of this design are restriction to the single dispatch and necessity to allocate new table for each specific type.
[...]
In this case, if there are very few surfaces I might implement (in
assembly or in the compiler's output) what you might call /static/
dispatch by duplicating the assembly code for shapes (one copy for
each surface) and call directly. To invoke the circle routine for
surface A:
call surface_A_circle
The case is a list of any shapes to draw. How to invoke the right
routine? How to write a cycle over the list calling that routines?
Stop evading,
provide a code/pseudo-code of drawing shapes from the list
without knowing specific types and their specific functions like surface_A_circle.
On 23/10/2021 12:18, Dmitry A. Kazakov wrote:
On 2021-10-23 12:02, James Harris wrote:
On 23/10/2021 09:59, Dmitry A. Kazakov wrote:
On 2021-10-23 10:46, James Harris wrote:
On 22/10/2021 17:08, Dmitry A. Kazakov wrote:
On 2021-10-22 17:45, James Harris wrote:
...
And as we've discussed, the dynamic dispatch that OO proponents are
normally so keen on is much slooooooower than a function call. They
seem to accept its poor performance without complaint when they
want to. ;-)
1. Where is your solution to drawing a list of shapes without
dispatch? So far you proposed exactly what OO proponents do.
2. It is double dispatch work-around that is slow. Multiple dispatch
dispatches just once.
Performance is a big subject but why not. First, let's review what
has been proposed here by OO proponents (AIUI):
1. Store a tag with the object.
Only class-wide objects need tags.
I don't know what you are talking about.
You refer to a specific implementation, which is good for objects
passed by reference and unusable for objects passed by value.
Again, I don't know what you are talking about. This has nothing to do
with whether an argument is passed by value or by reference - both of
which are implementation issues.
2. Use the tag and the name of the intended callee to find the right
callee for the types of one or more parameters. The right callee
would be found by, incredibly, sequential or binary search, possibly
fronted with a hash-table lookup.
Wrong. If restricted to single dispatch the tag could be implemented
as a pointer to the type-specific dispatching vector. The vector keeps
a pointer for each method:
vptr
.----------------.
| Draw |
+----------------+
| Rotate |
+----------------+
| Scale |
+----------------+
| ... |
They look like methods of Shape. To be clear, I was talking about
calling methods of Surface from within a Shape method as they were more performance critical and you asked about performance.
The drawbacks of this design are restriction to the single dispatch
and necessity to allocate new table for each specific type.
It's a characteristic. It's not necessarily a drawback. For example:
* The double dispatch that you objected to on performance grounds would,
in fact, be fast, likely much faster than a complex multiple-dispatch
lookup.
* It is guaranteed not to have any problems scaling. Each type (class)
would need only as many pointers as the number of functions (methods) as
are in the source code.
Stop evading,
Phew! I provide explanation and code down to assembler level and you
call it evading? Riiight! I think you need to work on your tact - and
your grip on reality!!! :-(
provide a code/pseudo-code of drawing shapes from the list without
knowing specific types and their specific functions like
surface_A_circle.
OK. HLL:
type Circle
function Draw(Surface S)
S.DrawCircle(X, Y, R)
end function
end type
Shape s
Surface surf = CurrentSurface(context)
s = shape[i]
s.Draw(surf)
On 2021-10-23 19:15, James Harris wrote:
On 23/10/2021 12:18, Dmitry A. Kazakov wrote:
On 2021-10-23 12:02, James Harris wrote:
On 23/10/2021 09:59, Dmitry A. Kazakov wrote:
On 2021-10-23 10:46, James Harris wrote:
On 22/10/2021 17:08, Dmitry A. Kazakov wrote:
On 2021-10-22 17:45, James Harris wrote:
...
And as we've discussed, the dynamic dispatch that OO proponents
are normally so keen on is much slooooooower than a function call. >>>>>> They seem to accept its poor performance without complaint when
they want to. ;-)
1. Where is your solution to drawing a list of shapes without
dispatch? So far you proposed exactly what OO proponents do.
2. It is double dispatch work-around that is slow. Multiple
dispatch dispatches just once.
Performance is a big subject but why not. First, let's review what
has been proposed here by OO proponents (AIUI):
1. Store a tag with the object.
Only class-wide objects need tags.
I don't know what you are talking about.
Object you can dispatch on is called class-wide.
You cannot dispatch on any argument of any C++ function except on the
first one. So, you need no tags for any them.
You refer to a specific implementation, which is good for objects
passed by reference and unusable for objects passed by value.
Again, I don't know what you are talking about. This has nothing to do
with whether an argument is passed by value or by reference - both of
which are implementation issues.
It has everything to do with that if you thought about what is dispatch
and which kind of objects are usually passed by value.
2. Use the tag and the name of the intended callee to find the right
callee for the types of one or more parameters. The right callee
would be found by, incredibly, sequential or binary search, possibly
fronted with a hash-table lookup.
Wrong. If restricted to single dispatch the tag could be implemented
as a pointer to the type-specific dispatching vector. The vector
keeps a pointer for each method:
vptr
.----------------.
| Draw |
+----------------+
| Rotate |
+----------------+
| Scale |
+----------------+
| ... |
They look like methods of Shape. To be clear, I was talking about
calling methods of Surface from within a Shape method as they were
more performance critical and you asked about performance.
And what makes you think that vptr would work for Shapes but not for Surfaces?
The drawbacks of this design are restriction to the single dispatch
and necessity to allocate new table for each specific type.
It's a characteristic. It's not necessarily a drawback. For example:
* The double dispatch that you objected to on performance grounds
would, in fact, be fast, likely much faster than a complex
multiple-dispatch lookup.
No, because you must re-dispatch inside Draw. Since Draw is class-wide
on the surface argument its implementation must work for all possible instances of surface. That is a heavy burden as you must provide a
unified fat interface for all possible surfaces.
* It is guaranteed not to have any problems scaling. Each type (class)
would need only as many pointers as the number of functions (methods)
as are in the source code.
It is guaranteed to have issues being a generic implementation for all surfaces. Such things never scale well. Decomposition of multiple
dispatch into a cascaded dispatch is not always possible.
Stop evading,
Phew! I provide explanation and code down to assembler level and you
call it evading? Riiight! I think you need to work on your tact - and
your grip on reality!!! :-(
I do not care about assembler. You claimed you do not need dispatch, I
want to see the code that does not dispatch, yet works.
provide a code/pseudo-code of drawing shapes from the list without
knowing specific types and their specific functions like
surface_A_circle.
OK. HLL:
type Circle
function Draw(Surface S)
S.DrawCircle(X, Y, R)
end function
end type
Shape s
Surface surf = CurrentSurface(context)
s = shape[i]
s.Draw(surf)
Since s has the type Shape, I presume the declaration
Shape s
means that?
So s.Draw must call Draw of the type Shape instead of Draw
of the type of the original object, e.g. Circle, Ellipse, Rectangle etc?
You killed that object when you assigned and converted/upcasted it to
its parent type Shape. How is that even possible to convert Circle to
Shape, there is no any hints that Circle is somehow related to it, or
that Draw of Circle overrides Draw of Shape?
Why
for i in ...
shape[i].Draw(CurrentSurface(context))
is not OK and how it would not dispatch?
On 23/10/2021 20:53, Dmitry A. Kazakov wrote:
On 2021-10-23 19:15, James Harris wrote:
On 23/10/2021 12:18, Dmitry A. Kazakov wrote:
On 2021-10-23 12:02, James Harris wrote:
On 23/10/2021 09:59, Dmitry A. Kazakov wrote:
On 2021-10-23 10:46, James Harris wrote:
On 22/10/2021 17:08, Dmitry A. Kazakov wrote:
On 2021-10-22 17:45, James Harris wrote:
...
And as we've discussed, the dynamic dispatch that OO proponents
are normally so keen on is much slooooooower than a function
call. They seem to accept its poor performance without complaint >>>>>>> when they want to. ;-)
1. Where is your solution to drawing a list of shapes without
dispatch? So far you proposed exactly what OO proponents do.
2. It is double dispatch work-around that is slow. Multiple
dispatch dispatches just once.
Performance is a big subject but why not. First, let's review what
has been proposed here by OO proponents (AIUI):
1. Store a tag with the object.
Only class-wide objects need tags.
I don't know what you are talking about.
Object you can dispatch on is called class-wide.
You cannot dispatch on any argument of any C++ function except on the
first one. So, you need no tags for any them.
You mean /dynamic/ dispatch? You are saying the tag is needed where the correct function cannot be known at compile time. Correct?
You refer to a specific implementation, which is good for objects
passed by reference and unusable for objects passed by value.
Again, I don't know what you are talking about. This has nothing to
do with whether an argument is passed by value or by reference - both
of which are implementation issues.
It has everything to do with that if you thought about what is
dispatch and which kind of objects are usually passed by value.
How "usually"?
AISI, if there's overloading then selection of which
procedure to call should be based purely on the /types/ of one or more parameters. How they are /usually/ passed has to be a matter of the
greatest irrelevance! :-(
2. Use the tag and the name of the intended callee to find the
right callee for the types of one or more parameters. The right
callee would be found by, incredibly, sequential or binary search,
possibly fronted with a hash-table lookup.
Wrong. If restricted to single dispatch the tag could be implemented
as a pointer to the type-specific dispatching vector. The vector
keeps a pointer for each method:
vptr
.----------------.
| Draw |
+----------------+
| Rotate |
+----------------+
| Scale |
+----------------+
| ... |
They look like methods of Shape. To be clear, I was talking about
calling methods of Surface from within a Shape method as they were
more performance critical and you asked about performance.
And what makes you think that vptr would work for Shapes but not for
Surfaces?
Whatever vptr is I didn't say it had limited application but from the
routine names you used you seemed to be thinking of something other than
what I had written about.
The drawbacks of this design are restriction to the single dispatch
and necessity to allocate new table for each specific type.
It's a characteristic. It's not necessarily a drawback. For example:
* The double dispatch that you objected to on performance grounds
would, in fact, be fast, likely much faster than a complex
multiple-dispatch lookup.
No, because you must re-dispatch inside Draw. Since Draw is class-wide
on the surface argument its implementation must work for all possible
instances of surface. That is a heavy burden as you must provide a
unified fat interface for all possible surfaces.
I don't think that's right.
For example, imagine that there are many
surfaces all with the same interface.
There may indeed be cases where what I am suggesting would not work well
but AFIACS this is not one of them!
Stop evading,
Phew! I provide explanation and code down to assembler level and you
call it evading? Riiight! I think you need to work on your tact - and
your grip on reality!!! :-(
I do not care about assembler. You claimed you do not need dispatch, I
want to see the code that does not dispatch, yet works.
You should care about assembly. Everything we write gets turned into
assembly eventually and it's a good way to show specifics of how
something might be implemented.
provide a code/pseudo-code of drawing shapes from the list without
knowing specific types and their specific functions like
surface_A_circle.
OK. HLL:
type Circle
function Draw(Surface S)
S.DrawCircle(X, Y, R)
end function
end type
Shape s
Surface surf = CurrentSurface(context)
s = shape[i]
s.Draw(surf)
Since s has the type Shape, I presume the declaration
Shape s
means that?
Yes.
So s.Draw must call Draw of the type Shape instead of Draw of the type
of the original object, e.g. Circle, Ellipse, Rectangle etc?
No, the intention was that it call Draw of the specific shape returned by
shape[i]
That could be a Circle, a Rectangle etc.
Maybe I've missed something but what I have in mind is that
s = shape[i]
would set s to whatever Shape is in the array at index i. All shapes
would have a Draw function at the same offset in a vector. The vector
would be found by the compiled code consulting the tag which is part of
the shape structure. (I'd have the tag /point at/ the vector.)
Why
for i in ...
shape[i].Draw(CurrentSurface(context))
is not OK and how it would not dispatch?
That looks OK except that I'd store the Surface rather than looking it
up each time.
On 2021-10-25 16:04, James Harris wrote:
On 23/10/2021 20:53, Dmitry A. Kazakov wrote:
On 2021-10-23 19:15, James Harris wrote:
On 23/10/2021 12:18, Dmitry A. Kazakov wrote:
It is enough to debunk you false claim about inefficiency of dispatch implementations. One thing at a time.
provide a code/pseudo-code of drawing shapes from the list without
knowing specific types and their specific functions like
surface_A_circle.
OK. HLL:
type Circle
function Draw(Surface S)
S.DrawCircle(X, Y, R)
end function
end type
Shape s
Surface surf = CurrentSurface(context)
s = shape[i]
s.Draw(surf)
Since s has the type Shape, I presume the declaration
Shape s
means that?
Yes.
So s.Draw must call Draw of the type Shape instead of Draw of the
type of the original object, e.g. Circle, Ellipse, Rectangle etc?
No, the intention was that it call Draw of the specific shape returned by
shape[i]
By reading minds?
That could be a Circle, a Rectangle etc.
How? The type is Shape. Circle is a different type.
[...]
Maybe I've missed something but what I have in mind is that
s = shape[i]
You missed types, as always. The type of s, should not have been Shape,
but "any instance of any type derived from Shape or Shape itself."
Why
for i in ...
shape[i].Draw(CurrentSurface(context))
is not OK and how it would not dispatch?
That looks OK except that I'd store the Surface rather than looking it
up each time.
Store? Rendering context cannot be copied, not unless creating a
completely independent one.
On 25/10/2021 15:34, Dmitry A. Kazakov wrote:
On 2021-10-25 16:04, James Harris wrote:
On 23/10/2021 20:53, Dmitry A. Kazakov wrote:
On 2021-10-23 19:15, James Harris wrote:
On 23/10/2021 12:18, Dmitry A. Kazakov wrote:
...
It is enough to debunk you false claim about inefficiency of dispatch
implementations. One thing at a time.
LOL! If that's your goal it helps explain some of your replies!!!
And it's a vain hope. You should realise you are wrong! :-)
provide a code/pseudo-code of drawing shapes from the list without >>>>>> knowing specific types and their specific functions like
surface_A_circle.
OK. HLL:
type Circle
function Draw(Surface S)
S.DrawCircle(X, Y, R)
end function
end type
Shape s
Surface surf = CurrentSurface(context)
s = shape[i]
s.Draw(surf)
Since s has the type Shape, I presume the declaration
Shape s
means that?
Yes.
So s.Draw must call Draw of the type Shape instead of Draw of the
type of the original object, e.g. Circle, Ellipse, Rectangle etc?
No, the intention was that it call Draw of the specific shape
returned by
shape[i]
By reading minds?
Er, no, by using the object's tag to locate a vector of routine
addresses - as in the code I have posted multiple times.
That could be a Circle, a Rectangle etc.
How? The type is Shape. Circle is a different type.
All Shapes have attributes in common. In the example, all Shapes have a
Draw function.
I am talking about /how/ things can be done but if you think it's not possible then perhaps you can explain how what I am proposing is
different from single dispatch Liskov substitution - something with
which I think you agree?
Note: "Substitutability is a principle
Or are you just trying to wind me up....? :-(
[...]
Maybe I've missed something but what I have in mind is that
s = shape[i]
You missed types, as always. The type of s, should not have been
Shape, but "any instance of any type derived from Shape or Shape itself."
In the model I was putting forward an object which has been declared of
type Shape can be any shape.
Why
for i in ...
shape[i].Draw(CurrentSurface(context))
is not OK and how it would not dispatch?
That looks OK except that I'd store the Surface rather than looking
it up each time.
Store? Rendering context cannot be copied, not unless creating a
completely independent one.
My example included:
Surface surf = CurrentSurface(context)
then, later,
s.Draw(surf)
The variable 'surf' would be a reference and would, in general, take up
just one word.
On 10/09/2021 08:42, Dmitry A. Kazakov wrote:
This is the requirement. Either way, manually or with the compiler
support, the programmers must do that. You cannot tell the customer,
it is too many shapes and too many surfaces you put in the
requirements. It is exactly as many as the customer requested.
I cannot agree.
I can see legitimate compiler-generated dispatching happening on either
1. the first parameter only
2. the first parameter and the result
As discussed elsewhere, single dispatch invoked twice may be
significantly faster than some of the more complex lookups that you
mentioned before.
What is your objection to my proposed solution? To be clear, the
idea is to have the object's pointer to link directly to the method
table as follows.
object: |___ptr___|___field0___|___field1___| etc
|
V
|__type___|__reserved__|__ptr__|__ptr__|__ptr__|__ptr__|
Show how this is supposed to work with full multiple dispatch, with
multi-methods, with multiple inheritance, with constructors and
destructors, with scalar types.
Those are /your/ desires.
The grapes are sour...
The replies are evasive....
On 2021-09-09 22:30, James Harris wrote:
On 04/09/2021 20:56, Dmitry A. Kazakov wrote:
On 2021-09-04 19:43, James Harris wrote:
On 03/09/2021 16:46, Dmitry A. Kazakov wrote:
On 2021-09-03 16:40, James Harris wrote:
On 03/09/2021 14:39, Dmitry A. Kazakov wrote:
...
Hash functions are pretty quick.
Hash functions are not fast enough to find a call target! I was/am >>>>>> even concerned about the extra cost of following a double pointer. >>>>>> Using a hash table would be slower still.
Single dispatch = single controlling argument
Milti-method = several controlling arguments from the same hierarchy
Full multiple dispatch = several controlling arguments from any hierarchies
Yes, you spoke about something that could be used in toy situations
where you had two or three parameters and a handful of types but it
would not scale. If you had just five parameters which could be of a
dozen types. You would need a dispatching table of 5^12 or 244 million
pointers to who knows how many functions. That is fantasyland.
This is the requirement. Either way, manually or with the compiler
support, the programmers must do that. You cannot tell the customer, it
is too many shapes and too many surfaces you put in the requirements. It
is exactly as many as the customer requested.
What is your objection to my proposed solution? To be clear, the
idea is to have the object's pointer to link directly to the method
table as follows.
object: |___ptr___|___field0___|___field1___| etc
|
V
|__type___|__reserved__|__ptr__|__ptr__|__ptr__|__ptr__|
Show how this is supposed to work with full multiple dispatch, with
multi-methods, with multiple inheritance, with constructors and
destructors, with scalar types.
Those are /your/ desires.
The grapes are sour...
On 2021-11-26 21:31, James Harris wrote:
On 10/09/2021 08:42, Dmitry A. Kazakov wrote:
As discussed elsewhere, single dispatch invoked twice may be
significantly faster than some of the more complex lookups that you
mentioned before.
But nothing can beat a program consisting of the single NOOP instruction.
What is your objection to my proposed solution? To be clear, the
idea is to have the object's pointer to link directly to the
method table as follows.
object: |___ptr___|___field0___|___field1___| etc
|
V
|__type___|__reserved__|__ptr__|__ptr__|__ptr__|__ptr__|
Show how this is supposed to work with full multiple dispatch, with
multi-methods, with multiple inheritance, with constructors and
destructors, with scalar types.
Those are /your/ desires.
The grapes are sour...
The replies are evasive....
They are pretty clear. You cannot fulfill the requirements, you are out.
This is the problem with you and Bart. When a customer wants a car, you
try to sell a boat. It is a wonderful boat with shiny polished rowlocks.
You keep on telling how dirty gasoline is and how healthy and emission
free rowing is. Fine, I go to a real car dealer.
On 2021-10-25 17:33, James Harris wrote:
On 25/10/2021 15:34, Dmitry A. Kazakov wrote:
On 2021-10-25 16:04, James Harris wrote:
On 23/10/2021 20:53, Dmitry A. Kazakov wrote:
On 2021-10-23 19:15, James Harris wrote:
On 23/10/2021 12:18, Dmitry A. Kazakov wrote:
...
It is enough to debunk you false claim about inefficiency of dispatch
implementations. One thing at a time.
LOL! If that's your goal it helps explain some of your replies!!!
And it's a vain hope. You should realise you are wrong! :-)
I presented an implementation of dispatch which complexity is adding
offset and doing call. Is that inefficient?
provide a code/pseudo-code of drawing shapes from the list
without knowing specific types and their specific functions like >>>>>>> surface_A_circle.
OK. HLL:
type Circle
function Draw(Surface S)
S.DrawCircle(X, Y, R)
end function
end type
Shape s
Surface surf = CurrentSurface(context)
s = shape[i]
s.Draw(surf)
Since s has the type Shape, I presume the declaration
Shape s
means that?
Yes.
So s.Draw must call Draw of the type Shape instead of Draw of the
type of the original object, e.g. Circle, Ellipse, Rectangle etc?
No, the intention was that it call Draw of the specific shape
returned by
shape[i]
By reading minds?
Er, no, by using the object's tag to locate a vector of routine
addresses - as in the code I have posted multiple times.
But you converted the type in assignment. s is Shape, it is not Circle.
Why on earth it should look after any tags? Oh, each call must always go through the vptr? This is what C++ does. It is inefficient and
semantically wrong.
That could be a Circle, a Rectangle etc.
How? The type is Shape. Circle is a different type.
All Shapes have attributes in common. In the example, all Shapes have
a Draw function.
All shapes have an implementation of Draw and these are all different,
which is the purpose of dispatch. So, again, why Shape must call
Circle's Draw instead of its own?
I am talking about /how/ things can be done but if you think it's not
possible then perhaps you can explain how what I am proposing is
different from single dispatch Liskov substitution - something with
which I think you agree?
No, you propose meaningless garbage, because differently to C++ you do
not explain when dispatch happens and when not. C++ does it wrong, but
it carefully specifies that wrongness so that the programmer always
knows from the source code when dispatch happens and when not. No riddles.
Note: "Substitutability is a principle
[...]
LSP is about subtypes. It has nothing to do with the issue at hand. You
did not make your declarations of types straight to even thinking about whether obtained types are Liskov's or not. Do types first, we can
discuss substitutability after that.
Or are you just trying to wind me up....? :-(
Good! (:-))
[...]
Maybe I've missed something but what I have in mind is that
s = shape[i]
You missed types, as always. The type of s, should not have been
Shape, but "any instance of any type derived from Shape or Shape
itself."
In the model I was putting forward an object which has been declared
of type Shape can be any shape.
That is a class-wide type. Now, how to declare a specific type? You need
a syntax for both.
Why
for i in ...
shape[i].Draw(CurrentSurface(context))
is not OK and how it would not dispatch?
That looks OK except that I'd store the Surface rather than looking
it up each time.
Store? Rendering context cannot be copied, not unless creating a
completely independent one.
My example included:
Surface surf = CurrentSurface(context)
then, later,
s.Draw(surf)
The variable 'surf' would be a reference and would, in general, take
up just one word.
Even better. Now you have:
1. Specific type Circle
2. Class-wide type Circle (those can hold any instance of any descendant
of Circle #1)
3. References to Circle #1
4. References to Circle #2
all confused in a huge mess. How do I know that Surface surf is #4 and
not #1?
C++ has several types for references: T&, T*. It conflates specific and class-wide types, though. The treatment depends on the context and there
is a syntax <type>:: to kill dispatch.
In Ada there is no reference types, because you can simply rename any
object any time. Class-wide and specific types are distinct.
You want to mix #1-#4? Good luck with that!
But this is still all smoke and mirrors on your side. I am still waiting
for an example that does not involve dynamic dispatch!
Remember, one thing at a time:
[x] Dispatch is efficient
[ ] Dispatch is necessary
[ ] Double dispatch is a limited work-around for multiple dispatch
On 25/10/2021 20:08, Dmitry A. Kazakov wrote:
On 2021-10-25 17:33, James Harris wrote:
On 25/10/2021 15:34, Dmitry A. Kazakov wrote:
On 2021-10-25 16:04, James Harris wrote:
On 23/10/2021 20:53, Dmitry A. Kazakov wrote:
On 2021-10-23 19:15, James Harris wrote:
On 23/10/2021 12:18, Dmitry A. Kazakov wrote:
...
It is enough to debunk you false claim about inefficiency of
dispatch implementations. One thing at a time.
LOL! If that's your goal it helps explain some of your replies!!!
And it's a vain hope. You should realise you are wrong! :-)
I presented an implementation of dispatch which complexity is adding
offset and doing call. Is that inefficient?
No, it's efficient as long as the compiler knows the offset at compile
time.
But you converted the type in assignment. s is Shape, it is not
Circle. Why on earth it should look after any tags? Oh, each call must
always go through the vptr? This is what C++ does. It is inefficient
and semantically wrong.
Simply put, in this case all Shapes would have a Draw function.
That could be a Circle, a Rectangle etc.
How? The type is Shape. Circle is a different type.
All Shapes have attributes in common. In the example, all Shapes have
a Draw function.
All shapes have an implementation of Draw and these are all different,
which is the purpose of dispatch. So, again, why Shape must call
Circle's Draw instead of its own?
The Draw function called would be whatever's supplied by the object.
[...]
Maybe I've missed something but what I have in mind is that
s = shape[i]
You missed types, as always. The type of s, should not have been
Shape, but "any instance of any type derived from Shape or Shape
itself."
In the model I was putting forward an object which has been declared
of type Shape can be any shape.
That is a class-wide type. Now, how to declare a specific type? You
need a syntax for both.
Shape would be an interface.
Specific shapes would Circle, Square, etc.
I don't know what problem you are seeing but I cannot see why they could
not be declared as such.
Even better. Now you have:
1. Specific type Circle
2. Class-wide type Circle (those can hold any instance of any
descendant of Circle #1)
3. References to Circle #1
4. References to Circle #2
all confused in a huge mess. How do I know that Surface surf is #4 and
not #1?
No, there are two independent concepts. First, there would be Shapes
that the user would define and manipulate. Second, there would a various Surfaces on which drawing would take place. Each Surface would provide various primitives for drawing on itself. Each Shape would use those primitives to render itself on the Surface.
C++ has several types for references: T&, T*. It conflates specific
and class-wide types, though. The treatment depends on the context and
there is a syntax <type>:: to kill dispatch.
In Ada there is no reference types, because you can simply rename any
object any time. Class-wide and specific types are distinct.
You want to mix #1-#4? Good luck with that!
But this is still all smoke and mirrors on your side. I am still
waiting for an example that does not involve dynamic dispatch!
I don't mind dynamic dispatch - as long as it's fast.
Remember, one thing at a time:
[x] Dispatch is efficient
[ ] Dispatch is necessary
[ ] Double dispatch is a limited work-around for multiple dispatch
[ ] Double dispatch is faster and more scalable than certain forms of multiple dispatch.
On 2021-11-27 11:44, James Harris wrote:
On 25/10/2021 20:08, Dmitry A. Kazakov wrote:
On 2021-10-25 17:33, James Harris wrote:
On 25/10/2021 15:34, Dmitry A. Kazakov wrote:
On 2021-10-25 16:04, James Harris wrote:
On 23/10/2021 20:53, Dmitry A. Kazakov wrote:
On 2021-10-23 19:15, James Harris wrote:
On 23/10/2021 12:18, Dmitry A. Kazakov wrote:
...
It is enough to debunk you false claim about inefficiency of
dispatch implementations. One thing at a time.
LOL! If that's your goal it helps explain some of your replies!!!
And it's a vain hope. You should realise you are wrong! :-)
I presented an implementation of dispatch which complexity is adding
offset and doing call. Is that inefficient?
No, it's efficient as long as the compiler knows the offset at compile
time.
And it does.
[...]But you converted the type in assignment. s is Shape, it is not
Circle. Why on earth it should look after any tags? Oh, each call
must always go through the vptr? This is what C++ does. It is
inefficient and semantically wrong.
Simply put, in this case all Shapes would have a Draw function.
Irrelevant, they must have. Answer the question, why do you want to to
check tag?
Sysop: | Keyop |
---|---|
Location: | Huddersfield, West Yorkshire, UK |
Users: | 296 |
Nodes: | 16 (2 / 14) |
Uptime: | 28:16:28 |
Calls: | 6,648 |
Calls today: | 3 |
Files: | 12,193 |
Messages: | 5,328,163 |