Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

>Do we have 67 implementations of sort.Interface?

Hahaha. This has to be satire right?

>Generics would not make our codebase significantly better, more maintainable, or easier to understand.

Generics are literally a form of abstraction. You might as well be arguing that abstraction doesn't help. Why do you even have subtype polymorphism then? Why not just reimplement everything? That's not a significantly difficult part of your job as you said.

One of the best things about Go is it seems to be a strong signaler of the type of engineering team I avoided.



Generics are literally a form of abstraction

Is your unstated assumption then that all forms of abstraction must be used? If you've done substantive projects, you'll come to realize that abstractions have a cost, and that everything should be considered on a cost/benefit basis.

You might as well be arguing that abstraction doesn't help.

This is a black and white binary fallacy invoked to then create a straw man, which also seems to suggest that you haven't learned the importance of considering cost/benefit.

One of the best things about Go is it seems to be a strong signaler of the type of engineering team I avoided.

I would agree, this would seem to be a good signaler.


can you articulate the exact cost of adding generics? The benefits are profound, and the PL community has been doing research on it for the last forty some odd years.

Some of the benefits are opportunities for

* specialization

* reduction in boilerplate

* parametricity

* free theorems

* type classes

Objectively, a collections library written with generics and no subtyping will be much better and cleaner than a subtype based one.

The problem is, I've never heard generics argued against by someone who really understands generics. It's usually folks who got confused by them in java or really didn't dig into the theory behind them. An argument from ignorance isn't much of an argument.


Your comment is illustrative of a lack of awareness of context. You don't specify the context, but it seems like you're stuck in this academic/language theory mindset. From that standpoint, I rather like generics. It's clear to see how they can enable DRY if used judiciously. (Clear from even a freshman CS undergrad perspective.)

However, as a professional who gets paid to wrangle C++, I find the "Tragedy of the Commons" that results from every bright-eyed recent grad wanting to leave their mark on a system...tiresome. I recently fixed a bug caused by a small find-replace mistake, where a static_cast<int> was left out, resulting in an int() operator being generated by a confluence of preprocessor macros, inlined functions, and composed templates, where the call breaking in the stack trace was expressed nowhere in the code-base. It's one thing to DRY, but taking it one step too far to "Don't State Yourself In The First Place" is way too implicit. Abstractions have a cost, and sometimes the cost is epiphenomenal and gets paid years afterwards.

An argument from ignorance isn't much of an argument.

The decision of the Go team to not include generics is conservative and pragmatic. The context they consider is across an entire language community, and their decision is informed by observations made on code-bases at Google and elsewhere. How many 500k+ line code bases that have been around more than a decade have you worked on? I'm on something like my 4th. My conclusion from that is that we programmers as a group are mostly too anxious to be "clever" and biased towards doing too much when they evaluate the cost-benefit of "clever."

Do you have good data/experience on the epiphenomenal harm done by many, many "clever" programmers over years?


Ah, so you can't argue with me, so you're going to try the ol' appeal to authority ("i've worked on such big code bases that you'll never see").

You seem to assume I'm some naive recent grad. I've worked on more than a few 500k LOC applications. I'm a lead at a very large tech company (Fortune 50). The idea that '"clever"' programmers is a thing is incorrect.

There are good programmers and bad programmers. It doesn't matter if the lack of abstraction used by one type creates a monolithic mess of spaghetti code or if they use overly obscure attempts at abstraction. Bad code causes technical debt either way, and I've seen both done quite often.


Ah, so you can't argue with me, so you're going to try the ol' appeal to authority ("i've worked on such big code bases that you'll never see").

Uh, no. I asked if you understand such a context and if you have such data. Going by what you state, you do. A simple "yes" would have sufficed, and you could have left out the projection. Thank you for including the projection, as it is another valuable "signal."

The idea that '"clever"' programmers is a thing is incorrect.

There are good programmers and bad programmers.

Bad code causes technical debt either way, and I've seen both done quite often.

You do understand the use of quotes, then? "Clever" programming is thought to be clever by the perpetrator, but is actually bad programming and comprises technical debt. So either you are contradicting yourself above, or you are implying that "bad programmers" know they are bad, but do bad things anyways? This doesn't fit my experience.


Generics introduce more complexity in the type system which in turn makes the compiler slower.

Generics introduce more complexity for the reader of the code because it's another abstraction to understand.

It's debatable but when your brain is thinking about generics or context-switching because it has to wait on the compiler to finish, it's less time making progress on the actual thing that needs to be done.


The whole point of abstractions is that you don't have to worry about as much. Generics take away complexity, that's the whole point.


I suppose it depends on the level of understanding that you want from your code. Generics introduce another dimension which you have to think about when you want a good level of control on allocations for example. Different data types also have different optimizations available which could be missed when blindly relying on the generic algorithm (think sorting on a fixed set for example)


The whole point of abstractions is that you don't have to worry about as much. Generics take away complexity, that's the whole point.

Sure. Abstractions are perfect, and you never have to think about their costs. Generics always take away complexity. Gotcha.


> >Do we have 67 implementations of sort.Interface?

> Hahaha. This has to be satire right?

Nope.

    /home/nate/src/github.com/juju/juju$ grep -r ") Less(" . | wc -l
    67
(granted, 10 are under the .git directory, so I guess 57)

But in any other language, we'd still have the same 57 definitions of how to sort a type.... we'd just have 3 fewer lines of boilerplate for each of those (which live off in the bottom of a file somewhere and will never ever need to change).


> But in any other language, we'd still have the same 57 definitions of how to sort a type...

That claim turns out to not be the case.


Aside from trivial types, like strings or integers, how does the language know how to sort a list of values, if you don't tell it how to?

Translate this into whatever language you like:

    Machine {
        Name string
        OS  string
        RAM int
    } 
You have 3 places that want to sort a list of machines, one by name, one by OS, and one by RAM. You're telling me there's a language that can do that without having to write some kind of code like this for each?

   sort(machines, key: Name)
I don't understand how that's possible, but I welcome your explanation.


Sorting on all three fields in priority order is what I had in mind, and that's trivial in Haskell by adding "deriving(Ord)" to the data type definition and then just using the standard "sort :: Ord a => [a] -> [a]".

If you're always going to sort them based on some (other) relation between the fields, make your type a custom instance of Ord, e.g. "instance Ord Machine where compare = compare `on` name".

To sort the same type with distinct comparators, you'll obviously need to distinguish them, as in e.g. "osSort = sortBy (compare `on` os)".


So... you will still need 57 spots in the code where you define how to sort a type.

Maybe my reference to sort.Interface is confusing people. When I say we have 57 implementations of sort.Interface, that's 57 different types and/or different ways of sorting one of those types. So, like, sorting Machine by Name would be one implementation, sorting Machine by Name then OS then RAM would be another implementation. You write an implementation of sort.Interface for every type, and for each way you would like to be able to sort it.

An implementation of sort.Interface just requires three methods:

    Len() int // return the length of the list
    Swap(i, j int) // swap items at indices i and j
    Less(i, j int) bool  // return true if list[i] is less than list[j]
It's the implementation in Less that determines the order.

That's not really so different than what you're describing in Haskell, it's just not part of the type, it's a new type that you convert the original type into, to pass into the sort.Sort() function (and because the underlying type is a slice, which is a glorified struct with a pointer to an array, that also sorts the original value).


It's possible to make one implementation for a type that supports multiple orderings, at the cost of another indirection [0]. This turns O(N*M) implementations for N types and M sorting orders into just O(N). (I'm not counting an inline anonymous function as a new implementation.)

In practice, it's rare to need to sort a slice more than one way.

[0]: https://gist.github.com/infogulch/5db15e5ae5cf073f1088033ba4...


There are no languages where you can sort machines by either name, OS, or RAM without in some way saying which to sort by.

That's a straw man: nobody is telling you that.

But, you are being told there are multiple languages where you can sort machines by their fields without having to write

    type byName []Machine
    type byOS []Machine
    type byRAM []Machine

    func (a byName) Len () int {
      return len(a)
    }

    func (a byOS) Len () int {
      return len(a)
    }

    func (a byRAM) Len () int {
      return len(a)
    }

    func (a byName) Swap(i, j int) {
      a[i], a[j] = a[j], a[i]
    }

    func (a byOS) Swap(i, j int) {
      a[i], a[j] = a[i], a[i]
    }

    func (a byRAM) Swap(i, j int) {
      a[i], a[j] = a[j], a[i]
    }

    func (a byName) Less(i, j int) bool {
      return a[i].Name < a[j].Name
    }

    func (a byOS) Less(i, j int) bool {
      return a[i].RAM < a[i].RAM
    }

    func (a byRAM) Less(i, j int) bool {
      return a[i].OS < a[j].OS
    }

    sort.Sort(byName(machines))
    sort.Sort(byOS(machines))
    sort.Sort(byRAM(machines))
For example, in Julia, you can just do

    sort(machines, by=m->m.Name)
    sort(machines, by=m->m.OS)
    sort(machines, by=m->m.RAM)
The equivalent is possible in any almost modern language. E.g. in C#

    machines.OrderBy(m=>m.Name)
    machines.OrderBy(m=>m.OS)
    machines.OrderBy(m=>m.RAM)
In Haskell

    sortBy (comparing Name) machines
    sortBy (comparing OS) machines
    sortBy (comparing RAM) machines
This is an extensively solved problem in modern programming languages.

As a bonus, the opportunity for making an error each time you want to sort a new type of array in a new way is reduced if you only have to write one line of code each time.


The canonical solution to this problem is to provide a function to perform the comparison, or to require the types implement a "Sortable" or "Comparable" interface.


yes, which for 57 different types and/or comparison methods requires 57 different functions... which is basically the exact same thing you do in Go. It's just in go, you define a new type based on the original value, rather than just a function.


Just to nitpick, that is 4 lines because you add a type too. Also, I noticed this in controller.go:

        // Unreachable based on the rules of there not being duplicate
	// environments of the same name for the same owner, but return false
	// instead of panicing.
	return false
Guess what, I worked with a sort function with the same kind of assumptions, but the implicit rules was broken: the "should never happen" path happened (names were not unique, after all). I found about that only after I wrote my own sort which was careful enough to check that the order was indeed total and when results diverged for some tests. I really disliked that because sorting was an important part in that tool (maybe it is not in yours).


> >Generics would not make our codebase significantly better, more maintainable, or easier to understand.

> Generics are literally a form of abstraction. You might as well be arguing that abstraction doesn't help.

You missed one word: "significantly". Sure, abstractions help. That wasn't the claim. The claim was that, in a million lines, the lack of that particular way of doing abstractions did not significantly hurt.

Would it have helped? Sure. Would it have helped enough to matter "very much"? No (by NateDad's standards, which may differ from yours).

67 implementations of sort.Interface? Sure, I don't like it, but in a million lines, you've got much bigger things to worry about.


   package pleasePutMeInYourVendorsDir

   import (
      "io"
       ...
   )
   ...

   func sureYouCan() {
      // ...
      io.EOF = io.ErrShortWrite
      // ...
   }




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: