6 ms·
Checking out the first example—object pools—I was initially blown away that this is not only possible but it produces no warnings of any kind: pool := sync
by stouset 2y ago
Checking out the first example—object pools—I was initially blown away that this is not only possible but it produces no warnings of any kind:
pool := sync.Pool{
New: func() any { return 42 }
}
a := pool.Get()
pool.Put("hello")
pool.Put(struct{}{})
b := pool.Get()
c := pool.Get()
d := pool.Get()
fmt.Println(a, b, c, d)
Of course, the answer is that this API existed before generics so it just takes and returns `any` (née `interface{}`). It just feels as though golang might be strongly typed in principle, but in practice there are APIs left and rigth that escape out of the type system and lose all of the actual benefits of having it in the first place.
Is a type system all that helpful if you have to keep turning it off any time you want to do something even slightly interesting?
Also I can't help but notice that there's no API to reset values to some initialized default. Shouldn't there be some sort of (perhaps optional) `Clear` callback that resets values back to a sane default, rather than forcing every caller to remember to do so themselves?
- tgv 2y agoYou never programmed in Go, I assume? Then you have to understand that the type of `pool.Get()` is `any`, the wildcard type in Go. It is a type, and if you want the underlying value, you have to get it out by asserting the correct type. This cannot be solved with generics. There's no way in Java, Rust or C++ to express this either, unless it is a pool for a single type, in which case Go generics indeed could handle that as well. But since Go is backwards compatible, this particular construct has to stay. > Also I can't help but notice that there's no API to reset values to some initialized default. That's what the New function does, isn't it? BTW, the code you posted isn't syntactically correct. It needs a comma on the second line.
- eptcyka 2y agoIs there a time in your career where an object pool absolutely had to contain an unbounded set of types? Any time when you would try know at compile time the total set of types a pool should contain?
- zaphodias 2y agoI assume they're referring to the fact that a Pool can hold different types instead of being a collection of items of only one homogeneous type.
- gwd 2y ago> That's what the New function does, isn't it? But that's only run when the pool needs to allocate more space. What GP seems to expect is that sync.Pool() would always return a zeroed structure, just as Golang allocation does. I think Golang's implementation does make sense, as sync.Pool() is clearly an optimization you use when performance is an issue; and in that case you almost certainly want to only initialize parts of the struct that are needed. But I can see why it would be surprising. > [any] is a type It's typed the way Python is typed, not the way Rust or C are typed; so loses the "if it compiles there's a good chance it's correct" property that people want from statically typed languages. I don't use sync.Pool, but it does seem like now that we have generics, having a typed pool would be better.
- 9rx 2y ago> so loses the "if it compiles there's a good chance it's correct" property that people want from statically typed languages. If that's what people actually wanted, Coq and friends would be household names, not the obscure oddities that they are. All the languages that people actually use on any kind of regular basis require you to write tests in order to gain that sense of correctness, which also ends up validating type-correctness as a natural byproduct. "A machine can help me refactor my code" is the property that most attracts people to the statically typed languages that are normally used. With "I can write posts about it on the internet" being the secondary property of interest.
- gwd 2y agoIt's a spectrum, with costs and benefits at each level. I lock my front door even though I don't have bars on my windows; I prefer Golang, where doing a basic compile will catch a fair number of errors and testing will catch the rest, to Python or Perl where testing is the only way to catch errors.
- 9rx 2y ago> where doing a basic compile will catch a fair number of errors In the case of refactoring this is incredibly useful. It doesn't say much about the correctness of your program, though.
- gf000 2y agoHow is it different than pre-generic Java? Map/List<T> etc are erased to basically an array of Objects (or a more specific supertype) at compile-time, but you can still use the non-generic version (with a warning) if you want and put any object into a map/list, and get it out as any other type, you having to cast it as the correct type.
- sapiogram 2y ago> You never programmed in Go, I assume? You might want to step off that extremely high horse for a second, buddy. It's extremely reasonable to expect a type-safe pool that only holds a single type, since that's the most common use case.
- pyrale 2y ago> There's no way in Java, Rust or C++ to express this either You make it look like it's a good thing to be able to express it. There's no way in Java, Rust or C++ to express this, praised be the language designers. As for expressing a pool value that may be multiple things without a horrible any type and an horrible cast, you could make an union type in Rust, or an interface in Java implemented by multiple concrete objects. Both ways would force the consumer to explicitly check the value without requiring unchecked duck typing.
- sophacles 2y agoRust has an Any type. It's rarely useful, but there are occasionally situations where a heterogeneous collection is the right thing to do. Casting the any type back to actual type is fairly nice though, as the operation returns an Option<T> and you're forced to deal with the case where your cast is wrong.
- tgv 2y ago> You make it look like it's a good thing to be able to express it. No, just that this pre-generics Go, and backwards compatibility is taken seriously.
- int_19h 2y ago> There's no way in Java, Rust or C++ to express this, praised be the language designers. That's not even the case. In Java, you'd just use Object, which is for all practical purposes equivalent to `interface{}` aka `any` in Go. And then you downcast. Indeed, code exactly like this was necessary in Java to work with collections before generics were added to the language. In C++, there's no common supertype, but there std::any, which can contain a value of any type and be downcast if you know what the actual type is.
- zaphodias 2y agoWhile I think you're right (generics might be useful there), it's fairly easy to wrap the `sync` primitives such as `sync.Pool` and `sync.Map` into your specific use case. Go is pretty strict about breaking changes, so they probably won't change the current implementations; maybe we'll see a v2 version, or maybe not. The more code you have, the more code you have to maintain, and given Go's backward-compatibility promises, that's a lot of work.
- Someone 2y ago> While I think you're right (generics might be useful there), it's fairly easy to wrap the `sync` primitives such as `sync.Pool` and `sync.Map` into your specific use case. That’s not a strong argument. You can easily (but sometimes tediously) wrap any API with one that (further) restricts what types you can use with it. Generics make it possible to avoid doing that work, and code you don’t write won’t have errors.
- zaphodias 2y agoDon't get me wrong, I agree! Especially performance-wise, I'd love to have the best primitives that let me build whatever I want and not some very generic primitives that perform a bit worse and I have to tune myself so I don't shoot myself in the foot.
- aktau 2y agoUpstream thinks a type-safer `sync.Pool` is a good idea too. It's being discussed in https://go.dev/issue/71076 https://go.dev/issue/71076.
- strangelove026 2y agoSync.map is meant to have poor performance I believe https://github.com/golang/go/issues/21031 https://github.com/golang/go/issues/21031
- PhilippGille 2y agoIt depends on the use case. From the Godoc: > The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex. Source: https://pkg.go.dev/sync#Map https://pkg.go.dev/sync#Map And regarding slow writes, those were recently improved in Go 1.24: > The implementation of sync.Map has been changed, improving performance, particularly for map modifications. For instance, modifications of disjoint sets of keys are much less likely to contend on larger maps, and there is no longer any ramp-up time required to achieve low-contention loads from the map. Source: https://go.dev/doc/go1.24#minor_library_changes https://go.dev/doc/go1.24#minor_library_changes ("sync" section)
- jfwwwfasdfs 2y agoA lot of languages have top types
- ncruces 2y agoThis is still strong typing, even it it's not static typing. It's static vs. dynamic and strong vs. weak. https://stackoverflow.com/a/11889763 https://stackoverflow.com/a/11889763
- 9rx 2y agoIt is strong, static, and structural. But structural typing is effectively compile-time duck typing, so it is understandable that some might confuse it with dynamic typing.
- masklinn 2y agoGgp is not talking about structural typing, but about sync.Pool type erasing (it takes `any` values, and returns `any` values). So you can put (and will retrieve) random garbage from it.
- 9rx 2y ago> Ggp is not talking about structural typing, Okay... but the reply was to the parent who questioned if the typing is static. It is by way of structural typing. The compiler enforces that the ducks, so to speak, are compatible. But as an empty interface has no constraints, all types are compatible. Whatever some other comment was talking about is irrelevant. > but about sync.Pool type erasing The type isn't erased...? p := sync.Pool{New: func() interface{} { return 1 }} fmt.Printf("%T", p.Get()) // Prints: int > So you can put (and will retrieve) random garbage from it. And you think that makes it dynamically typed? It does not.
- jlouis 2y agoIt is fairly common your type system ends up with escape hatches allowing you to violate the type rules in practice. See e.g., OCaml and the function "magic" in the Obj module. It serves as a way around a limitation in the type system which you don't want to deal with. You can still have the rest of the code base be safe, as long as you create a wrapper which is. The same can be said about having imperative implementations with functional interfaces wrapping said implementation. From the outside, you have a view of a system which is functionally sound. Internally, it might break the rules and use imperative code (usually for the case of efficiency).
- stouset 2y agoObviously every type system in practice has escape hatches. But I’ve never seen another staticly-typed language where you need to break out of the type system so regularly. Go’s type system has your back when you’re writing easy stuff. But it throws up its hands and leaves you to fend for yourself when you need to do nearly anything interesting or complex, which is precisely when I want the type system to have my back. I should not have to worry (or worse, not worry and be caught off guard) that my pool of database connections suddenly starts handing back strings.
- int_19h 2y ago> But I’ve never seen another staticly-typed language where you need to break out of the type system so regularly. It's about the same as Java and C# prior to their adoption of generics, and largely for the same reasons.