Allow methods on primitives

214 views
Skip to first unread message

Michael Ellis

unread,
Jun 7, 2019, 2:39:56 PM6/7/19
to golang-nuts
Count me among those who love the language just the way it is and regard the prospect Go 2.0 with dread and loathing born of the Python 3 experience.  

That being said, there's one itty-bitty change I'd like to advocate: Allow methods on primitives.

I think I understand why allowing new methods on external packages is a bad idea because it introduces a form of the fragile superclass problem.  But surely there's nothing fragile about strings, ints and floats.

Being unable to define a method on a string is *not* a showstopper since one can always do "type Foo string" or use type-assertions. It's just that it's inelegant in the case of a function that takes a recursive struct as an argument if the leaves of the tree are strings.  For example

type HTMLTree struct {
    tag string
    attributes string
    content * Content // 
}

type Content interface {
   Render()
}

// NOT ALLOWED
func (s  string) Render() {
}

So I have to do something like

type SC string
func (s SC) Render() {
}

but that means instead of being able to write

Div("", P("id=1", "When in the course of ..."))

one must use the wrapper type on every content string.

Div("", P("id=1", SC("When in the course of ...")))

Not the end of the world, but uglier than it ought to be, IMO.

Is there a way to get around this or, failing that, an explanation of why methods on primitives are verboten?

Thanks!




    

Burak Serdar

unread,
Jun 7, 2019, 3:01:04 PM6/7/19
to Michael Ellis, golang-nuts
If one library defines string.Render() for html, and another defines
another string.Render() for, say, a graphics library, which Render
will be called when I call "str".Render()?

>
> Thanks!
>
>
>
>
>
>
> --
> You received this message because you are subscribed to the Google Groups "golang-nuts" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/6daa53b9-c2e6-48e8-b022-c8339d3b8dbc%40googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

Michael Ellis

unread,
Jun 8, 2019, 5:22:06 PM6/8/19
to golang-nuts

On Friday, June 7, 2019 at 3:01:04 PM UTC-4, Burak Serdar wrote:

If one library defines string.Render() for html, and another defines
another string.Render() for, say, a graphics library, which Render
will be called when I call "str".Render()?

Thanks for the explanation. That makes sense.  Thinking it over, I'm wondering what would be required for the compiler to recognize synonyms of the form 'type Foo Bar' and allow passing a Foo for an argument of type Bar.  The compiler knows the expected type of each function argument so it seems as if checking to see if a passed argument is a simple synonym for the expected type and doing a coercion doesn't seem like a big task.  Note that this is different than asking for, say, numeric conversion from int to float64 because those types are not synonyms.  I actually like the fact that Go forces you to explicitly convert numeric types.


Michael Ellis

unread,
Jun 8, 2019, 5:25:50 PM6/8/19
to golang-nuts
Oops, got that the wrong way round. Should read "allow passing a Bar for argument of type Foo".
Cheers,
Mike

“I want you to act as if the house was on fire. Because it is.” — Greta Thunberg


--
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/L8XXHw9eiqM/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/aa26152a-fc5c-4048-9d30-446960df0ea9%40googlegroups.com.

Wojciech S. Czarnecki

unread,
Jun 8, 2019, 6:46:30 PM6/8/19
to golan...@googlegroups.com
On Sat, 8 Jun 2019 17:25:11 -0400
Michael Ellis <michael...@gmail.com> wrote:

> Oops, got that the wrong way round. Should read "allow passing a Bar for
> argument of type Foo".
type Foo int
type Bar = Foo

func main() {
var fo Foo = 1
var ba Bar = 77
upbar(fo, "Foo")
upbar(ba, "Bar")

}

func upbar(d Foo, s string) {
fmt.Printf("%s is %d\n", s, d)
}

https://play.golang.org/p/rubYGaCep6j


> Cheers,
> Mike

Burak Serdar

unread,
Jun 8, 2019, 11:53:17 PM6/8/19
to Michael Ellis, golang-nuts
If you have:

type A int
type B A

then A and B are not synonyms, they are different types. Because of
Go's type system:

func (a A) SomeFunc() {}
func (b B) SomeFunc() {}


func F(value A) {
value.SomeFunc()
}

x:=B{}
F(x)

With an implicit conversion like you mentioned, the F(x) invocation at
the bottom expects B.SomeFunc() will be called but in fact,
A.SomeFunc() will be called. That's why this is an error. You can
still do F(A(x)), which explicitly makes a copy of x as an A.

This is one of the reasons why it is so easy to read and write Go
code. Most of the time you can comprehend the code with only local
knowledge.


>
>
> --
> You received this message because you are subscribed to the Google Groups "golang-nuts" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/aa26152a-fc5c-4048-9d30-446960df0ea9%40googlegroups.com.

Michael Jones

unread,
Jun 9, 2019, 1:49:36 AM6/9/19
to Burak Serdar, Michael Ellis, golang-nuts
To emphasize the wisdom:

With an implicit conversion like you mentioned, the F(x) invocation at
the bottom expects B.SomeFunc() will be called but in fact,
A.SomeFunc() will be called. That's why this is an error. You can
still do F(A(x)), which explicitly makes a copy of x as an A.

This is not just a "Go style" issue, this is the result of distilling lessons of millions of C++ developer's billions of hours of debugging and large program / large team / long timeline development experience. Things that are missing from Go, or restricted in Go, or even "adversarial" in Go are there mostly because of hard-won experience. Many subtle design virtues are what's missing as much as what's present.


For more options, visit https://groups.google.com/d/optout.


--
Michael T. Jones
michae...@gmail.com

Michael Ellis

unread,
Jun 9, 2019, 9:14:34 AM6/9/19
to golang-nuts
I'm not disputing the wisdom of Go's design. As I said at the top of the initial post, I like Go the way it is and see no need for a Go 2.  

I was trying to find a clean solution to a specific use case:  nestable functions that generate html.  Since new methods on primitives are not allowed (for good reason as I now understand) I wanted to at least inquire about the possibility of loosening the restrictions on a very specific case:  function args whose type is effectively a string (or other primitive).  I get the point the about types being not the same in terms of Go's internal bookkeeping. However, in this particular narrow case I don't see a hazard to maintainability because the rule forbidding new methods on primitives excludes any possibility of confusion within the body of the function.

To my mind, having to define a type equivalent to a primitive solely for the purpose of implementing an interface detracts from readability.  But given that there's good reason for the requirement I think being able to pass the equivalent primitive as a function argument of that type seems like a potentially safe way to make the code more readable.

Just my $0.02


On Sunday, June 9, 2019 at 1:49:36 AM UTC-4, Michael Jones wrote:
To emphasize the wisdom:

With an implicit conversion like you mentioned, the F(x) invocation at
the bottom expects B.SomeFunc() will be called but in fact,
A.SomeFunc() will be called. That's why this is an error. You can
still do F(A(x)), which explicitly makes a copy of x as an A.

This is not just a "Go style" issue, this is the result of distilling lessons of millions of C++ developer's billions of hours of debugging and large program / large team / long timeline development experience. Things that are missing from Go, or restricted in Go, or even "adversarial" in Go are there mostly because of hard-won experience. Many subtle design virtues are what's missing as much as what's present.

On Sat, Jun 8, 2019 at 8:53 PM Burak Serdar <bse...@ieee.org> wrote:
> To unsubscribe from this group and stop receiving emails from it, send an email to golan...@googlegroups.com.

> To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/aa26152a-fc5c-4048-9d30-446960df0ea9%40googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golan...@googlegroups.com.

Bakul Shah

unread,
Jun 9, 2019, 9:56:43 AM6/9/19
to Michael Ellis, golang-nuts
On Jun 9, 2019, at 6:14 AM, Michael Ellis <michael...@gmail.com> wrote:
>
> I'm not disputing the wisdom of Go's design. As I said at the top of the initial post, I like Go the way it is and see no need for a Go 2.
>
> I was trying to find a clean solution to a specific use case: nestable functions that generate html. Since new methods on primitives are not allowed (for good reason as I now understand) I wanted to at least inquire about the possibility of loosening the restrictions on a very specific case: function args whose type is effectively a string (or other primitive). I get the point the about types being not the same in terms of Go's internal bookkeeping. However, in this particular narrow case I don't see a hazard to maintainability because the rule forbidding new methods on primitives excludes any possibility of confusion within the body of the function.
>
> To my mind, having to define a type equivalent to a primitive solely for the purpose of implementing an interface detracts from readability. But given that there's good reason for the requirement I think being able to pass the equivalent primitive as a function argument of that type seems like a potentially safe way to make the code more readable.

On the flip side, when someone does
fmt.Printf("%v", someString)
I know exactly what will be printed; it doesn't depend on the context.

You are almost always going to call a string's Render function
(as you defined it in your original post) from a parent HTMLTree
struct' Render(), almost never in isolation -- except perhaps some
tests. So one suggestion is to deal with string rendering issues
in the parent HTMLTree struct's Render(). Now you can still say

Div("", P("id=1", "When in the course of ..."))

But you can use, e.g. Printf("%v", html.tag) and something different
occur. Another option is to have functions like Div and P accept
normal strings in convert them to application specific strings in
the function body.

In other words, the builtin types and the standard Go library provide
*no-surprises* behavior but that may not be enough for any application
specific needs and it is better to just build what you need without
changing the behavior of standard components that everyone relies on.

Michael Ellis

unread,
Jun 9, 2019, 10:42:43 AM6/9/19
to golang-nuts


On Sunday, June 9, 2019 at 9:56:43 AM UTC-4, Bakul Shah wrote:

You are almost always going to call a string's Render function
(as you defined it in your original post) from a parent HTMLTree
struct' Render(), almost never in isolation -- except perhaps some
tests. So one suggestion is to deal with string rendering issues
in the parent HTMLTree struct's Render(). Now you can still say

  Div("", P("id=1", "When in the course of ..."))


Perhaps I'm misunderstanding, but I don't see how to make that work for tags that can take both text or other elements without resorting to making the content arguments of type interface{}. That's doable and the performance penalty is negligible but it discards the benefit of Go's compile time type checks.

For reference, I've pasted together the guts of my html rendering code, defined a couple of tag functions and added a short main to illustrate what I'm doing at https://play.golang.org/p/_qZ7Oyv2Foa

Any suggestions for cleanly allowing passing strings appreciated.

Burak Serdar

unread,
Jun 9, 2019, 12:54:54 PM6/9/19
to Michael Ellis, golang-nuts
If you absolutely do not want to use wrapper structs for string
content, and if Render is all Content has, then maybe you could use
fmt.Stringer instead of Content, and have a separate Render function
instead of a Render method. You could also have Render declared at the
ElementTree level.


>
> --
> You received this message because you are subscribed to the Google Groups "golang-nuts" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/c2d5558c-c5ae-42f2-ba5f-f512f9f765bf%40googlegroups.com.

Bakul Shah

unread,
Jun 9, 2019, 1:37:53 PM6/9/19
to Michael Ellis, golang-nuts
Actually this is a good candidate for sum-types, something I have
wanted in Go (and IMHO something that fits in well). So for example:

type Tree struct {
T, A string
C []Content
empty bool
}

type Content_ interface {
Render(b *bytes.Buffer, nindent int) error
}

type Content = Content_ | string
...
Div("", "Here's a link", A("href=someURL", "to some URL"), "and so on").Render(&b, 0)

This gives you better compile time checking and the same
convenience as using interface{}. Unfortunately....

Michael Ellis

unread,
Jun 9, 2019, 2:26:08 PM6/9/19
to Bakul Shah, golang-nuts
type Content = Content_ | string

That's a nice notation (and semantic). It certainly makes sense for this case.  Count me as a vote in favor.



“I want you to act as if the house was on fire. Because it is.” — Greta Thunberg

Steven Blenkinsop

unread,
Jun 9, 2019, 4:00:27 PM6/9/19
to Burak Serdar, Michael Ellis, golang-nuts
If one library defines string.Render() for html, and another defines another string.Render() for, say, a graphics library, which Render will be called when I call "str".Render()?

Method call syntax is the least of the worries. Presumably, that could be based on which packages are imported in the calling context, and you'd get an error if it's ambiguous. The larger problem is when you get interfaces involved. Two Content variables storing the same value of type string could have different implementations of Render depending on where the interface value was created. And if a variable v of type Content is used in a way equivalent to interface{}(v).(Content), you could end up changing the implementation of Render or failing the assertion altogether. So, the problem with allowing an arbitrary package to define methods on primitive types isn't with method call syntax, it's with using such methods to satisfy an interface like Content.

One workaround would be to allow you to use package-qualified method names, so

package foo

type Content interface {
    package.Render()
}

func (s string) package.Render() { ... }

At minimum, package-qualified methods defined in other packages couldn't be used to satisfy Content. Normal Render methods defined in other packages (for types which have to be defined in those packages) could be allowed to satisfy the interface, and/or other packages could be allowed to define methods like

func (t T) foo.Render() { ... }

for their own types to satisfy the interface.

There hasn't been much interest in pursuing anything like this for Go, however. Something like the sum types mentioned by Bakul might be a better match for existing Go idioms, since it could be a swap-in replacement for interface{} in existing functions that use a type switch with an interface fallback, allowing them to better specify which types they support. Of course, there's also been opposition to adding sum types, since many see interfaces as "good enough" to cover the use cases of sum types.

Michael Jones

unread,
Jun 10, 2019, 2:11:36 AM6/10/19
to Steven Blenkinsop, Burak Serdar, Michael Ellis, golang-nuts
The Go2 proposal process is open minded. Suggest that every well-considered suggestion be made. Personally, I’m no fan of idiomatic as an argument, it seems more of a consequence—like bare earth paths worn through grassy parks—when the paved sidewalks are in the wrong place, the idiomatic path goes off-road. When there are guardrails preventing that, the idiomatic path is wrong, just the closest allowed fit. Go2 is the time to ignore the rails. 

I miss discriminated unions too. (Fancy new name, “sum types”). People like enumerations. There are many odd to Go things that if added, would quickly become second nature: letting += and * etc apply to arrays and slices would be natural, smoothly integrate the myriad ugly named vector operations, and make such code 2x, 4x, 8x faster. It would be a shame to hold back suggesting it because “operators on non-primitive types are disallowed.”

Be bold. I’m saying this because in reading this thread I think my comment about hard-won lessons may have come across wrong. It is not that Go is perfect and should not be changed, more (to me) like Go’s omissions tend to me much more strategic than people recognize, so the way to add the benefit that’s missing is not usually to add the feature with that benefit from other languages. That should motivate new ways, not shut down changes. But those new ways require free thinking. 


For more options, visit https://groups.google.com/d/optout.
--
Michael T. Jones
michae...@gmail.com

Jesper Louis Andersen

unread,
Jun 10, 2019, 7:07:52 AM6/10/19
to Michael Jones, Steven Blenkinsop, Burak Serdar, Michael Ellis, golang-nuts
On Mon, Jun 10, 2019 at 8:11 AM Michael Jones <michae...@gmail.com> wrote:
I miss discriminated unions too. (Fancy new name, “sum types”).

They are called sum types because they work as an "addition" like construction in the type theory. Their dual, product types, are what people usually call records or structs in many languages. Structs work as "multiplication" in the type theory.

For some reason, many languages in the mainstream can multiply at the type level, but they cannot add. It looks like the newer generation, Rust, Swift, ... start looking into these in earnest as additions, and this is welcome. 

There are some interesting corollaries from having sum types:

There is no boolean anymore. A boolean is simply

type bool = true | false

and you can also remove the if-statement from the language since the elimination form, 

match x with
| true -> ExpT
| false -> ExpF

is essentially an if-statement. Having to add something like match seems odd, but it isn't! It's dual is the projection operation '.' on structs. Suppose

type GeoPoint struct {
  Lat, Lng float64
}
var gp GeoPoint

then `gp.Lat` is the elimination form.

Next, there is no need for a null value anymore:

forall 'a.
  type 'a option = None | Some 'a

Granted, you need polymorphism as well here, but once you have this, you can remove the concept of null for the value 'none'. Thus, the default in the language is that no value can be null, unless you wrap it into an option. Continuing, you can define

type ('a, 'b) result = Ok of 'a | Error of 'b

which is a clean way of returning errors from functions. In Go, we have to return a product pair such as `return v, nil` or `return nil, err`, but of course, people forget to check the error. With a sum, that kind of mistake cannot happen, since you are forced to match on the erroneous case. This is also what leads you to have programmable control flow eventually; what people call a whole slew of things: monads, applicatives, ... The recent `try` proposal for Go 2.0 is an extreme special case of this, where control flow is specialized to the above situation.

Personal bet: over time, mainstream languages will get sum types, at least in a limited form. It is as if programmers are writing expressions in a logic where they have && (AND) but no || (OR). You can do it because you have if-statements, but it gets really tedious to write. At some point, it is going to be "generally accepted" at which point languages without sum types are going to be regarded as a relic of the past. Also, it rounds out the logic of the programming language, and makes it internally consistent. 

Jan Mercl

unread,
Jun 10, 2019, 7:35:06 AM6/10/19
to Jesper Louis Andersen, Michael Jones, Steven Blenkinsop, Burak Serdar, Michael Ellis, golang-nuts


On Mon, Jun 10, 2019, 13:07 Jesper Louis Andersen <jesper.lou...@gmail.com> wrote:
At some point, it is going to be "generally accepted" at which point languages without sum types are going to be regarded as a relic of the past. 

I hope to be dead for a long time then.

Don't get me wrong, the advantages are clear. But I think it would be horrible to blindly apply this approach everywhere and consider anything else a relic.

Jesper Louis Andersen

unread,
Jun 10, 2019, 11:10:56 AM6/10/19
to Jan Mercl, Michael Jones, Steven Blenkinsop, Burak Serdar, Michael Ellis, golang-nuts
Let me moderate it a bit then.

Programming languages never cease to exist. There is far too much code out there for this to happen. You may consider a language a relic, yet there are still people doing useful work in those languages. Either because the language has some unique features which are hard to replicate (Prolog, APL, and Forth comes to mind), or because the body of code in those languages is so large that it acts like inertia by itself.

As a general rule of thumb, it looks like the incubation time for a PL concept is about 40 years. This was true with garbage collection, and might also hold for sum types. Hopefully we'll get Type&Effect systems before I stop programming.

--
J.

Burak Serdar

unread,
Jun 10, 2019, 11:39:31 AM6/10/19
to Jesper Louis Andersen, Jan Mercl, Michael Jones, Steven Blenkinsop, Michael Ellis, golang-nuts
On Mon, Jun 10, 2019 at 9:10 AM Jesper Louis Andersen
In my opinion, these things should be decided not based on how the
code is written but based on how it is read. The "sum types" idea
looks interesting even though I don't have a full grasp of it, but it
seems to hide a bit too much. Code that may run in different ways
based on declarations made somewhere else makes it easier to
misunderstand code.

>
> --
> J.
Reply all
Reply to author
Forward
0 new messages