[HN Gopher] Functional Programming in Go with Generics
___________________________________________________________________
Functional Programming in Go with Generics
Author : ngaut
Score : 103 points
Date : 2021-05-27 01:58 UTC (21 hours ago)
(HTM) web link (ani.dev)
(TXT) w3m dump (ani.dev)
| pharmakom wrote:
| Go seems like one of the worst languages you could choose for
| functional programming. Why paddle upstream when you could either
| write idiomatic Go or switch to a language properly designed for
| FP?
| pjmlp wrote:
| Anyone that has DevOps on their daily tasks is now bound to
| deal with Go code, due its relation to Docker, Kubernetes and
| cloud infrastructure, just like we need to deal with C on UNIX
| clones or JavaScript on the browser.
| eru wrote:
| Some people are stuck with Go for their current projects,
| perhaps?
| minkzilla wrote:
| Mixing in generics to an already existing code base sounds
| like a bad idea.
| andolanra wrote:
| I think people have a habit of taking relatively surface-level
| features of functional programming and focusing on them to the
| exclusion of the real benefits of functional programming. The use
| of something like _compose3_ in the "functional" example here is
| a perfect example. Sure, function composition is a "functional"
| thing, but I don't see why you wouldn't instead write something
| like (handwaving wildly on the specifics, since I know barely any
| Go and certainly am not up to speed on the generics proposal):
| func getTopUsers(posts []Post) []UserLevelPoints {
| return posts.GroupBy(func (v Post) string { return v.Level })
| .Values() .Map(getTopUser) }
|
| This pipeline style is significantly easier to read (especially
| without having to put all those extraneous type parameters in
| your call to _compose3_!) and doesn 't actually lose any of the
| core advantages of the functional style: purity, testability in
| isolation, equational reasoning, and so forth. Sure, the Haskell
| equivalent might use composition... but composition reads more or
| less naturally in Haskell, and I don't think it does _at all_
| here in Go. If your specific approach to "functional
| programming" makes your code theoretically easier to reason about
| but practically harder to both read and write, then is it really
| helping you much?
| mjburgess wrote:
| Exactly my thought: _why_ is this clearer?
|
| I'm often think FP-first (data science) -- here though, I was
| surprised by how I went back over the imperative alternative.
|
| Avoiding `append()` isnt worth it in a language where this
| level of annotation is required to do FP. It's _less clear_.
| [deleted]
| platinumrad wrote:
| Yeah if you're writing "Compose3" you've already lost. The
| extra type annotations only make it worse. x
| := foo() y := bar(x) z := baz(y)
|
| Wow.
| pharmakom wrote:
| Ok, now what if foo, bar and bar are async? Nullable? Result
| types?
| platinumrad wrote:
| Well it's not like Go has do notation so I don't know what
| you're expecting here.
| pharmakom wrote:
| I'm pointing out that functional programming goes beyond
| trivial things like compose3 and list operations (useful
| though they can be).
| platinumrad wrote:
| Sure, but e.g. monadic futures aren't going to be very
| usable in a language without do notation, custom
| operators, or non-verbose lambdas. Might as well just use
| channels. As for nullability and error handling, writing
| "if" over and over again might be bad, but in Go I don't
| think functional approaches are going to be any better.
| Maybe liftA2, etc. on pointers would be usable, but not
| much else.
| mappu wrote:
| Piling on here - My impression of your snippet here is that
| GroupBy() and Values() both construct (potentially very large)
| temporary arrays on the stack.
|
| Maybe Haskell and F# can elide this kind of thing, but it's not
| something the Go compiler is going to do.
| [deleted]
| Yoric wrote:
| Rust and OCaml (the latter if you use the right libraries)
| can definitely avoid it. Why couldn't Go?
| mappu wrote:
| An alternate-universe Go with a lot more (& slower)
| compiler passes could do it, but I don't believe there are
| any plans to build such a thing. As it stands, this kind of
| code (where all intermediates have type []T) will run much
| more poorly than you could do with imperative loops.
|
| The final generics design is probably not expressive enough
| to do it ergonomically in user code neither (building
| something where the intermediates are a library type with a
| last `.something()` call to collect a final []T).
| pjmlp wrote:
| OCaml compiles as fast Go.
|
| No secret sauce, just having the pleasure of chosing
| multiple backends, including an interpreter.
|
| Bytecode runtime during workflow, optimized native AOT
| for release and final production tests.
| duped wrote:
| That's just bad design - iterators and sequences need to
| be lazy. The intermediary type should not eagerly create
| large arrays on the stack, it should only ever do real
| work once the iterator has been driven.
| rad_gruchalski wrote:
| No, they don't need to be. They can be. Go wasn't
| designed as a functional language but to be fast to
| compile, fast to execute, zero dependency binary and
| simple syntax. It achieved that by a mile.
| duped wrote:
| Implementing iterator combinators by eagerly evaluating
| them into temporaries is a bad, naive design that results
| in the issue illustrated above.
|
| A much better design is to do it lazily which is possible
| if you can store closures as fields of a data structure.
|
| Not sure what fast compilation and execution or
| dependencies has to do with this. Implementing iterators
| that way is the worst possible solution that results in
| slow execution that can blow up the stack and crash a
| program. It would be better not to have iterators at all
| than to require them to be eagerly evaluated.
|
| You can support functional features without being a
| Haskell...
| rad_gruchalski wrote:
| Yes, lots of science. Go has slices which require
| specifying a size and you iterate over them using a for
| statement. You want lazy, pop in a channel and do it
| lazy.
| duped wrote:
| This isn't "lots of science" it's rudimentary software
| engineering...
|
| Sending data through channels just to implement an
| iterator seems insane. It's not strictly lazy either...
|
| Why do go developers reject simple patterns that make
| faster code?
| rad_gruchalski wrote:
| How are those magical lazy operations implemented in
| other languages? Someone had to take that insane step.
| It's just not by default in go.
|
| Look, I've written lots of scala and erlang so I get your
| argument. It just does not exist in go. And that's okay.
|
| Never got caught thinking "gee, need a lazy collection".
| Maybe a stream when reading stuff from the ether. In go,
| channel was enough. 5 minute job. Nothing insane.
| duped wrote:
| They're implemented with closures, not passing things
| through FIFOs. It's not magic. You barely need a compiler
| pass to implement the sugar for `for` loops: it's a
| trivial transformation to a `while`.
| rad_gruchalski wrote:
| Okay, but what's the benefit for go. I can iterate over
| slices, maps and channels (for ... range). Slices must
| have a capacity defined. What's the benefit of lazy
| iterator?
|
| for over a channel is in essence lazy. Channel stores the
| data as a linked list. So what's the benefit.
| skybrian wrote:
| That's probably okay if the code is I/O bound anyway, but
| in Go it will run a lot slower than doing multiple steps
| within a single for loop.
|
| For loops may be unfashionable but they aren't hard to
| read.
| duped wrote:
| I don't find iterator combinations hard to read.
| Especially with postfix methods.
|
| I do find deeply nested for loops with outer variables
| that might start uninitialized or need to be mutable much
| more difficult to read.
| skybrian wrote:
| Sometimes moving an inner for loop to a separate function
| (that will likely be inlined) is better to explicitly
| show its inputs and outputs.
| wrnr wrote:
| I've been waiting to do things like that in Go for a long
| time. It can be done efficiently just like the Java Stream
| interface.
| skjfdoslifjeifj wrote:
| The standard Ocaml compiler doesn't elide it and as far as
| I'm aware neither do any of the publicly available
| libraries. All of the lazy stream implementations in Ocaml
| have a performance cost similar to Java streams. Haskell
| and Rust are relatively unique in that they can completely
| eliminate the performance cost in the typical cases.
| Serow225 wrote:
| FWIW C# would as well (LINQ)
| mpfundstein wrote:
| i really love fp. heck i was even overly zealous about it few
| years ago, annoying everyone around me with it :-)
|
| but this development kibda makes me sad :-) i use go for so many
| projects because it finally restrained me and my employees enough
| to not argue about language features all the same. and all our
| code bases look the same. i loved that about go. no, with
| generics, and all potential libs (just wait for go lowdash, go
| rambda, go fantasyland, go promise etc), that will come to an end
| :-)
| graftak wrote:
| Missed a big opportunity to go with godash.
| pcr910303 wrote:
| (Not directly related, but) The article reminds of a recent
| article named 'Why Go Getting Generics Will Not Change Idiomatic
| Go'[0].
|
| Using functional programming in Go... just doesn't look right.
| Generics are useful, but won't change the whole Go ecosystem into
| FP. Maybe some uses of Map might make sense, but... idiomatic
| code will look the same.
|
| [0]: http://www.jerf.org/iri/post/2955
| mpfundstein wrote:
| of course it will change ideomatic go. there is no reason to
| think otherwise. just look at javascript or c++. both languages
| transformed heavily and nowadays people look at you strange if
| you write 'var' in js or use a raw pointer in c++. :-)
|
| so Go will change and i think that sucks hardcore. there are
| already enough languages where folks can go crazy with types,
| fp, monads etc. My selling point for Go was that it was mega
| restricted :-)
| jerf wrote:
| (I'm the author of that piece.) It will change what _some
| people_ insist is idiomatic Go, but my contention is that
| when this all settles out, call it a year after production
| release of the generics, the community as a whole will
| generally come down against "functional style" programming
| in Go, and people who want to program functionally in Go will
| discover that they continue to be quite frustrated because
| you need half-a-dozen more features.
|
| It's going to be so verbose and unfun in Go that only very
| stubborn people are going to stick to it anyhow. I'm not
| saying that because I hate the style, I'm saying that because
| it's still going to be a lot of boilerplate wrapped around
| not much payload. It really won't be fun. I like "map (f1 .
| f2 . f3)"; the equivalent in Go even post-generics will be
| crazy full of boilerplate and garbage.
|
| It will change some aspects of idiomatic Go. I don't care
| much at all about the "functional" aspects but I am greatly
| looking forward to a proliferation of more specialized data
| structures that are type-safe. I think one of the major
| weaknesses of Go in what is arguably its wheelhouse, network
| servers, is that network servers are often _precisely_ where
| I want some obscure data structure with the performance
| characteristics I _need_ to scale up some service properly.
| It 's true that the dynamic scripting languages showed us
| that you can get a long way with arrays & dictionaries, but
| I'd like some more stuff. I've got a few places a "map" would
| be slightly convenient, but I've got several places where
| I've got _architectural_ problems because I had to hack in an
| immutable data type or tree or something else that I 'd much
| rather be using a bullet-proof, community-tested
| implementation of a type-safe data structure than some subset
| I smashed together and lightly covered with unit tests just
| to do this one particular job. This is the change I'm
| actually looking forward to. I've also got some channel
| pipelines that, while they are set up and working, would be
| easier to understand with some helper functions that could
| deal with them.
|
| I am also looking forward to either using or writing a very
| simple concurrent pipeline that specifies a set of transform
| functions and the number of workers to start up for each and
| takes care of actually bringing up the network correctly. Or
| implement structured concurrency:
| https://vorpus.org/blog/notes-on-structured-concurrency-
| or-g... Go may never enforce this but I'm tempted to try to
| enforce it in my programs.
| mpfundstein wrote:
| thanks foe the reply and i understand your sentiment. i
| think what will happen is that people will start advocating
| for more and more syntax changes. think of => in js instead
| of function. a minor tweak but with HUGE impact. probably
| much more than const/let etc. and eventually more and more
| of this will change the language.
|
| but i hope i am wrong. :-)
|
| in general i get your point amd agree with it. i have
| written large go codebases and ran into exactly the same
| issues. but i wouldn't agree to the trade off :-) then i
| would have used Rust in the first place I guess
| jerf wrote:
| "i think what will happen is that people will start
| advocating for more and more syntax changes. think of =>
| in js instead of function."
|
| They won't need to start; hit the Go issue tracker to see
| a whole bunch of them have already been made and
| rejected. People have been advocating for them for a
| while. I virtually guarantee that of all languages, the
| Go team will not change their mind in response to
| existence of generics for them. Their bar for accepting
| changes is very, very high. It is not insurmountable, but
| it is very high.
|
| Remember, Go didn't come out yesterday. Go is now over
| ten years old. We have a good idea of the velocity of
| changes it makes. It is low.
| whateveracct wrote:
| At the end of the day, if you want to program immutably or in
| such a way that is declarative and introspectable, you best
| look to FP for prior art in those sorts of spaces. Certain
| problems are age-old child's play in FP-land.
| thatjamesdude wrote:
| Whenever I see interface{} mentioned in Go I see vague references
| to polymorphism.
|
| That is wrong.
|
| Interface is a special type in Go. _All_ interfaces in go,
| including ones you declare, hold 2 pieces of data in memory
|
| 1. Pointer to the concrete type
|
| 2. Metadata about the concrete type
|
| interface{} is not a special thing, it is just the empty
| interface, and _everything_ satisfies the empty interface.
|
| This is quite neat. All a type assertion does is follow the
| pointer and check the type data.
|
| There is also a type switch in Go.
| https://play.golang.org/p/22aegLocHXI
|
| This makes building handlers around the dynamic type easy. But
| for the most part: Single function interfaces. Compose those to
| make make larger interfaces if absolutely needed (see io.Writer,
| io.Reader and io.ReadWriter)
| rad_gruchalski wrote:
| I am ambivalent over generics. I can today write:
|
| type meh interface{ foo() error }
|
| type whatever struct {}
|
| func (h *whatever) foo() error
|
| And still pass []whatever{} to func canihaz([]meh) so not sure
| how much generics really add to that.
|
| I don't mind generics but when they are there, I use them.
| Whatever :)
|
| Accepting interface{} is a code smell. I'd accept that only when
| dealing with wire data. Never in the business logic.
| domano wrote:
| I dont understand the list of supported features. In my opinion
| currying and variadic functions are supported in go to some
| degree.
|
| Variadic functions even links to the go example, but classifies
| them as unsupported in go.
| platinumrad wrote:
| Variadic functions aren't even necessary for functional
| programming in the first place.
| jerf wrote:
| Go does have variadic functions, but what are you counting as
| "currying support"?
| domano wrote:
| Well, since functions are first class citizens nothing stops
| me from turning a (a,b,c) into a (a)(b)(c)
| jerf wrote:
| Sure, but nothing helps it either, and you're going to take
| a decent performance hit if you do that because you're
| turning one function call, possibly inlineable, into three
| that aren't going to inline, even with new Go generics you
| will not have enough power to write a "ToCurried" function
| that converts to a curried version at compile time (I think
| you could theoretically do it with reflect at runtime, but
| now you're taking an even _bigger_ performance hit...)
|
| I would not normally say any language "support currying"
| just by having first-class functions. Python can do several
| of those things I said Go can't do and I _still_ wouldn 't
| call that "supporting currying". It sure isn't anything
| like the experience of using curry & uncurry in Haskell: ht
| tps://hackage.haskell.org/package/base-4.15.0.0/docs/Prelu.
| ..
| domano wrote:
| Thanks, looking at it this way makes more sense!
| svnpenn wrote:
| Functional programming is a kind of leetcode thing that I don't
| want to see come to Go. In many cases is destroys readability,
| and allows people to forget about the complexity of the code they
| are writing (in a bad way).
|
| With most Go code today, you can just look at the indentation, or
| count the for loops.
|
| https://github.com/golang/go/issues/45955#issuecomment-83235...
| aliyfarah wrote:
| A leetcode thing? Most of leetcode solutions I've seen are
| terse and over optimised for a specific problem, don't think
| I've seen any functional programming when I was doing them.
| ngc248 wrote:
| i think what he meant was not leetcode the service, but
| leetcode as in elite code
| ebingdom wrote:
| It's a shame that someone can become so misguided about
| functional programming. Functional programming isn't about
| destroying readability. It's actually the opposite; it enhances
| the ability to reason about code by (a) being disciplined about
| where side effects are allowed to happen, and (b) leveraging
| reusable abstractions for common patterns (e.g., transforming
| each element of a list, building a generic container that can
| be used for arbitrary types without sacrificing type safety,
| etc.) rather than re-inventing them every time with copying and
| pasting. Generally, functional programs are safer and shorter
| than their procedural/OOP alternatives, which is a good thing.
| Less code means fewer places for bugs to hide.
| mappu wrote:
| All true, but the dichotomy here isn't between functional and
| imperative - it's between imperative, and a mix of both.
| ramenmeal wrote:
| In this article, which code sample would you say is more
| readable? The imperative style or the functional style?
| viraptor wrote:
| Theory vs practice for the specific language. Here, the
| extra signatures and unnatural patterns destroy the
| readability.
|
| It could still be better though if you have, for example a
| number of pipelines for your data. There, the reuse of
| Sort/Group/Frobnicate/... in close by blocks would actually
| be nicer.
|
| But yeah - the more extra syntax is required for those
| pattern, the worse they are in practice. (or, the more
| things you have to abstract in a small range to make it
| useful)
| ebingdom wrote:
| I would say the imperative version is easier to read in
| this article. The functional version doesn't really
| illustrate the benefits of functional programming very
| well, IMHO. I think there are two issues that reduce the
| effectiveness of the example:
|
| 1. Both examples contain a bunch of extra logic that has
| nothing to do with the difference between the two styles.
| For example, the code that converts users to that
| `UserLevelPoints` struct takes up a lot of space in both
| examples and is essentially the same in both. I think it
| would have been better (for pedagogical purposes) to just
| return the users (or perhaps a simpler struct containing a
| level and a user).
|
| 2. Both examples still have all the verbosity that is
| typical of imperative code, since it's still Go after all.
|
| If one were to write the same function in a syntax which
| was designed for functional programming, the result might
| look something like this: topUser = head
| . sortBy points topUserPerLevel = topUser . values
| . groupBy level
|
| I find this vastly quicker to read and understand than
| either of the examples in the article. But that relies on
| me knowing the functional-optimized syntax (e.g., that the
| `.` operator is just function composition), and for most
| imperative programmers that is the main hurdle. Since I am
| familiar with both styles, I can tell you that this would
| take me about 6 seconds to understand, whereas
| understanding each of the examples in the article took me
| at least a minute to even read the code, let alone
| understand it--and if there were bugs I wouldn't have
| noticed.
|
| In contrast, with the simple functional version I provided,
| I can read it in seconds _and_ be reasonably confident that
| it is correct (assuming all the pieces fit together in a
| valid way, which a type checker can check for me).
|
| If the unfamiliar syntax prevents you from grokking the
| example I provided, it's reasonable to assume it's just
| some clever code golf that should be discouraged in
| production code. But please resist that temptation; this is
| actually what good code looks like. It's simple, not
| repetitive, and there aren't many places for bugs to hide.
| cle wrote:
| One person's readable code is another person's unreadable
| mess. It depends on what kind of program you're writing.
|
| The fact that functional programming emphasizes "abstraction"
| so much means that, by definition, you're further away from
| what's actually happening. So looking at some declarative
| code it's not immediately obvious _what the computer will
| actually do_. The kinds of programs I write require me to
| care about that deeply. So when I see declarative code all I
| see is a giant question mark as I wonder what in the world
| the computer will _actually_ be doing when it executes that
| code.
|
| Anyway, my point isn't that one is better than the other,
| it's that it depends on what you're doing. For some of us,
| the higher levels of abstraction are actually _less_
| readable.
| ebingdom wrote:
| I think this is a reasonable point, and of course you could
| take it further and say that assembly language is better
| than functional programming when you're doing work that
| requires you to reason about what the CPU is doing exactly.
|
| But I would argue that the majority of Go programs should
| not require the programmer to follow the intimate details
| of a program's execution when trying to understand the
| business logic. The fact that the language has a garbage
| collector would suggest that it was designed for programs
| where some details can be hidden away.
|
| So I agree with your general point, but I am somewhat
| skeptical about its application to Go.
| nexuist wrote:
| If the lack of abstraction is a requirement, why use a
| language like Go at all? Shouldn't you be using C for these
| sort of projects?
| cultofmetatron wrote:
| zig is also a really good contender in this space as
| well. it also maps fairly cleanly to assembly while
| providing a bit more safety.
| kaba0 wrote:
| The only thing that makes programming any useful program
| even remotely possible is abstractions. The human brain
| simply breaks down even at the fraction of the complexity
| of any non-trivial app. There is wrong abstraction, and
| over-abstraction that can obscure a codebase, but it is
| orthogonal. And no, you can't write simpler code, there is
| essential complexity, that can't be reduced.
|
| Also, it's kind of laughable to think that writing code in
| a high level language with a runtime, GC, etc, you have any
| idea what will happen at execution.
| cle wrote:
| I'm not saying to never use abstractions. There's a
| balance.
| amw-zero wrote:
| > it enhances the ability to reason about code
|
| In your opinion.
|
| > Generally, functional programs are safer and shorter than
| their procedural/OOP alternatives
|
| Please provide any evidence that this is true, especially the
| 'safe' part.
|
| Any study I've read says there's almost no effect on language
| choice at all on bug rate.
| ebingdom wrote:
| > > it enhances the ability to reason about code
|
| > In your opinion.
|
| Correct. But my opinion is informed by years of experience
| with functional programming, procedural programming, and
| OOP (each).
|
| > Please provide any evidence that this is true, especially
| the 'safe' part. Any study I've read says there's almost no
| effect on language choice at all on bug rate.
|
| Sure, here is a paper which provides the evidence you're
| looking for: "A Large Scale Study of Programming Languages
| and Code Quality in Github"
| (https://dl.acm.org/doi/10.1145/2635868.2635922). That
| paper concludes:
|
| > The data indicates functional languages are better than
| procedural languages; it suggests that strong typing is
| better than weak typing; that static typing is better than
| dynamic; and that managed memory usage is better than
| unmanaged.
|
| I personally don't trust academic studies on programming
| language effectiveness, since they often contradict each
| other (e.g., https://arxiv.org/abs/1901.10220 contradicts
| the paper I cited above). But if you're looking for a peer-
| reviewed paper, there you go.
|
| Choosing the right paradigm absolutely has an effect on the
| bug rate. If you have to write more code, repeat code, or
| deal with low-level details irrelevant to the problem at
| hand, you are going to make more mistakes. On top of that,
| functional languages often have better type systems than
| procedural/OOP ones, which helps with bug catching that
| much more. Taking this to extreme, with dependent type
| systems you can actually verify arbitrary mathematical
| properties of your programs.
|
| How much experience do you have with functional
| programming? I have professional and academic experience
| with both functional programming and OOP, and everyone I've
| met with that experience agrees about the trade-offs I've
| been discussing in my comments.
| cle wrote:
| I seem to be a bit of an outlier, but I am not looking forward to
| generics in Go. I don't like the kinds of discussions it
| attracts...vague hand-wavy arguments about "expressiveness" and
| "purity". Expressiveness to me means more arguing in code
| reviews, and nothing is stopping us from writing pure functions
| now, I do it all the time.
|
| I maintain a few large Go codebases, tens of thousands of LoC
| each, and not having generics in practice is way, way down on my
| list of challenges. Equivalent Java codebases that I've
| maintained are usually harder to maintain _because_ of generics,
| with over-eager functional programmers itching to show off their
| skills in abstraction, instead of solving business problems.
|
| I hope I'm wrong, and that this will make Go a better language.
| But I doubt it.
| eru wrote:
| > [...] and nothing is stopping us from writing pure functions
| now, I do it all the time.
|
| One point of generics is that you have to write your pure
| functions less often..
| haolez wrote:
| All languages are converging to the ultimate language: The
| TypePyGon!
|
| Jokes aside, I wish our tools were more "final". When is a
| programming language done regarding the pain it was meant to
| solve?
| Zababa wrote:
| > When is a programming language done regarding the pain it
| was meant to solve?
|
| Elixir is often presented as "done"
| (https://elixirforum.com/t/is-elixir-done/20830/12). For
| example, the depreciation policy mention that function can
| only be removed on a major version (1.X to 2.Y for example),
| and for now it's still in 1.X and no Elixir 2 is planned.
|
| It still has releases (https://elixir-
| lang.org/blog/2021/05/19/elixir-v1-12-0-relea...) to follow
| Erlang releases, but Erlang itself is also more conservative
| than some other languages.
| munificent wrote:
| _> When is a programming language done regarding the pain it
| was meant to solve?_
|
| It's hard to imagine that a language can be "done" when the
| problems being solved with it keep changing and we keep
| discovering new techniques for solving them.
|
| When is automotive design done? Farming?
| klodolph wrote:
| I'm hoping people won't go crazy with it. Generics are one of
| the things that drive me nuts about the Rust ecosystem... it
| feels like every library in Cargo is based around a bunch of
| types with a bunch of unnecessary type parameters. Like, if I
| want to parse command-line arguments.
|
| What I do want is to stop implementing sort.Interface over and
| over again.
| duped wrote:
| The vast majority of the usage of generics in Rust are as
| replacements for inheritance or dynamic dispatch. It's not
| very different structurally.
| cle wrote:
| Agree, this is my hope. Seeing the same thing play out in
| other language ecosystems though, I'm just not very
| optimistic about it.
| masklinn wrote:
| > Like, if I want to parse command-line arguments.
|
| What command-line parsing libraries have "a bunch of
| unnecessary type parameters"?
|
| Only library I can think of which even has them would be
| structopt, and that's not unnecessary, the entire point of
| the library is to deserialise to a structured type.
|
| And of course rust was designed from the ground up to
| leverage generics, e.g. by design it does not have reflection
| / RTTI, interactions with variable or foreign types are thus
| generics based (often, sometimes trait objects will do though
| they have their own tradeoffs), or it doesn't have
| overloading so while not handing all overloading use cases by
| a long shot it uses conversion traits (aka generics) for
| "convenience" overloads e.g. foo(string) and foo(path) become
| foo<T>(_: T) where T: AsRef<Path>.
| klodolph wrote:
| Yes, Rust was designed from the ground-up to leverage
| generics, and as a result, the ecosystem is full of
| generics. There is _overuse_ of generics in Rust.
|
| > ...by design it does not have reflection / RTTI,..
|
| I don't think of those as comparable features.
|
| Reflection is more or less an alternative way to accomplish
| many of the same things that you get with macros in Rust.
| Where you'd see reflection in C# or Go is for something
| like serializing an object to JSON, and in Rust you'd get
| some library with a macro to do it for you. The Rust
| version _could_ still be completely monomorphic, most of
| the time.
|
| RTTI is just std::any. Rust has it. However, 99% of the
| time, when I'm using dynamic_cast<T>(x) in C++ or switch x
| := x.(type) in Go, I would have been using an enum in Rust.
|
| The convenience overloads for Rust are nice, that's not the
| overuse I'm talking about. I'm talking about the fact that
| some library authors seem to avoid &dyn like it were
| poisonous, even if it _makes total sense_ and would make
| large chunks of your library monomorphic. There are also
| minor code size issues that come about because some library
| authors forget that the convenience overload should
| probably just call an underlying monomorphic function,
| especially for cases like AsRef arguments.
|
| I am under the assumption here that if you really like
| generics, you probably like Rust, and vice versa. This is
| kind of a taste thing, so what is overuse of generics for
| me (hello, C++'s std::chrono, std::mt19937) might be the
| bee's knees for you.
|
| I did a fair bit of Haskell and C++ before Rust, and as I
| got more experience with those languages, I used generics
| less. Monomorphic types are always easier to understand,
| and they should be preferred.
| masklinn wrote:
| > There is overuse of generics in Rust.
|
| You made that claim several time, but still provided
| literally no evidence, just a lot of handwaving and
| careful avoidance of any sort of concrete point.
|
| > I don't think of those as comparable features.
|
| > Reflection is more or less an alternative way to
| accomplish many of the same things that you get with
| macros in Rust.
|
| They go together e.g. where Go uses reflection, Rust uses
| Derive or manually implemented traits and generic
| functions working with those traits. JSON is a pretty
| obvious example there.
|
| > I'm talking about the fact that some library authors
| seem to avoid &dyn like it were poisonous, even if it
| makes total sense and would make large chunks of your
| library monomorphic.
|
| I would not necessarily disagree with that although I
| would word it more kindly: since generics always work,
| they're the default, as a result dynamic dispatch is a
| fallback when either you must have dynamic dispatch or
| someone pointed out that the static dispatch was less
| than optimal (which can be impossible to find out through
| microbenchmarks).
|
| Plus `&dyn` means you're now restricted to borrow-
| ability, and `Box<dyn>` incurs allocation costs. Generic
| function skip both concerns.
| judofyr wrote:
| As someone who's working on code base at ~100k LoC I'd say that
| generics would be great to have:
|
| - We have some utilities (e.g. caching layer) which now either
| needs to use interface{} (and lose all type safety) or hard-
| code the type their dealing with (and thus be duplicated).
|
| - Lack of iterators are annoying. We often need to stream data
| from a database and now we need to introduce a separate
| iterator-interface for each thing that can be streamed (`Next()
| (Entry, error)`). Working with these iterators are clunky since
| we can't write common tooling around it (e.g. "fetch the first
| entry").
| cle wrote:
| I've written quite a few containers that use interface{} like
| that...they don't really "lose all type safety", you write a
| typed container around it (usually a one line implementation)
| that does the type assertion in one spot, and that's it, you
| never deal with interface{} again. For all practical
| purposes, it's just as type safe.
|
| I agree that lack of iterators is one area where there is
| permanent annoyance. Personally, I think that annoyance is a
| lot less than the other problems that come with generics (at
| least in other languages).
|
| I'm not saying there aren't legitimate scenarios where
| generics simplify things. I'm completely convinced that there
| are. But holistically I don't think they outweigh all the
| other baggage that they come with.
| [deleted]
| moldavi wrote:
| I've often wondered about this. Does this technique (a type
| safe wrapper around an interface{}ing class) kind of
| accomplish the same thing that generics do for Java? It
| would result in the same kind of type-erasure in the end,
| I'd assume.
|
| In a weird way, with this technique, did Go have "manual
| generics" all along?
| pharmakom wrote:
| Except you still have to write out the typed
| implementation - even if it is small. In Java this is not
| necessary.
| cratermoon wrote:
| Not really, but aside from the functional aspect that
| tends to come with them, all generics are really saying
| is "this operation will performed on a type to be named
| at runtime", optionally with a constraint that says the
| type-to-be-named-later will adhere to a certain interface
| (contract).
| masklinn wrote:
| > In a weird way, with this technique, did Go have
| "manual generics" all along?
|
| In the exact same way every langage without generics had
| generics all along: not in any meaningful way.
|
| You could also use codegen hook to instantiate your
| pseudo-generics after all: https://www.reddit.com/r/rust/
| comments/5penft/comment/dcsgk7...
| nicoburns wrote:
| > I've written quite a few containers that use interface{}
| like that...they don't really "lose all type safety", you
| write a typed container around it (usually a one line
| implementation) that does the type assertion in one spot,
| and that's it, you never deal with interface{} again. For
| all practical purposes, it's just as type safe.
|
| At that point aren't you just writing out the generics by
| hand? Why would you not want a built in language feature
| for that which saves you the trouble while simultaneously
| ensuring the code is correct.
| [deleted]
| cle wrote:
| You're writing out the _concretization_ by hand. Generics
| as a language feature are much more complicated than
| their concretizations, which is my point. Generics are to
| their concretizations as "-e^(ip)" is to "1".
| nicoburns wrote:
| If you're writing generic code using interface{} and then
| wrapping that in concrete wrappers then you're actually
| doing both!
| cle wrote:
| I'm talking about formal generics, where there are type
| system mechanisms for specifying type parameters and
| constraints. I suppose there are really three things
| we're considering here: the generic implementation that
| operates on some arbitrary type, the concretization that
| has only concrete types, and a generic type specification
| which is only required if the compiler emits the
| concretization instead of the programmer. (I'm ignoring
| interfaces as they aren't really relevant here, that I
| can see.)
|
| What I'm arguing is that for most cases, the
| implementation and concretization are so easy to write by
| hand, are safe enough, and perform well enough, that I
| don't think the downsides of having the compiler emit the
| concretizations are worth it. The downsides being the new
| complex language primitives that have lots of
| consequences that trickle out to the compiler, runtime,
| and ecosystem.
| [deleted]
| hajile wrote:
| That's just going back to dynamic typing.
| cle wrote:
| It's not anywhere close to that. There's a tiny
| expression where we override the type system, but
| everything else in the program is still statically typed,
| and we get to avoid all the complexity of generics.
| jshen wrote:
| I have similar feelings. I've been coding a long time, and the
| longer I do it, the less sure I am about any of this stuff. The
| one thing I can say for sure, I find it relatively easy to jump
| into an arbitrary go code base and make sense of it. I can't
| say this about languages with all the features people are
| clamoring for, and I've worked on large projects in Ruby,
| python, Java, scala, clojure, swift, JavaScript, typescript,
| and a few others.
|
| I'm not sure exactly which qualities/features of Go make it so
| much easier to read, but I worry that change will lead to the
| Go losing what makes it unique and special.
|
| Side note: clojure was oddly the second easiest for me to read.
| gher-shyu3i wrote:
| > I'm not sure exactly which qualities/features of Go make it
| so much easier to read,
|
| In my experience, golang is harder to read. It's overly
| verbose that it's hard to figure out the underlying logic.
| What can be written as items.filter(|n|
| n.price >= 10) .groupBy(|n| n.source)
| .mapValues(|v| v.sum()/v.length())
|
| would be over a dozen lines of code in golang, with
| potentially one off functions everywhere. This only gets more
| obnoxious with larger code bases.
|
| The problem is that even if golang added generics, they won't
| be composable. In their proposal, they proposed adding
| map/filter/etc functions in a separate package, not as
| functions on slices as a proper language would do. The same
| mistake they did with not having any functions on strings,
| but kept them in a separate package.
|
| Also, they need to improve their anonymous function syntax.
| foo("param1", func(a int, b string) bool { return len(b) < a
| }
|
| is verbose, more advanced languages simply shorten it to
| foo("param1", |a, b| b.length() < a)
|
| or something equivalent
| [deleted]
| kaba0 wrote:
| Making sense of a small, detailed part of a codebase is
| meaningless. But too much detail will make it much harder to
| see the big picture, something that may be more apparent in
| more expressive languages.
|
| On a deliberate hyperbole, reading assembly, each line is
| trivial, but it is very very hard to make sense of what does
| it do compared to a line of a high level language.
| edem wrote:
| It is no surprise that Clojure was the easiest to read. It is
| composed of S expressions and some syntactic sugar.
| sudhirj wrote:
| There was a thought expressed in one of the original drafts
| that went along the lines of "the only people worrying about
| generics will be library authors", which I think will and
| should be true. The structure and architecture of existing
| codebases won't change, but the removal of casting to and from
| interface{} when using libraries will be a welcome change.
| whateveracct wrote:
| > I don't like the kinds of discussions it attracts...vague
| hand-wavy arguments about "expressiveness" and "purity".
|
| Pretty much every argument about how to write Go programs is
| hand-wavy.
|
| I'd actually argue (from years of experience at least) that FP
| discussions of code are pretty much the only ones of actual
| substance instead of mostly aesthetics and opinion and
| heuristic.
|
| The best part of writing Haskell professionally is I no longer
| have to deal with hand-wavy programming folk-isms. We can do
| better, and it's not that hard.
| cle wrote:
| Any stylistic discussions are like that, I agree. The
| opinions that Go makes are themselves hand-wavy, like any
| stylistic opinion.
|
| What's important to me is that the decision is already made,
| and when me and my team are writing code, we don't have to
| worry about making those decisions over and over again.
| They're already made for us. IME the costs/benefits of the
| different styles are not even worth the discussion, just pick
| something and move on. This is the aesthetic that I like
| about Go.
| jackcviers3 wrote:
| Exactly. Is it RT? Is it well-typed? Is it as simple as it
| could be? Is there a small number of inhabitants? Ship it.
| Using some weird recursion scheme when fold right will do?
___________________________________________________________________
(page generated 2021-05-27 23:03 UTC)