switching from OOP to procedural

AlexKindel
I'll have to look into metaprogramming. Maybe it would be more powerful than C#'s generic capabilities

Generics are not close to "meta-programming". But C# has very powerful "meta-programming" capabilities called T4 Text Templates: https://docs.microsoft.com/en-us/...-generation-and-t4-text-templates You can generate whatever code you want during compilation time. This allows you to inspect members of existing data structures, generate new types, code, etc...
What helped me the most in switching far away from Java/OOP was to go to the "polar opposite": Haskell/PureFP.

You lose the tools you're familiar with, and instead of replacing them with similar looking tools that you should use in a different way (eg: C/C++/Golang/etc), which lets you fall back on your old ways, you're given a totally new set of tools and you have no idea how they work at all. It lets you unlearn your assumptions of how to model a problem and build up a solution. I've since moved from Haskell to Rust, but I still think that Haskell is the best way to break your mind out of OOP-mode.

Edited by Lokathor on
AlexKindel
In the current project, I think it would be necessary to error check at every layer: the only kind of error that can happen beyond user syntax errors that can be caught immediately is divide by zero errors (also introduced by the user input), which often can't be detected without a lot of processing, and the moment of detection might be deeply buried, like with 1/(1+1/(2^(1/2)+3^(1/2)-(5+2*6^(1/2))^(1/2))^(1/2), where 2^(1/2)+3^(1/2)-(5+2*6^(1/2))^(1/2) happens to equal zero.


You could maintain a rational number in each node, which is the result of the operation, that you can compute while simplifying. Thus you can detect subexpressions that evaluate exactly to 0. (You can also just maintain a float and approximate the result of the operation, so you can handle irrationals, but some expressions that don't evaluate to zero might produce a zero floating point value and vice versa). Now you only have to error-check when simplifying / evaluating a division node.

Error handling itself can be viewed as a simplification rule : if an operation has one child that contains an error, replace the whole subtree with that error. You could also tag nodes with a character index indicating where the node is located in the source, so that when an error occurs, the whole tree is simplified to an error node indicating the position of the error.
The way you designed your Nodes/Terms seems perfectly natural to me and is basically how you would model the thing in C anyway.

Each node in the expression would have a tag to specify its type and then it would have some values that are particular for its type.

> I've ended up with towering class hierarchies

Looking at what you showed us so far, I don't see any "towering class hierarchies". I just see sensible representation of data.

The one small "weirdness" is you have "Number" as a root but it's not doing anything. You don't need it. You already have a way to represent numbers with the `Integer` type.

What I would do is use Term as the basic representation every where, so for example, the fraction type would be:

1
2
3
4
5
class Fraction : Term
{
    Term Numerator;
    Term Denominator;
}



You can also have a much simpler representation: only two types of nodes:

1
2
3
4
5
6
class BinaryTerm : Term
{
    Term left;
    Term right;
    Operation op;
}


Where `Operation` could be an enum.

The other class would be a leaf for representing the actual integers:

1
2
3
4
class Number: Term
{
    int value;
}

For what it's worth, as I've forged on, the program has ended up looking like this. I feel rather bad that I didn't really take anyone's advice about doing things differently than I already was. It still bothers me that I couldn't find a way to factor out much of the code shared by the polynomial classes, like IntegerPolynomial.GetGCD and NestedPolynomial.GetGCD, but on the other hand, if it's possible but only by adding a significant amount of complexity, I'm not sure it would be worth doing.
Lokathor
I've since moved from Haskell to Rust, but I still think that Haskell is the best way to break your mind out of OOP-mode.


Oh hey, I've been thinking about doing a project in Rust. Have you ever written a Windows program with GUI in Rust? I'm used to how writing Windows code in C++ works - including the Windows headers and using MSDN to look up how the C/C++ API calls work as necessary - but was wondering what the process is like for a language not specifically mentioned by MSDN. Would it be like...writing a platform layer in C++, and providing C functions through which the Rust code can communicate with it?
AlexKindel

... but was wondering what the process is like for a language not specifically mentioned by MSDN. Would it be like...writing a platform layer in C++, and providing C functions through which the Rust code can communicate with it?


I have used rust to write applications that make use of the windows api, so I though I'd chime in here. There are really good rust bindings, which can be found here https://crates.io/crates/winapi, which allow you to directly call the functions listed on MSDN from rust. This means you can get away with writing everything in rust. The one downside of this is that it at times gets pretty gnarly when interacting with the more c++-heavy parts of the windows api (i.e. COM stuff). Many of those hard cases are however also well documented with examples of how you would write reasonable code.
seventh-chord
I have used rust to write applications that make use of the windows api, so I though I'd chime in here. There are really good rust bindings, which can be found here https://crates.io/crates/winapi, which allow you to directly call the functions listed on MSDN from rust. This means you can get away with writing everything in rust. The one downside of this is that it at times gets pretty gnarly when interacting with the more c++-heavy parts of the windows api (i.e. COM stuff). Many of those hard cases are however also well documented with examples of how you would write reasonable code.


Thank you! One other thing along these lines: my understanding is that in C, what determines whether it's a console program versus a GUI program is what format one uses for the entry point -
1
int main()

versus
1
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow)

How does one make the distinction in Rust?
AlexKindel
...How does one make the distinction in Rust?


There is a attribute you can set to choose between the window and the console subsystem. It is mentioned in the release notes for 1.8 and in the documentation for attributes. In short, you put either one of the two following lines at the start of your main file, depending on whether you want a console or a window app.

1
2
#![windows_subsystem = "console"]
#![windows_subsystem = "windows"]


The main function still remains the same as always in rust, and as far as I can remember you access the parameters you would have gotten passed in C++ through some function calls.