[HN Gopher] Constraints in Go
___________________________________________________________________
 
Constraints in Go
 
Author : gus_leonel
Score  : 63 points
Date   : 2024-11-17 08:44 UTC (5 hours ago)
 
web link (bitfieldconsulting.com)
w3m dump (bitfieldconsulting.com)
 
| indulona wrote:
| i have been writing Go exclusively for 5+ years and to this day i
| use generics only in a dedicated library that works with
| arrays(slices in Go world) and provides basic functionality like
| pop, push, shift, reverse, filter and so on.
| 
| Other than that, generics have not really solved an actual
| problem for me in the real world. Nice to have, but too mush fuss
| about nothing relevant.
 
  | throwaway63467 wrote:
  | Honestly so many things profit from generics, e.g. ORM code was
  | very awkward before especially when returning slices of objects
  | as everything was []any. Now you can say var users []User =
  | orm.Get[User](...) as opposed to e.g var users []any =
  | orm.Get(&User{}, ...), that alone is incredibly useful and
  | reduces boilerplate by a ton.
 
    | vbezhenar wrote:
    | ORM is anti-pattern and reducing boilerplate is bad.
 
      | makapuf wrote:
      | I agree. The best language to handle data in a RDBMs is
      | SQL, and in that case the best language to handle
      | application logic is Go (or Kotlin, Python or whatever). So
      | there must be some meeting point. Handling everything in Go
      | is not optimal, and all in sql not always practical. So how
      | to avoid redundant data description ? I often have structs
      | in a model Go file that reflect _queries_ I do, but that 's
      | not optimal since I tend to have to repeat what's in a
      | query to the language and the query to struct gathering is
      | often boilerplate. I also almost can reuse the info I need
      | for a query for another query but leave some fields blank
      | since they're not needed.. the approaches are not optimal.
      | Maybe a codegen sql to result structs / gathering info ?
 
      | bluesnews wrote:
      | Could you expand on this?
      | 
      | I don't like ORM because in my experience you inevitably
      | want full SQL features at some point but not sure if you
      | have the same issues in mind or not
 
        | vbezhenar wrote:
        | ORM is for object-relation mapping. Go is not object-
        | oriented language and OOP-patterns are not idiomatic Go,
        | so using ORM for Go cannot be idiomatic. That's generic
        | answer. As for more concrete points:
        | 
        | 1. Mapping SQL response to maps/structs or mapping
        | maps/structs to SQL parameters might be useful, but
        | that's rather trivial functionality and probably doesn't
        | qualify as ORM. Things get harder when we're talking
        | about complex joins and structs with relationships, but
        | still manageable.
        | 
        | 2. Introducing intermediate language which is converted
        | to SQL is bad. Inevitably it will have less features. It
        | will stay in the way for query optimisations. It'll make
        | things much less obvious, as you would need to understand
        | not only SQL, but also the process of translating
        | intermediate language to SQL.
        | 
        | 3. Automatic caching is bad. Database has its own caching
        | and if that's not enough, application can implement
        | custom caching where it makes sense.
        | 
        | In my opinion the only worthy database integration could
        | be implemented with full language support. So far I only
        | saw it with C# LINQ or with database-first languages
        | (PL/SQL, etc). C# and Go are like on opposite spectrum of
        | language design, so those who use Go probably should keep
        | its approach by writing simple, verbose and obvious code.
 
        | indulona wrote:
        | > Go is not object-oriented language
        | 
        | That is most definitely not true. Go just uses
        | composition instead of inheritance. Still OOP, just the
        | data flow is reversed from bottom to the top.
 
        | nordsieck wrote:
        | >> Go is not object-oriented language
        | 
        | > That is most definitely not true.
        | 
        | I think at best, you could say that Go is a multi-
        | paradigm language.
        | 
        | It's possible to write Go in an object oriented style.
        | 
        | It's also possible to write programs with no methods at
        | all (although you'd probably have to call methods from
        | the standard library).
        | 
        | That's in contrast to a language like Java or Ruby where
        | it's actually impossible to avoid creating objects.
 
        | kgeist wrote:
        | I find libraries like sqlx more than enough. Instead of a
        | full-blown ORM, they simply help hydrate Go structs from
        | returned SQL data, reducing boilerplate. I prefer the
        | repository pattern, where a repository is responsible for
        | retrieving data from storage (using sqlx) using simple,
        | clean code. Often, projects which use full-blown ORMs,
        | tend to equate SQL table = business object (aka
        | ActiveRecord) which leads to lots of problems. Business
        | logic should be completely decoupled from underlying
        | storage, which is an implementation detail. But more
        | often than not, ORM idiosyncracies end up leaking inside
        | business logic all over the place. As for complex joins
        | and what not, CQRS can be an answer. For read queries,
        | you can write complex raw SQL queries and simply hydrate
        | the results into lightweight structs, without having to
        | construct business objects at all (i.e. no need for
        | object-relational mapping in the first place). Stuff like
        | aggregated results, etc. Such structs can be ad hoc, for
        | very specific use cases, and they are easy to maintain
        | and are very fast (no N+1 problems, etc). With projects
        | like sqlx, it's a matter of defining an additional struct
        | and making a Select call.
 
      | TheDong wrote:
      | > reducing boilerplate is bad
      | 
      | Programming is about building abstractions, abstractions
      | are a way to reduce boilerplate.
      | 
      | Why do we need `func x(/* args _/ ) {  /_ body */ }`, when
      | you can just inline the function at each callsite and only
      | have a single main function? Functions are simply a way to
      | reduce boilerplate by deduplicating and naming code.
      | 
      | If 'reducing boilerplate is bad', then functions are bad,
      | and practically any abstraction is bad.
      | 
      | In my opinion, "reducing boilerplate is bad in some
      | scenarios where it leads to a worse abstraction than the
      | boilerplate-ful code would lead to".
      | 
      | I think you have to evaluate those things on a case-by-case
      | basis, and some ORMs make sense for some use-cases, where
      | they provide a coherent abstraction that reduces
      | boilerplate... and sometimes they reduce boilerplate, but
      | lead to a poor abstraction which requires more code to
      | fight around it.
 
      | bobnamob wrote:
      | Not liking ORM I can understand, db table <-> object
      | impedance mismatch is real, but "reducing boilerplate is
      | bad" is an interesting take.
      | 
      | Can you elaborate and give some examples of why reducing
      | boilerplate is generally "bad"?
 
        | rad_gruchalski wrote:
        | Not the person you're replying to. The orm sucks because
        | as soon as you go out of the beaten path of your average
        | select/insert/update/delete, you are inevitably going to
        | end up writing raw sql strings. Two cases in point:
        | postgres cte and jsonb queries, there are no facilities
        | in gorm for those, you will be just shoving raw sql into
        | gorm. You might as well stop pretending. There's a
        | difference between having something writing the sql and
        | mapping results into structs. The latter one can be done
        | with the stdlib sql package and doesn't require an
        | ,,orm".
        | 
        | There are two things an sql lib must do to be very
        | useful: prepared statements and mapping results. That's
        | enough.
 
        | bobnamob wrote:
        | You haven't answered my question at all.
        | 
        | The parent comment made two claims: ORM not great (I
        | agree) and "boilerplate reduction bad" which still needs
        | some elaboration
 
        | metaltyphoon wrote:
        | Perhaps you have to yet use a good ORM? I could probably
        | count on my fingers the times I had to drop to raw SQL in
        | EFCore. Even when you do that you can still have mapped
        | results, which reduces boilerplate.
 
    | indulona wrote:
    | understandable. thee are always valid uses cases. although
    | ORM in Go is not something that is widely used.
 
  | kgeist wrote:
  | Just checked, in my current project, the only place where I use
  | generics is in a custom cache implementation. From my
  | experience in C#, generics are mostly useful for implementing
  | custom containers. It's nice to have a clean interface which
  | doesn't force users to cast types from any.
 
    | BlackFly wrote:
    | Containers are sort of the leading order use of generics: I
    | put something in and want to statically get that type back
    | (so no cast, still safe).
    | 
    | Second use I usually find is when I have some structs with
    | some behavior and some associated but parameterizable helper.
    | In my case, differential equations together with guess
    | initializers for those differential equations. You can
    | certainly do it without generics, but then the initial guess
    | can be the wrong shape if you copy paste and don't change the
    | bits accordingly. The differential equation solver can then
    | take equations that are parameterized by a solution type
    | (varying in dimension, discretisation and variables) together
    | with an initializer that produces an initial guess of that
    | shape.
    | 
    | Finally, when your language can do a bit of introspection on
    | the type or the type may have static methods or you have type
    | classes, you can use the generic to control the output.
    | 
    | Basically, they are useful (like the article implies) when
    | you want to statically enforce constraints. Some people
    | prefer implicitly enforcing the constraint (if the code works
    | the constraint is satisfied) or with tests (if the tests pass
    | the constraint is satisfied). Other people prefer to have the
    | constraints impossible to not satisfy.
 
    | aljarry wrote:
    | > From my experience in C#, generics are mostly useful for
    | implementing custom containers.
    | 
    | That's my experience as well in C# - most of other usages of
    | generics are painful to maintain in the long run. I've had
    | most problems with code that joins generics with inheritance.
 
  | tonyedgecombe wrote:
  | I sometimes wonder if they should have implemented generics. On
  | the one hand you had a group of people using go as it was and
  | presumably mostly happy with the lack of generics. On the other
  | side you have people (like me) complaining about the lack of
  | generics but who were unlikely to use the language once they
  | were added.
  | 
  | It's very subjective but my gut feeling is they probably didn't
  | expand their community much by adding generics to the language.
 
    | cherryteastain wrote:
    | I think a lot of the people who wanted generics wanted them
    | more to be like C++ templates, with compile time duck typing.
    | Go maintainers were unwilling to go that route because of
    | complexity. However, as a result, any time I think "oh this
    | looks like it could be made generic" I fall into a rabbit
    | hole regarding what Go generics do and dont allow you to do
    | and usually end up copy pasting code instead.
 
    | vbezhenar wrote:
    | Generic containers are needed in some cases. Using generic
    | containers with interface{} is very slow and memory-
    | intensive. Not a problem for small containers, but for big
    | containers it's just not feasible, so you would need to
    | either copy&paste huge chunks of code or generate code.
    | Compared to those approaches, generic support is superior in
    | every way, so it's needed. But creating STL on top of them is
    | not the indended use-case.
 
  | kaba0 wrote:
  | Well, generics are mostly meant for library code. Just because
  | you don't need it, doesn't mean that code you use doesn't need
  | it.
 
  | gregwebs wrote:
  | There's an existing ecosystem that already works with the
  | constraints of not having generics. If you can write all your
  | code with that, then you won't need generic much. That
  | ecosystem was created with the sweat of library authors,
  | dealing with not having generics and also with users learning
  | to deal with the limitations and avoid panics.
  | 
  | Generics have been tremendously helpful for me and my team
  | anytime we are not satisfied with the existing ecosystem and
  | need to write our own library code. And as time goes on the
  | libraries that everyone uses will be using generics more.
 
| pansa2 wrote:
| I'm surprised by the complexity of Go's generic constraints,
| given the language's focus on simplicity. Things like the
| difference between "implementing" and "satisfying" a constraint
| [0], and exceptions around what a constraint can contain [1]:
| 
| > _A union (with more than one term) cannot contain the
| predeclared identifier comparable or interfaces that specify
| methods, or embed comparable or interfaces that specify methods._
| 
| Is this level of complexity unavoidable when implementing
| generics (in any language)? If not, could it have been avoided if
| Go's design had included generics from the start?
| 
| [0] https://stackoverflow.com/questions/77445861/whats-the-
| diffe...
| 
| [1]
| https://blog.merovius.de/posts/2024-01-05_constraining_compl...
 
  | rendaw wrote:
  | There are tons of random limitations not present in other
  | languages too, like no generic methods.
 
  | burakemir wrote:
  | Generics are a powerful mechanism, and there is a spectrum. The
  | act of retrofitting generics on go without generics certainly
  | meant that some points in the design space were not available.
  | On the other hand, when making a language change as adding
  | generics, one wants to be careful that it pulls its own weight:
  | it would be be sad if generics had been added and then many
  | useful patterns could not be typed. The design choices revolve
  | around expressivity (what patterns can be typed) and inference
  | (what annotations are required). Combining generics with
  | subtyping and inference is difficult as undecidability looms.
  | In a language with subtyping it cannot be avoided (or the
  | resulting language would be very bland). So I think the answer
  | is no, this part of the complexity could not have been avoided.
  | I think they did a great job at retrofitting and leaving the
  | basic style of the language intact - even if I'd personally
  | prefer a language design with a different style but more
  | expressive typing.
 
___________________________________________________________________
(page generated 2024-11-17 14:00 UTC)