Some thoughts about "modern" pascal, generics, code and data structures - Synopse


http://blog.synopse.info/post/2014/09/13/Some-thoughts-about-%22modern%22-pascal%2C-generics%2C-code-and-data-structures

Comments

  1. At one point you criticise generics for not being as flexible as variants. Well... they're not supposed to be - generics are about furthering strong, static typing, the opposite of variants, which are about weak, dynamic typing. You also write of 'the generics overhead'. Do you mean the generated code bloat Delphi's generics implementation can be subject to? Perhaps not, given you talk of C# generics in the very next sentence... which makes it unclear what 'overhead' you are alluding to.

    ReplyDelete
  2. IMO, Generics is a paradigm shift that is almost as significant as the move from structured programming to OOP.  It takes a little while to wrap your head around the possibilities and to recognize it's limitations, but it is worth the time and effort.

    For us, Generics gave a 15-20% over-all reduction in the size of our code base, and it has given us a unified way of working with lists, collections, stacks, queues, and dictionaries for a large number of structures and classes.

    Generics can eliminate some of the inheritance issues of OOP, such as type branch incompatibilities, and it simplifies abstraction, containment and aggregation while maintaining type safety.  IMO, Generics helps enforcing SOLID since the generic container has to remain largely ignorant about the actual type of its contents.

    The benefits from Generics contra code generation is that with Generics, it still is possible and easy to do custom solutions where necessary, without breaking the code, or having a code-regen undo those solutions.

    If I break something, it will quickly surface, as the same flaw will hit all the different uses.  When I fix something, it is fixed for all use.

    Does Generics solve all problems?  No.  

    There are annoying limitations , such as the lack of declaration type specifiers that would allow limiting the argument type of a generic to f.x. sets and enumerables.

    There are Generics classes in the current RTL that frankly could do with a rewrite.

    It still is possible to program yourself into a corner, make thing overly complex, and break the SOLID rules over and over.

    Generics doesn't make you a better programmer, but it can help you simplify and unify code that otherwise may have been duplicated.

    ReplyDelete
  3. A. Bouchez My firstname is written with an f, just saying ;)
    Also you are shortsighted if the only thing you can think of is collections when it comes to the use of generics.

    ReplyDelete
  4. A. Bouchez I'm not quite sure what you mean by "IMHO as programmers, we should focus on data structures, not on code." (I did follow the link to Linus Torvald's quote, "Bad programmers worry about the code. Good programmers worry about data structures and their relationships.")

    To some extent I get what you mean. With the right data structures (or right organisation or representation of the data a program manipulates) I find the code almost writes itself - it can still be tricky of course, the way code can be, but with a clear understanding of data flow and manipulation (let's expand "algorithms" to cover this) the structure of the code becomes crystal clear and sometimes it feels like it's just writing something that's already there, waiting to be written.

    But I'm not sure how that quote applies to not using generics. In fact I find generics invaluable, not just for data structures but for sharing code among implementations. A generic class that applies the same behaviour (algorithm?) to two data types can be very useful.  In a sense, you could think of this as inheriting the algorithm not inheriting the data type (not quite the right way of putting it, but an interesting way of seeing one thing that generics can give you beyond just traditional OO structures.)

    Most of my code is not generic, but the parts that are are parts I find almost impossible to envisage writing any other way.  It's more than just collections. They're a paradigm you "get" and once you get it, you realise how useful and elegant it can be.

    I know you spend most of your time in D7, eg with the enhanced RTL you publish. Do you ever feel like you're missing out? There are some amazing features in newer versions, of which generics is only one.

    ReplyDelete
  5. Data structures alone solve nothing. You need to manipulate those data structures and that's where the code comes in. So no wonder people focus on code as well.

    With generics you can do more than write just a simple TList container. It makes it a lot easier to introduce functional concepts for one, which in turn can make it much easier to manipulate those data structures. At least in my view :)

    While I prefer templates, I would really miss having neither.

    ReplyDelete
  6. Algorithms + Data Structures = Programs (c) Niklaus Wirth :)

    ReplyDelete
  7. David Millington As I stated, I use heavily generics on c# and even our little mORMot supports latest versions of Delphi and have generics methods, if needed. I "got" the paradigm, thank you.
    And I am still not convinced. As Go designers do, BTW....
    Please do not make shortcuts on my abilities. :-)
    And when I see how ARC is implemented, generics floated and compatibility broken in the NextGen compiler..... I am sorry to say that I am quite confident I am not missing much.
    Only with Smart did I find some real fresh air.
    All this is very personal and I respect your diverse POV.
    But I can assure you that my Software design is not the one I used 20 years ago. Take a look at mORMot documentation and you may find out where I go. I am no dinosaur lost in my cave. :-D

    ReplyDelete
  8. A. Bouchez Not a dinosaur but a marmot - scnr :p

    ReplyDelete
  9. Lars Fosdal
    I shared the same POV first, when switching to generics use for most of my code.
    Then I stepped back a little, go around e.g in Go or fuctionnal languages, and ask myself if this was really worth it. Even the Delphi RTL does not use them much.
    BTW I am confident you can reduce 20% of your code base by any clever rewrite. And generics in Delphi will increase your executable size by 30%. ;-)
    I am missing generics in some low level cases of low level code, in Smart, to be honest.
    But as implementation details.
    What you state about generics benefit can be achieved with OOP + liskov patterns.
    Generics are a tool. But I see a lot of code which just tends to be written like if anything was a nail... :-D especially in object pascal, where we have better ways to implement patterns.
    Projects where almost every class use a generic just smells to me.
    I just wanted to point out the fact that we always need to step back and understand that we are creating code for CPUs, not for compilers.
    Generics are not evil. They could be great. But pascal has some other strengths.... We all love.
    Point of my article was just an occasion to step out. :-)

    ReplyDelete
  10. Stefan Glienke yes a mORMot which will go back hibernating in his cave with his objects for the winter... :-P

    ReplyDelete
  11. A. Bouchez A SOLID hibernate I guess :)

    ReplyDelete
  12. Stefan Glienke yes it is 1:30 am here.... Time to go....

    ReplyDelete
  13. Stefan Glienke - I agree.  We had a lot of boilerplate code which basically was duplicated for various classes (a large number of classes) and where various classes had various fixes and workarounds.  In essence, it was same or similar code.  Not very fancy or complicated, just duplicated.  Generics was like a hot knife through butter.  Gone is the duplication, and variation due to spot fixes.

    In aggregation, Generics allows me to see the actual type as opposed to base classes with virtual overrides.  This means that I no longer have to use potentially unsafe type casts to get at methods that are not exposed in the base class, without having to write a another layer of wrapper classes.

    We all have our coding style.  Generics has certainly made me rethink how to write multipurpose code. Once you get used to that flexibility, there is no going back.

    As for executable size - for the Windows platform in the corporate world, that is not a point of worry at all.

    Also - I am not creating code for CPUs nor for compilers.  I am creating code for programmers.  The sole intent is that the code is robust, readable and maintainable.  

    I trust the compiler to make sense of that code and hopefully make assembly that the CPUs doesn't find too hard to digest.

    ReplyDelete
  14. A. Bouchez You are looking at generics usage only through OOP. The part where generics are the most useful is when applied to non-class types. Unfortunately, this is also the place where Delphi falls short because there is not much you can do with such types besides storing them in collections.

    ReplyDelete
  15. Generics provide another kind of polymorphism. This is something that was brought from functional programming to us.

    ReplyDelete
  16. Lars Fosdal You can significantly reduce generics code bloat if you create base class that will deal with some base object type, and then add generics with type safety on top of that base class.

    ReplyDelete
  17. Dalija Prasnikar Which only works if you have a common base type. I was experimenting a bit with it for the Spring4D collections to reduce the code bloat for object lists. But that does not help for other types like dictionaries that inherit from TCollectionBase>. If you only have one standalone generic type it's easy but it get's harder if you are referring another generic type that does refer to yet another generic type. You then need to create 3 base classes with generic wrapper around them and make sure that you provide the type information about the generic type argument into the base class depending on what you are doing with it.

    ReplyDelete
  18. Dalija Prasnikar Most of our database/json supporting classes share a common ancestor. These classes don't really know much about the database they store and retrieve themselves to and from, and the little they do know is inherited. For JSON, RTTI and attributes does most of the job.

    ReplyDelete
  19. Dalija Prasnikar non-class types is where templates just dominate generics.

    However both template and generics have tendency to turn into those huge obfuscating  frameworks, where an innocuous line of code can result in very long and near-impossible to debug call stacks, as the layers get piled up, and you end up pulling megabytes of source code dependencies.

    Language-supported lists and array comprehension do not have that disadvantage: they allow the code to stay simple both at coding-time and at debugging-time.

    Lars Fosdal I disagree about not being able to go back from generics. Go, Python, JS or any language with powerful lists/arrays will cut not just your number of lines of code, but the length and complexity of those lines of code as well.
    They will also cut the call stack depth (and very drastically so if you are starting from Java generic frameworks f.i.).

    And if that's not enough, lists are often able to exhibit much better performance, as the compiler can "know" things about a list/array it cannot about a generic. It can automatically unroll, parallelize, use SIMD, etc.
    Contrast that with a TList where you can't even access an item without a reference count and an implicit exception frame (vs none of these when accessing an item for an array of string).
    And the more complex the generics, the worse the overhead becomes.

    Last advantage is that languages with list/array comprehension you have a standard for collections for all source code in that language, vs having many "standards" when Generic Frameworks are involved, as every framework has its own semantics and quirks (plus changes across framework versions), and you have multiple collections that do "almost the same thing, but not quite".
    Sheesh! Give me one well-tuned collection that is solid and robust rather than 12 corner-case collections, each with different gotchas.

    If you liked the step up to generics, you would probably love the step up to array and list comprehension, dumping another 20% of lines code in the process and a lot of scaffolding.

    ReplyDelete
  20. Eric Grange - Isn't that a bit apple and pears compared?  I still work in Delphi, after all?

    I've done lists, trees and arrays in Pascal for years, and they can be handy and efficient, but I also did a lot of explicit casts - and sometimes the wrong explicit cast.

    Does the Delphi compiler really do unrolling, parallelizing, and use modern CPU instructions better for non-generics code, than for generics code?  

    As for performance - although I'd prefer stellar performance, most of my challenges with performance are dictated by external world interfacing.  The Delphi applications rarely have issues with keeping up.

    ReplyDelete
  21. Lars Fosdal I was reacting to your "can't back to before generics" :)

    If Delphi added or had added proper list/array comprehension , I'm sure you would be dropping generics lists asap.
    Add a compiler supported dictionary ("array [string] of Xxx" f.i.), and generic collections become corner-case.

    As to the other points:

    The Delphi compiler does have more optimizations when dealing with arrays than it does with TList<> (and other generic collections), and the debugger has better support of them for that matter (though it's still far from optimal).
    While the Delphi Windows compiler does not unroll or parallelize a "for x in array", it often generates more efficient code for it than with GetEnumerator, and it could unroll/parallelize in a future version, while it couldn't for a generic collection, short of adding fragile framework-specific peepholes optimizations in the compiler.

    (That said, I have not checked as I do not have them, but the LLVM Delphi compilers might already be capable of doing some unrolling and parallelization, at least LLVM is capable of it with other languages)

    "array of X" is as strongly typed as TList, you need explicit casts for neither of them.

    But yes, the Delphi language is not quite there, and does have fairly incomplete array/list support, which is a pity, but when you look at other languages that do, the benefits are clear, and the required improvements are not that many.
    TArray<> was doing it the wrong way IMHO, and just added complexity & scaffolding over a simple concept, but the new XE7 syntax is going in the right direction. Hopefully they'll pursue it further, make them into full indirections & add compiler-supported standard methods (map, sort...).

    ReplyDelete
  22. Eric Grange There is more than arrays and dictionaries when it comes to collections. You want them all to be built into the language? Also as already said generics are not only for collections, although that's the usecase everyone that hasn't worked much with them names first.
    Try designing a typesafe fluent interface without generics or type inference for instance.

    ReplyDelete
  23. Stefan Glienke Language can do list & dictionary, if done right that covers fifo & stack, possibly lifo, which takes care of the bulk of "everyday" collections. That leaves out linked-lists, uniform trees and graphs, but a generics foundation would likely often be inappropriate for these in many cases (because of the inheritance constraint, you would be compromising the design to fit the generic constraints rather than the problem's constraints).

    And IMHO generics (in Delplhi) are more problem than solution outside of collections because of the interface scaffolding they require whenever you need any form of processing. Oh, that can be done, but you will then be looking at so much scaffolding outside of trivial stuff that most of the time, the outcome will only be worthy as a coding curiosity.

    Case in point: just look at the scaffolding for a simpler sort / comparison in the RTL... and that's just for a comparison. And while it works okay-ish for float or integers (if slowly and inefficiently), it's already not quite there for strings. Let alone anything more complex.
    At some point, the base requirement that a generic be all-aware of all its potential uses means it has to become either bloated, useless or fairly specific.

    Another illustration are the generic frameworks that rely on RTTI for performing processing, which in essence boils down to a poor-man's duck-typing... with all the issues that brings: runtime-only error detection, fragility, complexity to debug, etc.

    Doing more than collections "properly" means templates, compiler-supported duck-typing, or a JIT compiler & debugging suite.

    ReplyDelete
  24. Eric Grange For non-class types I would prefer templates because they are far more appropriate and powerful.

    Like you said, both generics and templates can backfire with complexities, but that doesn't mean they are not quite useful in simpler scenarios, where you can replace plenty of duplicate code and reduce maintenance costs.

    ReplyDelete
  25. Dalija Prasnikar I do not say that generics are not useful, but that they are not the only tool in the toolbox, and probably not the "best" way to handle the simple collections (lists/arrays/dictionaries/stacks).

    ReplyDelete
  26. Interesting discussion. However in Delphi we have generics. So unless I want to leave for another fancy flavor of the month language that has this or that awesome feature. How about talking how to deal with generics in Delphi and how to improve them. Because it does not help me that DWS has this or that or that go or some other language deals with them in some particular way.
    And on a side note. Even if arrays and lists would have been more integrated into the language I might not want to fully expose them outside of my classes as I can do with something like IEnumerable or IReadOnlyCollection or IReadOnlyList. So in such case I still would need to write a wrapper around them.. how to do that without generics?

    ReplyDelete
  27. Stefan Glienke Well my point is that rather than talking about improving generics in Delphi,  we could be talking about improving the language so that generics (and their complexity) would be unnecessary for most of the code, with immediate benefits in readability and maintenance.

    Also generics were introduced as ".Net mimic", except without a JIT, there are things Delphi won't be capable of doing with them that .Net can. And their syntax is as alien to the Pascal syntax as can be (cf. the more Pascalish syntax FreePascal used for them initially). I'm mentioning here simple extensions that flow with the existing Delphi syntax and language.

    For readonlys, your exemple illustrates the exact trouble you have with generic frameworks: the  generic interfaces you named basically dance around the same concept, with slightly different semantic and different gotchas. And then you'll have an IReadOnlyStack, an IReadOnlyTree, etc. and likely several of each those as frameworks will reinvent them.

    But for read-only collections, what about not just having a proper, compiler enforced "const" qualifier? Not just at the parameter level but also at the type level. That solves your read-only problem in those cases, and many others, like compiler-enforced class/record immutability, and it also makes a step towards introducing functional programming seamlessly.

    And for the enumeration, what about just having a compiler and language supported iterator concept?
    That's much broader than a framework-based IEnumerable, more flexible, and can completely be compiler-enforced and optimized, vs having to re-implement the enumerable methods in all collections, with various gotchas, inefficiencies, and (usually) poor debuggability. No more need to have a "fallback" duck-typed GetEnumerator record either for things that aren't classes.

    ReplyDelete
  28. Sad part of this discussion is that no matter what we agree or disagree on here, it will have zero influence on actual language and RTL.

    Delphi as language and its core frameworks need to evolve systematically and at much faster rate than it is happening right now. What we have now are sporadic changes in compiler and RTL based on what is the easiest thing they could do to make it look like they are changing something, instead of having real meaningful evolution of features.

    I don't see such evolution possible without open discussion between Delphi team and community about what, when and how.

    ReplyDelete
  29. I keep wishing, while I keep working with what I've got. It's not perfect, but it's there.

    ReplyDelete
  30. Dalija Prasnikar Not going to happen as long as some people act as hostile as they do because they know everything much better.
    Also not going to happen because the majority of the people using Delphi are not asking for such things - not the ones shouting loudest.

    ReplyDelete
  31. Stefan Glienke you mean it is not going to happen because they are afraid of people that could be wrong on the internet? :) http://xkcd.com/386/

    ReplyDelete
  32. Eric Grange Stefan Glienke No, they are afraid they might be wrong on the internet ;-)

    ReplyDelete

Post a Comment