Which language is the least bad?


  • Impossible Mission - B

    @blakeyrat said in Which language is the least bad?:

    If do you using( var blah = new Blah() ){}, and Blah's constructor does using( var ted = new Ted() ){}, and the outer using scope goes away, all of those dispose calls happen right away in the reverse order of the object initialization.

    If a constructor has a using statement, the Ted will be disposed at the end of the constructor call, not when blah is disposed. What he's talking about is putting the Ted as a field of Blah, and how that won't be automatically disposed just because Blah is disposed, unless you explicitly write it into the Dispose method for class Blah. (This can be done in Boo with metaprogramming, but in C# and Java there's no way to do it except manually.)


  • Impossible Mission - B

    @dkf said in Which language is the least bad?:

    @masonwheeler said in Which language is the least bad?:

    The definition of OOP for decades has been support for the so-called "three pillars:" encapsulation, inheritance, and polymorphism. If you don't have these three pillars, you have something else that is not OOP.

    You happen to be wrong, but you're entertaining anyway.

    Well, if we want to be super :pendant:ic, it's true that there are some who add a fourth pillar, "abstraction," but that doesn't really change the point I was making.


  • Impossible Mission - B

    @bulb said in Which language is the least bad?:

    If you are used to Java or C#, you are used to rely on the convention that methods only modify the invocant and hope for the others to follow it and mostly it's good enough.

    No.

    No, no, no. This is simply not the case. I don't know where you got this idea, but it's pure straw.



  • As I have pointed out before, when comparing Turing-complete languages, you aren't talking 'power' at all - they are all equally powerful by definition, due to being equivalent to a Universal Turing Machine.

    The relevant form of 'power' here is expressiveness. And you know what? That's entirely subjective. You cannot effectively compare languages in an objective way because there are no objective measuring sticks for them.

    Oh, you can come up with a suite of criteria about it, saying that having such-and-such features and not having such-and-such flaws can make a language more or less 'powerful', but those criteria are inherently arbitrary - they are things people feel are better, and while they can measure how much 'better' they are in some ways using some scale they devised, they still reflect the priorities of the ones who made the scale, not the actual languages.

    In another thread I previously stated, somewhat facetiously, that the only 'objectively' better language was Ada, and that despite being allegedly better for software engineering that the language was unacceptable to most programmers. The point I was trying to make was that trying to base language design on some set of fixed criteria doesn't necessarily - or even usually - result in a language coders will find useful.

    But consider this: My main goal is flexibility. I want a language which I can tailor to use the syntax and semantics I want for a given task, and be able to the get those different sub-languages to talk to each other in a somewhat sensible way, despite having different paradigms. The languages I tend to be drawn to - the Lisp family, Forth, some of the FP languages - reflect that. I can live with this because my goals are related to research in language design, but most people wouldn't find them inviting at all. I want a language that is the equivalent of an aerospace wind tunnel or a chemistry lab.

    @blakeyrat, on the other hand, is almost entirely task-oriented - he wants to write solid, usable software which isn't actively hostile to the ordinary users. The language itself is secondary - he wants something that makes writing applications fast and reliable, without too many warts in the code, but he's only concerned about the code itself to the extent that it isn't so ugly or complicated that it makes debugging and maintenance more trouble than it is worth. He wants a language that is like a commuter train car - it doesn't have to be fancy, it just gets the job done for people.

    And frankly, that's probably more rational than the rest of us, WRT to making user applications that don't suck. Not everyone is making user applications, though, and perhaps more importantly, not everyone who is making them wants to be.

    His view makes a lot of sense for that kind of programming. But for research? Or database design? Or compiler development? Not so much.

    There's just no way I can think of that those different sets of requirements are going to mesh. And I'll bet it is the same for everyone else here.

    So it might be worth looking at the criteria, and explaining to each other what we each think makes a language 'powerful', before we keep flaming pointlessly. Well, unless pointless flaming is the whole purpose of the exercise, in which case, fire away.


  • Impossible Mission - B

    @scholrlea said in Which language is the least bad?:

    In another thread I previously stated, somewhat facetiously, that the only 'objectively' better language was Ada

    Based on what objective criteria? For various criteria X, there are plenty of languages that do X objectively better than Ada. (And if it's what I'm thinking of, what Ada is famous for, there are those who would argue that Haskell is objectively better than Ada.) If you're going to make a claim like that, you have to establish the rules.

    Afterall, how are the rest of us going to have fun :moving_goal_post: if you never set out any goal posts in the first place? :P


  • Discourse touched me in a no-no place

    @blakeyrat said in Which language is the least bad?:

    @dkf said in Which language is the least bad?:

    I think he means that if you glue some objects together in a larger object, that also obeys RAII finalisation rules.

    But that happens with using too.

    If do you using( var blah = new Blah() ){}, and Blah's constructor does using( var ted = new Ted() ){}, and the outer using scope goes away, all of those dispose calls happen right away in the reverse order of the object initialization.

    I don't believe that does what you want. The lifetime of ted is that block right there, not the lifetime of the blah object. The whole point of the RAII is that it makes the disposal of ted automatically tied to the blah.

    If I've not totally misunderstood. :-)

    It is true that you can't do: using( var foo = new Foo(), var bar = new Bar() ){} in C# if Foo and Bar are different types. I'm not sure why it's not allowed, but I'm sure the C# designers had a good reason for it.

    It's because they used the local_variable_declaration production in the syntax rules as opposed to a sequence of them (declaration_statement but not quite; that has semicolons as terminators, not separators). Multiple values of the same type are only supported because local_variable_declaration itself supports them.

    (Names above refer to syntax production rules in the language spec.)

    Java used slightly different syntax, and so supports multiple variables of different types. It's not a huge deal; the syntax form is arbitrarily nestable after all.

    I'm not sure how RAII differs.

    RAII differs because it is a trick that uses the C++ exact object lifetime semantics. At the point where the object drops out of its ownership scope (as opposed to scopes that hold references or pointers to it), it gets immediately destroyed. Both Java and C# explicitly reject that sort of approach by virtue of using garbage collection, and instead go for requiring more explicit disposal-aware code.

    In practice, it means that instead of doing:

    using (var locked = theLock.lock()) {
        // ...
    }
    

    or:

    try (Locked locked = theLock.lock()) {
        // ...
    }
    

    they instead do:

    {
        Locked locked(theLock);
        // ...
    }
    

    (Leaving aside the whole business of how to make classes participate in all this.)


  • Banned

    @masonwheeler said in Which language is the least bad?:

    @gąska said in Which language is the least bad?:

    Where did you get this stupid misconception that objects require class keyword?

    Where did you get the idea that I have such a stupid misconception?

    Because you said...

    Objects do, however, require inheritance and virtual dispatch. Without that, you have something else that is not OOP. It might be better to say that OOP requires the base keyword.

    ...which is not true. There are many ways to do OOP, only some of which require inheritance. It's just different kind of OOP. Not to mention you can have objects without OOP. Object is a very flexible term, and it's best to keep all discussions about what is and isn't object within the context of a particular language. Object in Java is different from object in Python and different from object in C++ and different from object in JavaScript.

    To give a real-world example, Go has interface-based polymorphism, which allows you to create multiple struct types that follow the same interface, and you can build a tree out of them, but you can't implement a proper Visitor Pattern to walk that tree, the way you can in OO languages.

    You can do visitor pattern in Go and Rust by downcasting from interface/trait object. And in 99% cases where you'd use visitor pattern in Java, Rust's enums are better.


  • Banned

    @bulb said in Which language is the least bad?:

    And, well, it actually solves the issues with objects-as-value-types, because the bases—interfaces only—can't ever be by value, only the concrete types can

    Not entirely true in Rust. There's this thing called trait object, which is variable-sized object that's taken by value and implements a given trait. And yes, it's dynamic, not static. It comes with some limitations though.

    @masonwheeler said in Which language is the least bad?:

    C++ has const for this, in Java and C# it requires verbose interfaces.

    C++ also has const_cast to work your way around it.

    I've been doing C++ for almost a decade and not even once used const_cast. And I mark everything I can as const. It's very useful because then you can safely make some very strong assumptions about the state of your local variables after calling some functions.

    Anders Hejlsberg once suggested that that's the only reason const works in C++ at all, because you can cast your way out of it when you need to.

    He forgot that casting away constness breaks compiler's and class's internal invariants which might result in logic bugs, data loss, inconsistent behavior, or even access violation due to writing to read-only memory location. It's very similar to goto in that it's taught to new programmers as a thing they should never, ever use under any circumstances, and for very good reason.


  • Banned

    @masonwheeler said in Which language is the least bad?:

    @bulb said in Which language is the least bad?:

    @masonwheeler said in Which language is the least bad?:
    They do have inheritance. Of interfaces only, but that's still inheritance.

    No, that's implementation of an interface.

    Technically they inherit the default implementation. But since it doesn't work with intermediate traits, I'm hesitant to call it inheritance.


  • Banned

    @masonwheeler said in Which language is the least bad?:

    The definition of OOP for decades has been support for the so-called "three pillars:" encapsulation, inheritance, and polymorphism.

    [citation needed]


  • Impossible Mission - B

    @gąska said in Which language is the least bad?:

    ...which is not true. There are many ways to do OOP, only some of which require inheritance. It's just different kind of OOP.

    No, it's something else. Words have meanings.

    You can do visitor pattern in Go and Rust by downcasting from interface/trait object.

    See my explanation about deciding when to recurse, above. Without a base visitor class and overridden handlers for different node types in an inherited class, there's no good way to do the visitor pattern in general. (Which is not to say that it can't be done; there are several bad ways. But doing it well requires inheritance and virtual methods.)

    And in 99% cases where you'd use visitor pattern in Java, Rust's enums are better.

    I'll have to take your word for it, as I know nothing about Rust's enums, how they're different from other languages' enums, or how they could possibly be equivalent to the Visitor Pattern. 😕


  • Impossible Mission - B

    @gąska said in Which language is the least bad?:

    @masonwheeler said in Which language is the least bad?:

    The definition of OOP for decades has been support for the so-called "three pillars:" encapsulation, inheritance, and polymorphism.

    [citation needed]

    Seriouly? Take a freaking 100-level CS course!


  • Banned

    @masonwheeler said in Which language is the least bad?:

    @gąska said in Which language is the least bad?:

    ...which is not true. There are many ways to do OOP, only some of which require inheritance. It's just different kind of OOP.

    No, it's something else. Words have meanings.

    And the meaning of OOP is bundling code and data together in nice little packages called objects, and virtual dispatch in one way or another. Just because Java's OOP model is the most prevalent in the world doesn't mean it's the only valid one.

    @masonwheeler said in Which language is the least bad?:

    Without a base visitor class and overridden handlers for different node types in an inherited class, there's no good way to do the visitor pattern in general.

    Visitor pattern is basically a switch statement where the switch instruction isn't written explicitly.

    @masonwheeler said in Which language is the least bad?:

    I'll have to take your word for it, as I know nothing about Rust's enums

    They're basically tagged unions.


  • Impossible Mission - B

    @gąska said in Which language is the least bad?:

    And the meaning of OOP is bundling code and data together in nice little packages called objects, and virtual dispatch in one way or another. Just because Java's OOP model is the most prevalent in the world doesn't mean it's the only valid one.

    Simula's OOP model. Java didn't invent it. C++ didn't invent it. It came from Simula, an extension to ALGOL, and it's been the way to do OOP ever since. With the sole exception of JavaScript, which can be explained by monopoly economics, every language that's tried some other model (generally Smalltalk's) has been a failure in the marketplace of ideas.

    Visitor pattern is basically a switch statement where the switch instruction isn't written explicitly.

    Visitor pattern is a combination of the "non-explicit switch" and tree walking. Without inheritance, you can emulate the non-explicit switch with an explicit switch, but you can't get the tree walking right.

    They're basically tagged unions.

    All right. But that still won't give you tree walking in arbitrary logic.


  • Banned

    @masonwheeler could you explain the tree walking part in more detail? Simple examples of visitor pattern don't look much like a tree, and I've never had a complicated enough visitor to notice.



  • @gąska said in Which language is the least bad?:

    It's very similar to goto in that it's taught to new programmers as a thing they should never, ever use under any circumstances, and for very good reason.

    On a side note: much like goto, new programmers should be shown an example where it makes sense and then there should be a discussion why it's to be avoided and how to do it properly. Any argument that is basically "because everybody thinks so"/"because I think so"/"because." is bad.


  • Banned

    @cvi s/new/intermediate or even semi-advanced/


  • Impossible Mission - B

    @gąska A visitor can be used to "visit" single objects, but the place where it really shines is tree walking, particularly when working with highly heterogeneous trees. There's a reason why so many basic tutorials about the Visitor Pattern involve very simple expression trees: if you look inside a compiler, most of the logic is implemented as visitors.

    Let's say we had a simple expression tree for arithmetic. Highly contrived example:

    class Node:
        Value as number
    
        abstract def Visit(visitor as ArithmeticVisitor)
    
    class Literal(Node):
        override def Visit(visitor as ArithmeticVisitor):
            visitor.OnLiteral(self)
    
    class Operator:
        Kind as OperatorKindEnum
        Left as Node
        Right as Node
    
        override def Visit(visitor as ArithmeticVisitor):
            visitor.OnOperator(self)
    
    class ArithmeticVisitor:
        def Visit(node as Node):
            node.Visit(self)
    
        virtual def OnLiteral(node as Literal):
            pass
    
        virtual def OnOperator(node as Operator):
            Visit(node.Left)
            Visit(node.Right)
    

    Here we have a simple expression tree and a visitor that knows about the tree structure and walks it. Every node has a value, but only literals get their value assigned inherently. Now let's say we want to build an evaluator. If we knew that the left and right sides of every operator was a literal, it would be trivial, but what if you have an expression like 3 + 4 * 8, with standard mathematical precedence conventions applying? In tree form, the right side of the + node is a * node, and that means that you have to ensure that all child nodes are evaluated before evaluating the parent node.

    class ArithmeticEvaluator(ArithmeticVisitor):
        override def OnOperator(node as Operator):
            super(node) //evaluate left and right
            var l = node.Left.Value
            var r = node.Right.Value
            switch node.Kind:
                case OperatorKindEnum.Addition:
                    node.Value = l + r
                case OperatorKindEnum.Subtraction:
                    node.Value = l - r
                case OperatorKindEnum.Multiplication:
                    node.Value = l * r
                case OperatorKindEnum.Division:
                    node.Value = l / r
    

    If you don't have any way to make an inherited method call to the walking logic, you have to do it the way Go AST does it, by having the walker be a higher-order function that you pass the visitor to as an argument, and your visitor's return value tells it whether or not to recurse further into the tree. That means you can't cleanly recurse into the tree first and then perform logic on the parent node afterwards. Generally in order to do that, your visitor function has to call the walker on the child nodes (thus requiring it to know about the walking logic that it's supposed to be cleanly separated from) passing itself as an argument (which is just really weird.) Having run into cases where I've had to do this, it's much, much more cumbersome than a proper OO visitor.


  • Banned

    @masonwheeler I've got a little confused about your walker/visitor split. I'm not sure which is which for you - the one that traverses the tree, or the one that operates on the nodes. So I'm gonna write a code where walker=traversing and visitor=operating, without inheritance, and without the problem you mention.

    class Node:
        Value as number
    
    class Literal(Node): //#/*; don't recognize this language, not sure about syntax */
        
    
    class Operator(Node):
        Kind as OperatorKindEnum //#/*; if we're going visitor, we might as well go full visitor and make each operator a child class, but whatever */
        Left as Node
        Right as Node
    
    class ArithmeticVisitor:
        virtual def OnLiteral(node as Literal):
            pass
        virtual def OnOperator(node as Operator):
            pass
    
    class ArithmeticWalker:
        Visitor as ArithmeticVisitor
    
        def Walk(node as Node):
            if node is Literal:
                Visitor.OnLiteral(node)
            else if node is Operator:
                Walk(node.Left)
                Walk(node.Right)
                Visitor.OnOperator(node)
            else:
                pass //#/*; actually report some kind of error or warning I guess */
    
    class ArithmeticEvaluator(ArithmeticVisitor):
        override def OnOperator(node as Operator):
            var l = node.Left.Value
            var r = node.Right.Value
            switch node.Kind:
                case OperatorKindEnum.Addition:
                    node.Value = l + r
                case OperatorKindEnum.Subtraction:
                    node.Value = l - r
                case OperatorKindEnum.Multiplication:
                    node.Value = l * r
                case OperatorKindEnum.Division:
                    node.Value = l / r
    

    As far as I can tell, it's functionally equivalent to your inherited visitor and doesn't introduce any sort of trouble. Is there something I'm missing?


  • Impossible Mission - B

    @gąska said in Which language is the least bad?:

    Is there something I'm missing?

    Now you've hard-coded child-first visiting semantics into the walker. It's the opposite problem as the Go AST walker has, which hard-codes parent-first visiting semantics into the walker.

    There are valid use cases for both versions, which is why that decision should be left to the visitor. But there's no good way to do that unless the visitor inherits from the walker.



  • @gąska said in Which language is the least bad?:

    @masonwheeler said in Which language is the least bad?:

    The definition of OOP for decades has been support for the so-called "three pillars:" encapsulation, inheritance, and polymorphism.

    [citation needed]

    Oldest reference I can find is from "The Simula Handbook", 1979 [which was pretty late, so I am pretty sure that is not the first printing date.


  • Banned

    @masonwheeler said in Which language is the least bad?:

    @gąska said in Which language is the least bad?:

    Is there something I'm missing?

    Now you've hard-coded child-first visiting semantics into the walker.

    Or did I? I can always use different walker if I want to. It's actually easier to change traverse semantics in my code than when using inheritance (you cannot hot-swap base class).


  • Impossible Mission - B

    @gąska said in Which language is the least bad?:

    Or did I? I can always use different walker if I want to. It's actually easier to change traverse semantics in my code than when using inheritance (you cannot hot-swap base class).

    Not really, because the walker holds a reference to the visitor. If the visitor held a reference to the walker, that might be true, but that would really complicate the architecture.

    Also, now that I think of it, your version actually makes one thing worse. Not only can you not determine traversal order in the visitor, you also can't choose not to recurse into child nodes. This is frequently a useful optimization, and in some cases it can change visible semantics as well as the run time of the operation.


  • Banned

    @masonwheeler said in Which language is the least bad?:

    Not really, because the walker holds a reference to the visitor.

    And why is that a problem? All I need is an abstract interface for whoever is using walker.

    @masonwheeler said in Which language is the least bad?:

    Also, now that I think of it, your version actually makes one thing worse. Not only can you not determine traversal order in the visitor, you also can't choose not to recurse into child nodes. This is frequently a useful optimization, and in some cases it can change visible semantics as well as the run time of the operation.

    When the choice is to traverse all children or traverse no children, then visit() returning bool solves the problem. When you need to selectively traverse some children, then it becomes worse - but so does the inheritance approach (you can't super() anymore; you have to repeat it by hand).


  • Impossible Mission - B

    @gąska said in Which language is the least bad?:

    And why is that a problem? All I need is an abstract interface for whoever is using walker.

    It means that the walker is calling into the visitor, and the visitor can't change its walker. It could instantiate a different walker, but the one that owns it would still do its traversal anyway, which is not what you want.

    When the choice is to traverse all children or traverse no children, then visit() returning bool solves the problem.

    At the cost of requiring parent-first visiting. Now we're back to square 1.

    When you need to selectively traverse some children, then it becomes worse - but so does the inheritance approach (you can't super() anymore; you have to repeat it by hand).

    No. I'm not talking about "selectively traversing some children", I'm talking about deciding to traverse no children or all children, while also retaining the ability to do do the traversing at any arbitrary time, including not at all.


  • BINNED

    @scholrlea said in Which language is the least bad?:

    In another thread I previously stated, somewhat facetiously, that the only 'objectively' better language was Ada, and that despite because of being allegedly better for software engineering that the language was unacceptable to most programmers.

    All of the features that make it better for software engineering make it more of a pain in the ass to use, and you get RSI just typing in "Hello World".


  • Banned

    @masonwheeler said in Which language is the least bad?:

    @gąska said in Which language is the least bad?:

    And why is that a problem? All I need is an abstract interface for whoever is using walker.

    It means that the walker is calling into the visitor, and the visitor can't change its walker.

    The visitor isn't even aware of the existence of walker. That's the nice thing about encapsulation - limiting things that go wrong and things that are stuck together by limiting how much each part knows. What I meant is that whoever calls walker, can have it easily swapped for something else, leaving the visitor intact.

    @masonwheeler said in Which language is the least bad?:

    When the choice is to traverse all children or traverse no children, then visit() returning bool solves the problem.

    At the cost of requiring parent-first visiting. Now we're back to square 1.

    If you want non-parent-first traversal so bad, have a separate optional<bool> ShouldTraverse(Node node) method on your visitor.

    @masonwheeler said in Which language is the least bad?:

    When you need to selectively traverse some children, then it becomes worse - but so does the inheritance approach (you can't super() anymore; you have to repeat it by hand).

    No. I'm not talking about "selectively traversing some children", I'm talking about deciding to traverse no children or all children, while also retaining the ability to do do the traversing at any arbitrary time, including not at all.

    Then the only thing left is have traversal and visiting combined into single object. The downside is that they're then inherently bound together (exactly like with inheritance). Still, if you refactor the traversal into separate named method so that visitFoo() calls traverseFoo(), you can do without inheritance at all. And if you want several different visitors with the same traversal logic, even then a full-fledged inheritance isn't always needed - if the visitor hierarchy is flat, and they inherit no member fields, then Rust's traits (interfaces) + default implementations will do just fine.


  • Impossible Mission - B

    @gąska said in Which language is the least bad?:

    The visitor isn't even aware of the existence of walker. That's the nice thing about encapsulation - limiting things that go wrong and things that are stuck together by limiting how much each part knows. What I meant is that whoever calls walker, can have it easily swapped for something else, leaving the visitor intact.

    Which is great, right up until you need different visitation-order semantics in different parts of the same tree.

    If you want non-parent-first traversal so bad, have a separate optional<bool> ShouldTraverse(Node node) method on your visitor.

    And now you've greatly increased the complexity of your visitor.

    Then the only thing left is have traversal and visiting combined into single object. The downside is that they're then inherently bound together (exactly like with inheritance).

    No, it's even worse, because then you have to implement the traversal logic all over again for every visitor, with the attendant risks of bugs, copy/paste errors, and so on. With inheritance, you get the visit logic and the walking logic bound together--which is not a downside when they're meant to be used together and neither one is particularly useful without the other--but you get a nice, clean separation of concerns. The parent class takes care of traversal logic, and it only has to be written once and then many visitors can inherit from it, and the descendant visitor classes take care of visiting logic and deciding when to traverse.


  • Banned

    @masonwheeler said in Which language is the least bad?:

    If you want non-parent-first traversal so bad, have a separate optional<bool> ShouldTraverse(Node node) method on your visitor.

    And now you've greatly increased the complexity of your visitor.

    You've done it the moment you decided it needs more complicated traversal logic. Complicated problems yield complicated solutions. Note that inheritance doesn't reduce any of that complexity - just moves it around a little bit.

    Then the only thing left is have traversal and visiting combined into single object. The downside is that they're then inherently bound together (exactly like with inheritance).

    No, it's even worse, because then you have to implement the traversal logic all over again for every visitor, with the attendant risks of bugs, copy/paste errors, and so on.

    Unless you do what I said in the part of my post that you've cut off.


  • Impossible Mission - B

    @gąska said in Which language is the least bad?:

    You've done it the moment you decided it needs more complicated traversal logic. Complicated problems yield complicated solutions. Note that inheritance doesn't reduce any of that complexity - just moves it around a little bit.

    It greatly reduces the complexity of writing 50+ visitors. (As is the case in the compiler I help maintain. All of this stuff I'm talking about is coming from real experience.)

    Unless you do what I said in the part of my post that you've cut off.

    Then I must have misread something, because I cut it off specifically because, as I understood it, the vistFoo() vs traverseFoo() concept only applies to writing the whole thing in one class. And having no state in the base class can be done for very simple visitors, but for a tree rewriter--where you might want to replace a node of one type with a node of another type where both types are valid in the slot in question--the walker needs to maintain state. (Either that or the entire tree needs to be immutable and you rewrite the tree into a copy with every pass, at a non-trivial performance penalty.)



  • I wonder, does anyone else recall that the original primary purpose of design patterns was as an aid for understanding existing code in the absence of adequate documentation? Just curious.


  • Impossible Mission - B

    @scholrlea No, I don't remember that. Link?



  • @scholrlea Hey, don't distract @masonwheeler from his self-righteous, more-enlightened-than-thou-plebians rant/trance!


  • Banned

    @masonwheeler said in Which language is the least bad?:

    @gąska said in Which language is the least bad?:

    Unless you do what I said in the part of my post that you've cut off.

    Then I must have misread something, because I cut it off specifically because, as I understood it, the vistFoo() vs traverseFoo() concept only applies to writing the whole thing in one class.

    Well, it's relevant with traits because you cannot call the default implementation once you override it. A limitation of Rust more than anything else.

    And having no state in the base class can be done for very simple visitors, but for a tree rewriter--where you might want to replace a node of one type with a node of another type where both types are valid in the slot in question--the walker needs to maintain state. (Either that or the entire tree needs to be immutable and you rewrite the tree into a copy with every pass, at a non-trivial performance penalty.)

    That I believe. Yes, I agree that with many complicated visitors in play, inheritance is the best way to go. The thing is, it's because the visitor pattern, like every other pattern in Design Patterns book, is strongly tied to the Java inheritance model, and is built to take advantage of that model. It's obvious that it can't be implemented as good in a language with different object model. But it doesn't mean the original problem - modifying an object tree by recursively traversing it - cannot be solved using different model, different patterns and different data structures, with the resulting code just as clean, concise and maintainable as with visitors.


  • Discourse touched me in a no-no place

    @gąska said in Which language is the least bad?:

    Design Patterns book

    Are we talking about the Gang-of-Four book here? If so, that was originally written for Smalltalk IIRC and only adapted to Java and C# later because that sold far better. In general though, design patterns don't need objects at all, just common ways of arranging code to solve particular problems. Also, they should have properly have descriptions of when to apply them, what the consequences are, and also when to not apply them. The whole point of a pattern is that it transcends particular code, and isn't some hammer to smash into everything that looks like a (thumb)nail.


  • Banned

    @dkf can your device display abbr tag hover text?


  • BINNED

    @dkf said in Which language is the least bad?:

    The key problems are once you move away from the standard library. The potential for dastardly tricks hidden behind operators whose application is invisible… well, let's just say that good code doesn't do it but there's also the other 99% of all code out there.

    I don't understand that complaint. I prefer writing a*b + c over plus(multiply(a, b), c), both because of the symbol names and because of the infix notation (and because I needed to double check that the latter expression actually is correct). Operator overloading is a useful feature and afaik C# has it, too. It's no more surprising to me what operator * does than what a multiply function does.

    1. It's bloatedly complicated.

    That is certainly true. The syntax is clinically insane, and for the more subtle cases you need a language lawyer to understand the semantics (but that's rare in practice).

    1. It remains thoroughly hostile to stable ABIs.

    If you want a stable ABI you can have one. Qt does this, for example.

    1. It's exceptions remain awful and expensive.

    As long as they're not expensive when not thrown (?) I don't care. I never have more than a handful try blocks anyway.


  • Discourse touched me in a no-no place

    @gąska said in Which language is the least bad?:

    @dkf can your device display abbr tag hover text?

    Yes. It's not particularly important to what I was saying though.



  • @bulb said in Which language is the least bad?:

    So the ted instance leaves the Blah constructor and gets disposed in Blah::Dispose? If so it would achieve the same goal as C++ RAII, but I wouldn't call it exactly readable, because the var ted appears, for all that's written, to be inside the Blah constructor and thus deleted at end of that, not in Blah::Dispose, which is a different function.

    You're right, it was a shitty example.

    @bulb said in Which language is the least bad?:

    using(var foo = new Foo(), var bar = new Bar()) { … } is no different from using(var foo = new Foo()) { using(var bar = new Bar()) { … }} and you can obviously write the later. No reason for that, really.

    I'm guessing the hitch might be that the latter has a prescribed order, and the former does not.

    @dkf said in Which language is the least bad?:

    It's because they used the local_variable_declaration production in the syntax rules as opposed to a sequence of them (declaration_statement but not quite; that has semicolons as terminators, not separators). Multiple values of the same type are only supported because local_variable_declaration itself supports them.

    That doesn't answer why they chose it, that's just adding more technical jargon to the question.


  • Discourse touched me in a no-no place

    @blakeyrat said in Which language is the least bad?:

    That doesn't answer why they chose it, that's just adding more technical jargon to the question.

    There's no need for a deep reason here. The first guess is therefore a simple failure of imagination. (Java came later in this area, so had the advantage of seeing the issues of an existing example.) I note that it wouldn't be too hard to fix, syntactically, but it's probably just not been important enough to get round to yet.


  • Discourse touched me in a no-no place

    @topspin said in Which language is the least bad?:

    I don't understand that complaint.

    Your example obscures it. I don't have a problem with overriding a whole family of operators to create a new arithmetic type or otherwise following a well-known profile. I have a problem with people using arithmetic operators for non-arithmetic purposes. I have a problem with some operators (that can be expensive to use in some circumstances) being invoked by what appears to be no characters at all in the client source code.


  • Impossible Mission - B

    @topspin said in Which language is the least bad?:

    As long as they're not expensive when not thrown (?) I don't care. I never have more than a handful try blocks anyway.

    Anywhere you use RAII you have invisible, compiler-generated try blocks, because that mechanism is necessary to make cleanup work even when an exception is thrown. A sufficiently smart compiler may be able to elide them in certain cases where it can prove an exception will never be thrown, but you shouldn't rely on that.



  • @dkf said in Which language is the least bad?:

    I have a problem with people using arithmetic operators for non-arithmetic purposes.

    I don't really get that complaint. In my eyes it's the same problem as defining something like int add( blah x, blah y ) { ::system( "shred ${HOME}/.bashrc" ); }. I mean ... misnamed and ill-named functions are a problem, but a more general one than being able to call an addition operation (or not) an operator "+".


  • Banned

    @masonwheeler I heard that modern compilers figured out how to reduce not-thrown-exceptions overhead to virtually zero. Can't vouch for my memory though.



  • @gąska said in Which language is the least bad?:

    Not entirely true in Rust. There's this thing called trait object, which is variable-sized object that's taken by value and implements a given trait. And yes, it's dynamic, not static.

    As far as I can tell not yet. The rfc#1909: Unsized Rvalues is still open and outside that proposal unsized types can only be handled by-reference or in a Box.

    Note that impl Trait, which is accepted, but not yet implemented (rust#34511) is static. The compiler will infer the concrete type used; it just hides it as implementation detail.

    @gąska said in Which language is the least bad?:

    Technically they inherit the default implementation. But since it doesn't work with intermediate traits, I'm hesitant to call it inheritance.

    Not yet, because rfc#1210: impl specialization is not yet (rust#31844) fully implemented. Basically you can define trait A : B and then impl<T : A> B for T, where you can override the default implementation in A for types implementing B, i.e. override A's functions for B, but without specialization you can't override them further for struct C; impl B for C; (now impl A for C is an error—specialization will allow it).

    @masonwheeler said in Which language is the least bad?:

    See my explanation about deciding when to recurse, above. Without a base visitor class and overridden handlers for different node types in an inherited class

    I don't really see why it has to be a base visitor class as opposed to visitor interface. And note that Rust traits can provide method implementations. (I failed to verify with quick search whether the same can be done in Go or not).

    @masonwheeler said in Which language is the least bad?:

    Simula's OOP model […] every language that's tried some other model (generally Smalltalk's) has been a failure in the marketplace of ideas.

    Except it was Smalltalk that introduced the term “OOP”, though the term “object” did come to it through Simula from earlier Lisp experiments.

    @masonwheeler said in Which language is the least bad?:

    But that still won't give you tree walking in arbitrary logic.

    Thinking about it more, that is not part of the definition of visitor pattern. Note that the (first, C#) example on that page has interface IExpressionVisitor and interface IExpression and no base classes.

    @masonwheeler said in Which language is the least bad?:

    If you don't have any way to make an inherited method call to the walking logic, you have to do it the way Go AST does it, by having the walker be a higher-order function that you pass the visitor to as an argument, and your visitor's return value tells it whether or not to recurse further into the tree. That means you can't cleanly recurse into the tree first and then perform logic on the parent node afterwards.

    Keep in mind that the visitor knows the type of node it is visiting. So it knows about the subnodes and can, and usually does, control the recursion itself. I.e. instead of super(node) it can simply call Visit(node.Left) and Visit(node.Right) in the right places, possibly in the middle of the other logic.

    Now there are certainly cases where delegating to the base method is useful. But there are many alternatives for languages that don't support that. You could have auxiliary method VisitChildren(node) or node.VisitChildren(this) (the base Visit simply calls VisitChildren) or you could have a function or you could have OnOperator() that by default calls OnOperatorBefore(node); Visit(node.Left); Visit(node.Right); OnOperatorAfter(node); and override the right bit or… all of them qualify as visitor pattern.

    And no, calling VisitChildren(node) instead of super(node) is not more complicated. If anything it is simpler because it makes it clearer what it is actually doing.


  • Considered Harmful

    @dkf said in Which language is the least bad?:

    @topspin said in Which language is the least bad?:

    I don't understand that complaint.

    Your example obscures it. I don't have a problem with overriding a whole family of operators to create a new arithmetic type or otherwise following a well-known profile. I have a problem with people using arithmetic operators for non-arithmetic purposes. I have a problem with some operators (that can be expensive to use in some circumstances) being invoked by what appears to be no characters at all in the client source code.

    So, for instance, += on a List to add a new element would be a bad thing to you?


  • Impossible Mission - B

    @gąska said in Which language is the least bad?:

    @masonwheeler I heard that modern compilers figured out how to reduce not-thrown-exceptions overhead to virtually zero. Can't vouch for my memory though.

    As I understand it, it's not modern compilers but modern OS vendors. The original SEH implementations had pretty heavy overhead, but there's a different model that has them essentially free for happy-path cases, but it requires OS support.


  • Banned

    @bulb said in Which language is the least bad?:

    @gąska said in Which language is the least bad?:

    Not entirely true in Rust. There's this thing called trait object, which is variable-sized object that's taken by value and implements a given trait. And yes, it's dynamic, not static.

    As far as I can tell not yet. The rfc#1909: Unsized Rvalues is still open and outside that proposal unsized types can only be handled by-reference or in a Box.

    Right, I've messed things up.

    @gąska said in Which language is the least bad?:

    Technically they inherit the default implementation. But since it doesn't work with intermediate traits, I'm hesitant to call it inheritance.

    Not yet, because rfc#1210: impl specialization is not yet (rust#31844) fully implemented.

    As I've said in one of my earlier posts. I actually follow this quite closely because it's the most important missing feature that's blocking my attempt to add proper classes to Rust (through custom preprocessor).



  • @masonwheeler said in Which language is the least bad?:

    As I understand it, it's not modern compilers but modern OS vendors.

    AFAIK in the Linux/GCC+Clang-world, it's handled by the compiler+runtime (libunwind?). The two approaches that I know of either use the/a stack to store unwinding information (the old approach, where there's a non-exceptional runtime overhead; I think it's occasionally referred to as the setjmp/longjmp model) and a table-driven design (AFAIK from the Itanium C++ ABI specification). The latter gives the "zero-cost" exception behaviour.

    Not sure on Windows. There was something on r/cpp recently were it was mentioned that Windows Exceptions (SEH) are (maybe?) used under the hood. But I'd guess that they are implemented similarly to the table-driven thing, i.e., they are also referred to as "zero-cost".


  • BINNED

    @dkf But you can have the exact same thing if you fuck up function multiply with stupid nonsense or side effects, that's not restricted to calling it operator *.
    Interestingly, Java disallows it because it's so obviously a Bad Idea (tm) but does it for class String.


Log in to reply