We see this in C#. Has a ridiculous amount of features outside default OOP and continues to get more baseline, even has great interop with several other languages. But culture changes very, very slowly. Large scale adoption tends to happen only when some major framework insists on using a certain paradigm.
C# has been trending towards introducing more things from the functional paradigm in particular, but just getting away from typical OOP patterns and replacing them with something more on the spot or functional isn't amazingly well received. The main thing which seems very well adopted is LINQ, and that's primarily due to Entity Framework and Microsoft pushing LINQ-adoption hard. Most places still program in C# as if it were Java with Lombok built-in.
And that's where I'm getting at mostly. New paradigms and languages are cool, but historically it's been difficult for them to get mass adoption unless a big name is pushing for it hard. Convergence has been the name of the game for a while now, rather than divergence.
Works fine on non-Windows systems (well, Linux and macOS at least) IME and isn't several years behind.
Also I think you are giving nullable ref types too much credit
Having the type system be able to tell you that you may have a null reference exception in a complicated code base is magic. I've inherited bad code bases and refactored them to use nullable references and watched lots of bugs fall out.
That said, I get where you are coming from - .net framework is pretty much just windows, and there a large number of libraries/apps that are Windows first, other platforms second.
If you do end up needing to do Windows dev, though, I'd recommend it, same as if I were doing Apple Dev I'd recommend Objective C/Swift, or recommend you brush up on bash/zsh/fish, C and maybe Xorg/Systemd for linux.
You can use other languages on all these platforms but the 'native' toolkit tends to enjoy better interop and have better support.
Edit: The other reason Unity developers are careful around new language features is avoiding memory allocations. When you write C# like it was Java it's more straightforward to see where allocations happen but it's all too easy to accidentally write allocations in your main loop if you use everything the language has to offer.
That aside. Your point around ecosystems is interesting to me. As a wannabee coder I'm always trying new things. Most of my code was in PowerShell although I did do a bit of C++ and Delphi in school.
I have been dabbling with dotnet core (C#) and Python the last couple of years.
I tried to get into Java a bit, but to be frank the learning curve to go from start to consuming a REST service was just way too hard. I couldn't understand the difference between when I should be using JavaEE, JavaSE, spring boot, spring framework etc. I gave up after two days.
In contrast Python is just so easy. And if I want something more robust I'll rather go to dotnet.
Go is on my list as it seems to offer a lot of what I want, and although there are some apparently weird things in it I think i can be productive with it for some basic business logic within a couple of days.
Google library you need "java http client"
https://openjdk.org/groups/net/httpclient/intro.html - it doesn't look complicated.
First example is using "reactive-streams", you don't have to use that syntax if you aren't familiar (same as in Python there are new features, which beginners may learn later), other examples strait forward.
If library you need doesn't exist in standard library, download jar, add it your `classpath` and use.
Skip any tutorials which are using Spring (unless you want to learn Spring). If you want to write more advanced Java code, learn about [Maven](https://maven.apache.org/)
Same thing about JavaEE/JavaSE, if you google about it, you will find that you may use any, there are different libraries included in installation, it doesn't mean you can't add them later.
In Python a library like requests is a thin abstraction over http calls it is very easy to understand what is going on and get started.
In Java, there is a tendency to wrap everything so it fits with a framework and is super generic and configurable. For example look at the Feign library.
Each approach has its pro's and cons, but knowing both of them good as it allows you to pick the right solution for different use cases.
About getting into java: i'd recommend using Spring Boot. I provides a low abstraction http client (RestTemplate) that allows for straigforward calls to REST services.
Which functional language are you talking about? Of the two functional languages I know, Elm can only have a single return statement. Scala encourages a single return.
To make an example the following expression can be considered to have two returns:
max x y =
if x > y
then x
else y
The Java equivalent is int max(int x,int y) {
if (x>y)
return x;
else
return y;
}
Some people abhor the idea of having multiple returns in a method, and say that you should write it like int max(int x, int y) {
int result;
if (x>y)
result = x;
else
result = y;
return result;
}
(Disclaimer: Contrived and buggy example as I am on mobile) int max(int x,int y) {
if (x>y)
return x;
else
return y;
}
I just looks completely fine to me, but colleagues would complain about it in reviews and I just don't understant the problem at all.
The variant with the extra result variable looks just wierd to me, and I have seen much uglier code written by people desperately avoiding early/multiple returns.In imperative programming, a bunch of nested conditionals can easily have incomplete coverage of possible program states, and it can be easy to overlook problems if you have a mixture of side-effect branches and early returns. I think some people struggle with this more than others, and it can flummox them almost like goto-laden spaghetti code.
Of course, there are other areas where similar errors can occur in different languages, i.e. in exception-handling or pattern-matching constructs. There are many different coding styles which can make these control-flow structures easier or harder to debug. But, I think there can also be a lot of "cargo culting" where zombie rules of thumb continue beyond when they were really particularly helpful.
The fact everything is context-dependent is the basis of all these discussions to begin with. We severely lack evidence and most rules are hearsay taken as gospel.
I initially fell in love with the Rust convention of simply bubbling up errors throughout a function but I've gradually realized that leads to a poor API. [1]
I'm currently learning c++ for a personal project. I want to use a library with an API that's sufficiently templated it'll be easier to write a c++ shim than directly generating bindings. So far I'm at the stage where I'm constantly thinking Rust does things better. Hopefully later on I'll realize why the different tradeoffs c++ makes make sense. (One example would be how generic code is typechecked).
[1]: If you want to be able to simply propagate up errors with the `?` operator you need to implement automatic conversations from your dependencies' errors to your error type. But that means your API will have a single error type for every dependency error type - say MyError::Io(io::Error) - which is the wrong level of specificity. I now think you should either expose only a string message your user can just show their end user or an error that's much more specific so they can write code that intelligently responds to it. Something like MyError::ReadConfigFailed(io::Error)
[1]: https://doc.rust-lang.org/rust-by-example/error/multiple_err...
> I now think you should either expose only a string message your user can just show their end user or an error that's much more specific so they can write code that intelligently responds to it.
a boxed trait object is the main way to implement an error that merely conveys a string message.
Edit: Realized it might not have been clear that "boxed error" and "all you get are a string message" refer to the same thing. While you can try and convert a trait object to a specific type at runtime it's a crude and ugly approach that suggests that trait object should have been something else to start with. The main thing you do with a Box<dyn Error> is ask it for a Display message.
Some things that are taken as best practices are bad practices in other langauges and vice versa.
For example, lots of Java devs are against early and multiple returns to the point of absurdity, while in functional languages this is idiomatic and no problem at all.
Using different languages lets you see how different approaches work out in practice, so you can get a better view about which approaches work and which approaches are just cargo-cult nonsense.