One reason I like go is it doesn't have all the bells and whistles that give too many choices.
One reason I like go is it doesn't have all the bells and whistles that give too many choices.
struct Data {
idx: usize
// ...
}
struct Foo {
stuff: Vec<Vec<Data>>
}
struct Bar {
stuff: Vec<Data>
}
fn flatten_and_sort (foo:Foo, bar:Bar) -> Vec<Data> {
let mut output =
foo.stuff.into_iter()
.flat_map(|v| v.into_iter())
.chain(bar.stuff.into_iter())
.collect::<Vec<_>>();
output.sort_by(|lhs, rhs| lhs.idx.cmp(&rhs.idx));
output
}
Now you could argue that this is just "generics for collections" but the way those iterator combinators are written make heavy use of generics/traits that aren't immediately applicable for collections. Those same combinator techniques can be applied to a whole host of other abstractions that allow for that kind of user code, but it's only possible if the type system empowers library authors to do it. let mut output: Vec<_> =
foo.stuff.into_iter()
.flat_map(|v| v)
.chain(bar.stuff)
.collect(); let mut output: Vec<_> =
foo.stuff.into_iter()
.flatten()
.chain(bar.stuff)
.collect();
I might make it slightly more clear what the intent is by doing this: let mut output: Vec<_> =
Iterator::chain(foo.stuff.into_iter().flatten(), bar.stuff)
.collect();
But still basically the same.I definitely don't use generics if they can be avoided, and I think preemptive use of genericity "just in case" can lead to the situation you describe. If I'm not sure I'll really need generics I just start writing my code without them and refactor later on if I find that I actually need them.
But even if you only really care about generics for collections, that's still a massive use case. There's a wealth of custom and optimized collections implemented in third party crates in Rust. Generics make these third-party collections as easy and clean to use as first party ones (which are usually themselves implemented in pure Rust, with hardly any compiler magic). Being easily able to implement a generic Atomic type, a generic Mutex type etc... without compiler magic is pretty damn useful IMO.
class Result<T>
{
bool IsSuccess {get; set;}
string Message {get; set;}
T Data {get; set;}
}
On many occasions, I like using result types for defining a standard response for calls. It's typed and success / fail can be handled as a cross-cutting concern.It may frustrate coworkers who need to edit the code.
It adds another dependency into your workflow.
E.g.
libraryA.Result struct {
Err error
Data SomeDataType
}
libraryB.Result struct {
err string
Data SomeDataType
}
func (r libraryB.Result) Error() string {
return r.err
}
Now you have two different implementations of the same fundamental idea, but they each require different handling. In Go, where many things simply return an error type in addition to whatever value(s), you would now have three different approaches to error handling to deal with as opposed to just whatever the language specified as the best practice.Let your caller bring their own error type and instantiate your library code over that.
At $work we definitely have examples where we care about preserving errors, and if that tool were implemented in Go a solution like a Result struct containing an error instance and a data type instance could make sense.
I love multiple returns/errors. I find that I never mistakenly forget to handle an error when the program won't compile because I forgot about the second return value.
I don't use go at work though, I use a language with lots of throw'ing exceptions, and I regularly miss handling exceptions that are hidden in dependencies. This isn't the end of the world in our case, but I prefer to be more explicit.
golang is not a modern language (how old it is is irrelevent), and the people who designed it did not have a proper language design background (their other accomplishments are a different matter).
Having worked on larger golang code bases, and I've seen several times where errors are either ignored or overwritten accidentally. It's just bad language design.
> people who designed it did not have a proper language design background
Irrelevant.
> It's just bad language design.
try { ... } catch(Exception ex) { ... }
The error here is explicitly handled, and cannot be accidentally ignored. Unlike golang where it's quite easy for errors to go ignored accidentally.
Each language has its wonks.
It's not similar to that at all. Without it, the exception bubbles up until it gets caught somewhere, or crashes the program with a useful stacktrace.
With golang, it just goes undetected, and the code keeps running with corrupt state, without anyone knowing any better.
If you care with go, use errcheck.
class Query {
class QueryResult {
Exception error;
Value result;
}
public QueryResult query() {
try {
return doThing();
} catch(Exception e){
return new QueryResult(error, null);
}
}
}
Gorm is going out of its way to make error handling suck.It's about the default error handling method being sane. In exception based languages, an unhandled error bubbles up until it reaches a handler, or it crashes the program with a stacktrace.
Compare to what golang does, it's somewhat easy to accidentally ignore or overwrite errors. This leads to silent corruption of state, much worse than crashing the program outright.
Gorm isn't using the default error handling.
I've seen a bunch of code that does the equivalent of the Java I posted above. Mostly when sending errors across the network.
fmt.Println("foo")
Compare to a language with exception handling where an exception will get thrown and bubbles up the stack until it either hits a handler, or crashes the program with a stack trace.And I was referring to accidental ignoring. I've seen variations of the following several times now:
res, err := foo("foo")
if err != nil { ... }
if res != nil { ... }
res, err = foo("bar")
if res != nil { ... }>The following line in golang ignores the error:
fmt.Println("foo")
fmt.Println() is blacklisted for obvious reasons, but this: a := func() error {
return nil
}
a()
results in: go-lint: Error return value of 'a' is not checked (errcheck)
>And I was referring to accidental ignoring. I've seen variations of the following several times now: res, err := foo("foo")
if err != nil { ... }
if res != nil { ... }
res, err = foo("bar")
if res != nil { ... }
results in: go-lint: ineffectual assignment to 'err' (ineffassign)That's the issue with the language, there are so many special cases for convenience sake, not for correctness sake. It's obvious why it's excluded, but it doesn't make it correct. Do you want critical software written in such a language?
Furthermore, does that linter work with something like gorm (https://gorm.io/) and its way of handling errors? It's extremely easy to mis-handle errors with it. It's even a widely used library.
Perhaps more fundamental: it’s impossible to accidentally use an uninitialized “success” return value when the function actually failed, which is easy to do in C, C++, Go, etc.
func foo() (*SomeType, error) {
...
return someErr
}
...
result, err := foo()
if err != nil {
// handle err
}
// handle result
vs type Result struct {
Err error
Data SomeType
}
func (r *Result) HasError() bool {
return r.Err != nil
}
func bar() *Result {
...
return &Result { ... }
}
...
result := bar()
if result.HasError() {
// handle result.Err
}
// handle result
I'm not really sure I see the benefit to the latter. In a language with special operators and built-in types it may be easier (e.g. foo()?.bar()?.commit()), but without these language features I don't see how the Result<T> approach is better.The Result<T> approach prevents you from accessing Data if you haven't handled the error, and it does so with a compile-time error.
Even with Go's draconian unused variable rules, I and my colleagues have been burned more than once by forgotten error checks.
https://github.com/kisielk/errcheck
https://golangci-lint.run/usage/linters/ has a solid set of options.
That's the point! I want language features!
I don't want to wait 6 years for the designers to bake some new operator into the language. I want rich enough expression so that if '?.' is missing I just throw it in as a one-liner.
Generics is one such source of richness.
It's also incredibly unsafe and why generics aren't enough. C++, Java, and so on have had generics for ages and with types like the one above, null pointer exceptions are incredibly common. Nothing prevents the user from attempting to retrieve the data without first retrieving the success status.
On the other hand, this improves on it dramatically:
enum Result<T, E> {
Success(T),
Failure(E)
}uh, you'd never get a null-pointer exception in C++ given the type that OP mentioned. Value types in C++ cannot be null (and most things are value types by a large margin).
class Result<T> where T: struct
{
...
}
In that case it can't be null with C# either.A Result<T> that can only contain successful values doesn't seem very useful
int x = default; // x becomes zero
T x = default; // x becomes whatever the default value for struct isDon't think get;set is C++, though it breaks encapsulation.
A Result<T> that can only contain successful values doesn't seem very useful
See for example https://doc.rust-lang.org/std/result/enum.Result.html#method... in rust, https://docs.oracle.com/javase/8/docs/api/java/util/Optional... in Java, and https://en.cppreference.com/w/cpp/utility/optional/value_or in C++.
class Result<T>
{
bool IsSuccess {get; set;}
string Message {get; set;}
T Data {get; set;}
}[1] unfortunately not standardized yet https://github.com/TartanLlama/expected
class Result<T>
{
bool IsSuccess {get; set;}
string Message {get; set;}
T Data {get; set;}
}You: "C++, Java, and so on have had generics for ages and with types like the one above, null pointer exceptions are incredibly common."
jcelerier: "you'd never get a null-pointer exception in C++ given the type that OP mentioned."
You: "Then you can't construct it unless it's successful, no?"
Me: "The equivalent type in C++ [to what the OP mentioned] is std::expected". It is not possible to get a null-pointer exception with this type and yet you can construct it.
It sounds quite a lot like you're looking to be pointlessly argumentative, do I have that right?
1. The first, my original point was that a high quality type system enforces correctness by more than just having generics. There's no proper way in C++ to create this class and make a sum type - there's no pattern matching or type narrowing like can be done in languages with more interesting type systems and language facilities. Generics is just a first step to a much more interesting, safer way of writing code.
2. The second, my replies to folks who have corrected me, and I'll borrow your little paraphrase here:
> [Me]: "C++, Java, and so on have had generics for ages and with types like the one above, null pointer exceptions are incredibly common."
>
> jcelerier: "you'd never get a null-pointer exception in C++ given the type that OP mentioned."
>
> [Me]: "Then you can't construct it unless it's successful, no?"
I think this is exactly correct still. If it's impossible to create an instance of Result<T> without... well, a successful result, you may as well just typedef Result<T> to T, right? If it can't store the "failure" case, it's totally uninteresting.
If it _can_ store the failure case, making it safe in C++ is fairly challenging and I dare say it will be a little longer and a little less safe than a Result I can write in a few lines of TypeScript, Scala, Rust, an ML or Haskell derivative, and so on.
Now, I'd love to be proven wrong, I haven't written C++ for a while so the standard may have changed, but is there a way to write a proper enum and pattern match on the value?
It looks like this std::expected thing is neat, but can a _regular person_ write it in their own code and expect it to be safe? I can say with certainty that I can do that for the languages I listed above and in fewer than 10 lines of code.
The C++ answer to that is, well, this:
https://github.com/TartanLlama/expected/blob/master/include/...
I don't think it's a comparison.
the linked link has a ton of things that are "quality-of-life" things. For instance comparing two Result values efficiently (you don't want to compare two Result<T> bitwise, and you don't want the "is_valid" flag to be first in the structure layout to fallback on the automatic default of lexical order as that would sometimes waste a few bytes, but you want the "is_valid" flag to be the first thing being compared for instance. Do you know of a language that would do that automatically ?).
It also supports back to C++11 and GCC4.9 with various fixes for some specific compiler versions's bugs, supports being used with -fno-exceptions (so a separate language than ISO C++) - sure, today's languages can do better in terms of prettiness, but so would a pure-C++20 solution that only needs to work with a single implementation.
If you are ready to forfeit some amount of performance, for instance because you don't care that the value of your Result will be copied instead of moved when used in a temporary chain (e.g. `int x = operation_that_gets_a_result().or_else([] (auto&& error) { return whatever; });` 3/4 of the code can go away (and things will still likely be faster than most other languages).
They can just not exist. And C++ being C++, dereferencing an empty std::optional is UB. In practice this particular UB often leads to way worse consequences than more "conventional" null-pointer derefs.
Then chances are so are most if not all of the uses of generics OP criticises. The only "non-container" generics I can think of is session types where the generic parameter represents a statically checked state.
It doesn't hurt that static languages (TypeScript) or tools (mypy) that lightly lay on top of dynamic languages often do support sum types.
The good news is that many languages seem to be cozying up to them, and both the JVM (through Kotlin, Scala, et all) and .net (through F# or C# w/ language-ext) support them.
Even better news is that the C# team has stated that they want to implement Sum Types and ADTs into the language, and are actively looking into it.
I dislike dynamic languages where parameters and variables can take on any type -- it's rarely the case that same variable/parameter would ever need to contain a string, a number, or a Widget in the same block of code.
I find it much more freeing to have the compiler be in charge of exactness so I can make whatever changes I need knowing that entire classes of mistakes are now impossible.
And sum types aren’t for “radically different types”. You can define an error type to be one of different options (I.e. a more principled error code), or to represent nullability in the type system, or to indicate fallibility without relying on exceptions, etc.
Rust uses all of these to great effect, and does so because these sum types are generic.
It's a good software engineering principle to make control and data flow as streamlined as possible, for similar data. Minimize branches and special cases. Generics help with this, they hide the "irrelevant" differences, surfacing only the relevant.
On the other hand, if there are _actually_ different cases, that need to be handled differently, you want to branch and you want to express that there are multiple choices. Sum types make this a compiler-checked type system feature.
let v = match map.entry(key) {
Entry::Occupied(o) => {
o.get_mut() += 1;
o.into_mut()
}
Entry::Vacant(v) => {
update_vacant_count(v.key());
v.insert(0)
}
};
I view sum types as enabling the exact same exactness as you describe in your last line; especially since you can easily switch/match based on a specific subtype if you realize you need that, without adding another method to the base class and copying into the x subclasses that you have for implementing the different behavior.[0]: https://doc.rust-lang.org/std/collections/hash_map/enum.Entr...
Let's say you're opening a file that you think is a CSV. There can be several outcomes:
- the file doesn't exist
- the file can't be read
- the file can be read but isn't a valid CSV
- the file can be read and is valid, and you get some data
All of these are different types of results. You can get away with treating the first 3 as the same, but not the last. Without a tagged union, you'll probably resort to one of a few tricks:
- You'll have some sort of type with an error code, and a nullable data field. In reality, this is a tagged union, it's just that your compiler doesn't know about it and can't catch your errors.
- you'll return an error value and have some sort of "out" value with the data: this is basically the same as the previous example.
- you'll throw exceptions, which usually ends up with people writing code that forgets about the exception because the compiler doesn't care about it, and the code works 99% of the time until it completely blows up.
So, I mean, forget 'the next decade or two'; the majority of people are doing this right now; python and js are the probably the two most popular languages in use right now.
Will it end up in all statically typed languages? Dunno; I guess probably not in java or C# any time soon, but swift and kotlin support them already).
...ie. if your excuse for not wanting to learn it is that it's probably an edge case that most people don't have to care about now, and probably never will, you're mistaken I'm afraid.
It's a style of code that is very much currently in use.
Yes.
For example, some use cases: https://www.typescriptlang.org/v2/docs/handbook/unions-and-i...
This sort of code is very common.
I really don't know what more to say about this; if you don't want to use them, don't. ...but if your excuse for not using them is that other people don't, it's wrong.
They also tend to defer handling out into places where the context isn't always there.
The trend in language design does seem to be more broadly away from exceptions for this kind of thing and into generic pattern matching and status result types.
> Partially because people just get in the lazy habit of catch and rethrow RuntimeException, or catch and log, etc. when confronted with them.
After quite a while of thinking this way, I came to the conclusion that:
95% of the time, there's no way to 'handle' an error in a 'make it right' sense. Disk write failed? REST request failed? DNS lookup? There usually isn't an alternative to logging/rethrowing.
When there is a way to handle an error (usually by retrying?), it's top level anyway.
Furthermore, IO is the stuff that can just 'go wrong' regardless of how good the programmer is, and IO tends to sit at the bottom in most Java programs. This means every method call is prone to IOExceptions.
https://verraes.net/2014/12/erlang-let-it-crash/
If IOException on a read is truly happening, and it isn't just a case of a missing file, there are serious issues that aren't going to be fixed with a catch-and-log, or be able to be handled further up the call stack.
They're not popular in Java though, because the ergonomics is a lot worse than working with a Result type.
That it's mutable, at the very least!
Or we can learn to read them. Just treat types like a first class value. You either assign names to types like you do to values, or you can assign a name to a function that returns a type, this being generics.
That's an awful way to think about hard to read code. I could produce the most unreadable one liners you've ever seen in your life. We should condemn that and not blame it on others to "learn how to read".
Think about the audience when you code.
Most of the time I hear about "hard to read code" is "pattern I don't currently have a mental model for". We didn't move on from COBOL by letting that be a deterrant.
What about algorithms (sorts, folds, etc) on those containers? I write a lot of numerical code. It sucks to do multiple maintenance for functions that work on arrays of floats, doubles, complex floats, and complex doubles. Templates/Generics are a huge win for me here. Some functions work nicely on integer types too.
For example, what if PHP could somehow detect string-concatenation in SQL queries and instantly `die()` with a beginner-friendly error message explaining to use query parameterisation from the very beginning: tens of billions of dollars of PHP SQL injection vulnerabilities simply never would have happened - and people who were already writing database queries with string-concatenation in VB and Java who gave PHP a try would then be forced to learn about the benefits of parameterisation and they'd then take that improved practice back to their VB and Java projects - a significant net worldwide improvement in code-quality!
[1]: https://blog.codinghorror.com/falling-into-the-pit-of-succes...
I've been writing in TypeScript for about 5 years now - and I'm in-love with its algebraic type system and whenever I switch back to C#/.NET projects it's made me push the limits of what we can do with .NET's type system just so I can have (or at least emulate as closely as possible) the features of TypeScript's type system.
(As for generics - I've wondered "what if every method/function was "generic" insofar as any method's call-site could redefine that method's parameter types and return types? Of course then it comes down to the "structural vs. nominative typing" war... but I'd rather be fighting for a hybrid of the two rather than trying to work-around an poorly-expressive type system.
For enterprise-level programming (which is the environment Go grew up in), a language that's too flexible is a hindrance, because you can always pay for more eng-hours, but you can't necessarily afford smarter programmers.
Ex.
class ID<T> {
int id;
}
The idea is that an ID is just an int under the hood, but ID<User> and ID<Post> are different types so you can’t accidentally pass in a user id where a post is is expected.Now, this is just a simple example that probably won’t catch too many bugs, but you can do more useful things like have a phantom parameter to represent if the data is sanitized, and then make sure that only sanitized strings are displayed.
// UserID and PostID are distinct types
type UserID int
type PostID intI think the generic approach is a little bit better because of the flexibility, but this approach is still better than not having it at all.
It still provides a documentation benefit of course.
EDIT: Whoops, yes, as lentil points out, they are indeed distinct types not aliases. So it does provide the benefit of the Rust solution.
Your playground example didn't try to pass a UserID to a function accepting a PostID, but if you do that, you'll see the error:
For instance, if you have a function `f : Vec<a> -> SomeType`, the fact that `a` is a type variable and not a concrete type gives you a lot of information about `f` for free: namely that it will not use any properties of the type `a`, it cannot inspect values of that type at all. So essentially you already know, without even glancing at the implementation, that `f` can only inspect the structure of the vector, not its contents.
I agree with the sentiment that it is very easy to overuse genetics though there are scenarios where they are very useful.
- Promises
- Mutexes like Rust's Mutex<T> (would be much nicer than sync.Mutex)
- swap functions, like swap(pointer to T, pointer to T)
- combinators and other higher-order functions
In typescript I don't need generic too much / too complex, because the typing definition is more lax, and we can use dynamic in the very complex scenario.
I don't know which approach go is taking.
But I also have seen the problem with overuse of generics and other "advanced" type system features first hand (in libraries but also done by myself before I knew better).
s -> (s, a) is generic and a Functor (mappable - often conflated with a collection) but it's no collection!