Hacker News new | ask | show | jobs
by synthc 1339 days ago
Different languages also have their own culture and ecosystem, which are also valuable to learn in order to get a different perspective.

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.

5 comments

I think this is my first post on HN, but have been a long time lurker.

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.

I am using Java occasionally, there isn't any special learning curve if you know basic syntax. Same as in Python you don't start with Jdango and virtual environments, you shouldn't start using Java with Spring Boot/Spring.

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.

You sound like you'd enjoy TypeScript with NodeJS. Low learning curve. Highly productive. Massively versatile ecosystem. You can be functional or OO. And of course, the type system, which will feel similar to C#. It's very fast and obviously JS being the language of the web is a natural benefit too.
For starting with TypeScript today, I’d almost certainly recommend trying Deno before Node. It’s much closer to standard web APIs, the tooling story is “you don’t need any until you know you do”. And it doesn’t have a zillion footguns like CJS/ESM interop, or different stream APIs, or complex package.json configs.
This is the way. Being able to make a website, PWA, cross-platform desktop app with Tauri, mobile app with RN / Quasar / Svelte Native / Cordova, or a standalone cross-platform executable with Deno is invaluable. Edge functions / cloudflare workers / Deno deploy, or even embedded js runtimes are all first class. As a typescript dev I feel like I can do almost anything, and UI is cake with Svelte and the other great web ui frameworks around JS. Not to mention the incredible ecosystem and the top-tier DX with PNPM and Vite. I feel spoiled when the possibilities are limitless and the tooling / ecosystem has it all!
Yes this is an excellent example of different cultures and ecosystems.

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 straightforward calls to REST services.

The problem being, cargocults tend to hold the power. So yes, learn new concepts, practice them and more, but don't be surprised if in practice you can barely utilize them thanks to cargocults insisting they are wrong.

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.

The reason why I’m not adopting C# is because I’m never going to be a windows user.
And this is the problem OP is talking about. Outdated thinking from a 15 years ago is how cargo cults hold onto all the power.
.net core is not Windows only - works on the big three (linux mac windows).

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.

The other big C# ecosystem is Unity Engine with its own frameworks and plugins. It lags a bit behind Windows, and might be one reason why some C# developers seem to be behind the times when it comes to new language features.

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.

I've been coding .NET on linux fulltime for multiple years now.
This comment is about 7 years out of date.
It works on Linux just fine
Problem is the mono C# compiler is several years behind Roslyn in terms of features. It's pretty much a non-starter if you don't have nullable types in 2022.
https://dotnet.microsoft.com/en-us/download

Works fine on non-Windows systems (well, Linux and macOS at least) IME and isn't several years behind.

Dont use Mono

Also I think you are giving nullable ref types too much credit

I don't think you're giving them enough 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.

Which features are you talking about?
Take about anything you like of minimum level C# 6. My personal gripe being poor adoption of tuples/anonymous types and records.

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.

"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."

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.

In a functional language technically you don’t have multiple returns, because the function is a single expression so in a way you’re right. On the other hand the actual result of the expression is determined on a leaf of the expression so you could consider it a return point

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)
Yes this is exactly what I meant. After years of coding in Haskell and Clojure, and then going back to Java I have absolutely no problems with

    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.
What do these languages do when you accidentally miss a branch in you decision tree? Is there any lexical or static analysis error, or does it cause a runtime error or implicit null return when you hit the actual missed condition? I think these differences in potential outcomes are what guide many of these cultural rules of thumb in different programming styles.

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.

And this is likely one of those zombie rules. It's a trivial case and anyone with a semblance of programming knowledge understands what happens. You don't even need the else statement in GP's case. Meanwhile, any non-trivial case is so context-dependent you can't blindly apply rules and claim to have the best result or even just a better result from what people would come up with naturally.

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.

Cargo-culting from C where you have explicit malloc() and free(). Best practice was to have only 1 return to ensure free() was always called on everything just before the return, so you wouldn't have a memory leak if one was missing from an early return.
I was wondering where this 1 return practice originated from, but yeah, it makes sense in that context.
Rust convinced me that having a section at the top of a function that early returns in error/exceptional situations works very well. I find myself missing this in the one functional language I've tried, Elixir, although I find Elixir worth it for other reasons.

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)

Regarding citation #1: I'm a Rust learner myself, so this is something that's rather salient for me. What do you think about the "boxing errors[1]" approach? This strikes me as one possible way you could bubble up dependency errors without being forced to deal with type conversions.

[1]: https://doc.rust-lang.org/rust-by-example/error/multiple_err...

When I said

> 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.

I see, thanks for the response! I guess you're looking for something more deterministic so you can more effectively pattern match against the error's type?
I think both are very good options. In a lot of cases it's not useful to deterministically match against the error type. The classic example of this is an application that's just going to pop up an error box, but this applies whenever your consumer can't fix the error even if you tell them specifics. In that case it's a significant waste of time to go beyond the boxed trait style (but anyhow or eyre add nice frills on top of a boxed trait).

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.

I find the java example different to my experience. java doesn't have all expressions return values, like if, making the attempt to have single returns from functions much harder to do tidily than in other languages like for example ruby, rust, scala.
You can program just about any darned way in Java which can lead to some pretty schizophrenic code bases and organizations. I recall a fairly recent code review where the author had a static method in a class which took in another type, did some reasoning on the state of the type and returned a result. I asked why that method was not in the type itself since all the state required to answer the question lived in the instance of that object. The author said, oh we tend not to use “smart” objects.