For fear of disagree downvotes: I would say that many of the qualms brought up in this article are problems that are encountered fighting the language.
The problem of 'summing any kind of list' is not a problem that is solved in Go via the proposed kind of parametric polymorphism. Instead, one might define a type, `type Adder Interface{Add(Adder)Adder}`, and then a function to add anything you want is fairly trivial, `func Sum(a ...Adder) Adder`, put anything you want in it, then assert the type of what comes out.
When it comes to iteration, there is the generator pattern, in which a channel is returned, and then the thread 'drinks' the channel until it is dry, for example `func (m myType) Walk() chan->myType` can be iterated over via `range v := mt.Walk(){ [...] }`. Non-channel based patterns also exist, tokenisers usually have a Next() which can be used to step into the next token, etc.
The writer seems to believe that functions on nil pointers crash the program, this is not the case. It's a common pattern in lazy construction to check if the receiving pointer is nil before continuing.
Go is not flawless by any means, but it warrants a specific style of simplistic but powerful programming that I personally enjoy.
>The writer seems to believe that functions on nil pointers crash the program, this is not the case. It's a common pattern in lazy construction to check if the receiving pointer is nil before continuing.
And what happens when you don't check? It crashes. That's the unsafe part.
These crashes are simply not possible in Rust and Haskell, and the type system notifies you if failure is possible (because the function will return an Option/Maybe).
Woa, Rust is great, but I think you're going a little far on the hyperbole there.
You can easily generate a segfault in Rust in 'unsafe' (or 'trusted') code; that might only restrict errors of that nature to code that uses unsafe blocks.
Practically speaking that's pretty common; once you enter an FFI unsafe block, you lose all type safety; but you can totally do it without FFI too. Eg. using transmute().
In fact, there's no way to know if you code contains 'hidden' unsafe blocks wrapped in a safe api in some 3rd party library that might cause a mysterious segfault later on.
You can argue that 'if you break the type system you can do anything, obviously'; that's totally true.
I'm just pointing out the statement: "These crashes are simply not possible in Rust and Haskell" <-- Is categorically false.
You can chop your own arms off in Rust just like anything else (including Go).
> You can easily generate a segfault in Rust in 'unsafe' (or 'trusted') code; that might only restrict errors of that nature to code that uses unsafe blocks, but practically speaking that's pretty common; once you enter an FFI unsafe block, you lose all type safety; but you can totally do it without FFI too. Eg. using transmute().
Not directly addressing what you're saying, but, IME people are far too quick to use `unsafe` code. One needs to be quite careful about it as there's a pile of invariants that need to be upheld: http://doc.rust-lang.org/master/rust.html#behavior-considere...
> once you enter an FFI unsafe block, you lose all type safety
You don't lose all type safety, especially not if the FFI bindings you're using are written idiomatically (using the mut/const raw pointers correctly; wrapper structs for each C type, rather than just using *c_void, etc).
Maybe "unsafe" is being used to mean different things, here. Some may interpret it as to refer to unsafe memory access. Others may use it to mean possibility of crashing at run time (due to null pointer dereference).
There is some subtlety about dereferencing a null pointer. Many languages (C, C++, Rust) state that *NULL is undefined behaviour, that is, the compiler can assume that it never happens and optimises based on this. This can lead to a "misoptimised" program that doesn't actually segfault when the source suggests it should.
The compilers don't map pages there, the operating system does. The problem is the compiler will optimise assuming that a null deref never happens, so you can have source that looks like it should crash due to a null deref, but the compiler has "misoptimised" it to have very different behaviour.
Specifically, the word safe, when referring to type systems means "memory safe". Meaning the compiler or runtime either prevents bad memory accesses by construction or ensures dynamic checks are in place that throw an exception or halt execution in case a bad memory access was about to occur. It means the program isn't accessing uninitialized memory and isn't vulnerable to buffer overflows etc. It doesn't mean your program won't ever crash.
I agree it's better to avoid null dereference errors by not putting null in your language, but by the normal meaning of safe here, go is safe.
> It's a common pattern in lazy construction to check if the receiving pointer is nil before continuing.
I disagree: if the construction can fail, the constructor must return an error, which will be checked; only if the error is nil can the process continue. There shouldn't be logic on the actual data returned to assert whether a constructor worked or not.
>For fear of disagree downvotes: I would say that many of the qualms brought up in this article are problems that are encountered fighting the language.
If that's so, it's because it's a language that also fights lots of things a modern programmers wants to do/have.
>When it comes to iteration, there is the generator pattern, in which a channel is returned, and then the thread 'drinks' the channel until it is dry, for example `func (m myType) Walk() chan->myType` can be iterated over via `range v := mt.Walk(){ [...] }`. Non-channel based patterns also exist, tokenisers usually have a Next() which can be used to step into the next token, etc.
Actually using channels as a general iterator just for the sake of using the range operator is considered as an anti-pattern. The reason is not performance (although it has a cost), but the risk of leaking producer goroutines. Your example:
for v := range mt.Walk() {
if blah {
break
}
}
How will the goroutine writing into the channel returned by mt.Walk know when there are no more consumers which will possibly read from it?
One way out is:
done := make(chan struct{})
for v := range mt.Walk(done) {
if blah {
break
}
}
close(done) // or defer close(done)
Picking the right cleanup is error-prone.
What about errors? How will mt.Walk tell you that it had to interrupt the iteration because an error happened? Either your channel has a struct field containing your error and your actual value (unfortunately Go lacks tuples or multivalue channels).
Furthermore uncaught panics in the producer goroutine will generate a deadlock, which will be caught by the runtime, but it will halt your process. One way to do it is:
errChan := make(chan error)
for v := range mt.Walk(errChan) {
if blah {
break
}
}
err := <-errChan
The producer will use the select statement to write both to errChan and your result channel. The success of writing to errChan is a signal for the producer that the consumer exited.
However same thing here about relying on the last statement being executed to avoid a leak in case of returns or panics. Here the defer is less nice since you're supposed to do something with the error:
func Example() (err error) {
errChan := make(chan error)
for v := range mt.Walk(errChan) {
if blah {
break
}
}
defer func() {
err = <-errChan
}()
}
Next-style methods just pass through the panics, and allow you to handle errors either by having a func Next() (error, value) or with this pattern which moves the pesky error handling outside:
i := NewIterator()
for i.Next() {
item := i.Item()
...
}
err := i.Error()
First, any panic that happens inside either your code or the generator will bubble through.
Second, if you return from your loop body, you will have to provide your own error (the compiler will remind you about your function signature, if in doubt). You can return early if the iterator can be stopped and GCed out (i.e. it doesn't handle goroutines or external resources), otherwise you'd have to call a cleanup as with channels.
The rule of thumb with Go should be that you don't have to do things just because they use some syntactic sugar. After a while you start to think about beauty in terms of properties not about calligraphy.
However, I do see this as a weak point of the language, which hopefully can be solved by education; after all Go is so simple to learn that you might be tempted to make it look even simpler. But the fact that the language has (almost) no magic, it means that you can actually understand what some code does, which imho outweighs the occasional syntactical heaviness or having to learn a few patterns.
I'm not suggesting these are the best patterns, or even 'good' patterns. I am simply drawing a parallel between them and pythonic iterators. When it comes to errors from generator patterns, I've gone between your suggestion and chan<-struct{T;error} when channel based generator patterns are appropriate.
The problem of 'summing any kind of list' is not a problem that is solved in Go via the proposed kind of parametric polymorphism. Instead, one might define a type, `type Adder Interface{Add(Adder)Adder}`, and then a function to add anything you want is fairly trivial, `func Sum(a ...Adder) Adder`, put anything you want in it, then assert the type of what comes out.
When it comes to iteration, there is the generator pattern, in which a channel is returned, and then the thread 'drinks' the channel until it is dry, for example `func (m myType) Walk() chan->myType` can be iterated over via `range v := mt.Walk(){ [...] }`. Non-channel based patterns also exist, tokenisers usually have a Next() which can be used to step into the next token, etc.
The Nil pointer is not unsafe as far as I know, from the FAQ: http://golang.org/doc/faq#no_pointer_arithmetic
The writer seems to believe that functions on nil pointers crash the program, this is not the case. It's a common pattern in lazy construction to check if the receiving pointer is nil before continuing.
Go is not flawless by any means, but it warrants a specific style of simplistic but powerful programming that I personally enjoy.